ホームページ  >  記事  >  バックエンド開発  >  Redis に基づいて分散ロックを実装する方法

Redis に基づいて分散ロックを実装する方法

无忌哥哥
无忌哥哥オリジナル
2018-07-20 09:14:481515ブラウズ

はじめに

分散ロックは分散アプリケーションで広く使用されています。新しいことを理解したい場合は、まずその起源を理解する必要があります。そうすることで、それをより深く理解し、推論することもできます。

まず、分散ロックについて話すとき、当然分散アプリケーションのことを思い浮かべます。

アプリケーションを分散アプリケーションに分割する前のスタンドアロン システムでは、在庫の差し引きやチケットの販売など、いくつかの同時シナリオでパブリック リソースを読み取るときに、同期またはロックを使用するだけで実現できます。 。

しかし、アプリケーションの配布後、システムはそれまでのシングルプロセス、マルチスレッドのプログラムからマルチプロセス、マルチスレッドのプログラムに変化するため、上記の解決策では明らかに不十分です。

したがって、業界で一般的に使用されるソリューションは、通常、サードパーティのコンポーネントに依存し、独自の排他性を使用して複数のプロセスの相互排他を実現することです。例:

  • DB に基づく一意のインデックス。

  • ZK に基づいた一時的な順序付けされたノード。

  • Redis の NX EX パラメータに基づきます。

ここでの説明は主に Redis に基づいています。

実装

Redis が選択されているため、排他的である必要があります。同時に、ロックのいくつかの基本特性を備えていることが最善です。

  • 高パフォーマンス (追加およびロック解除時の高パフォーマンス)

  • ブロッキング ロックはノンブロッキング ロックと併用できます。

  • #デッドロックは発生しません。

  • 可用性 (ノードがダウンした後にロックが失敗することはありません)。

ここでは、

Redis set key で使用される NX パラメーターにより、キーが存在しない場合でも確実に書き込みを成功させることができます。また、EX パラメータを追加すると、タイムアウト後にキーを自動的に削除できます。

したがって、上記の 2 つの機能を使用すると、同時に 1 つのプロセスのみがロックを取得することが保証され、デッドロックは発生しません (最悪の場合、キーはタイムアウト後に自動的に削除されます)。

Lock

実装コードは次のとおりです。

    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";
    
    public  boolean tryLock(String key, String request) {
        String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);

        if (LOCK_MSG.equals(result)){
            return true ;
        }else {
            return false ;
        }
    }

ここで使用されている jedis の

String set(String key, String value, String nxxx, String expx, long time);

api に注意してください。

このコマンドは、NX EX のアトミック性を保証できます。

2 つのコマンド (NX EX) を別々に実行しないように注意してください。NX 以降のプログラムに問題がある場合、デッドロックが発生する可能性があります。

ブロッキング ロック

同時に、ブロッキング ロックも実装できます:

    //一直阻塞
    public void lock(String key, String request) throws InterruptedException {

        for (;;){
            String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
            if (LOCK_MSG.equals(result)){
                break ;
            }
                
              //防止一直消耗 CPU  
            Thread.sleep(DEFAULT_SLEEP_TIME) ;
        }

    }
    
     //自定义阻塞时间
     public boolean lock(String key, String request,int blockTime) throws InterruptedException {

        while (blockTime >= 0){

            String result = this.jedis.set(LOCK_PREFIX + key, request, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, 10 * TIME);
            if (LOCK_MSG.equals(result)){
                return true ;
            }
            blockTime -= DEFAULT_SLEEP_TIME ;

            Thread.sleep(DEFAULT_SLEEP_TIME) ;
        }
        return false ;
    }

Unlocking

ロック解除も非常に簡単です。キーを削除するだけで問題は解決します。たとえば、

del key コマンドを使用します。

しかし、現実はそれほど簡単ではないことがよくあります。

プロセス A がロックを取得してタイムアウトを設定した場合、実行サイクルが長いため、タイムアウト後にロックは自動的に解放されます。このとき、プロセスBがロックを取得し、すぐにロックを解放します。このようにして、プロセス B はプロセス A のロックを解放します。

したがって、最良の方法は、ロックを解除するたびに、そのロック

が自分のものであるかどうかを判断することです

現時点では、ロック機構と組み合わせて実装する必要があります。

ロックするときにパラメータを渡す必要があり、このパラメータをこのキーの値として使用することで、ロックを解除するたびに値が等しいかどうかを判断できます。

したがって、ロック解除コードを単純に

del にすることはできません。

    public  boolean unlock(String key,String request){
        //lua script
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

        Object result = null ;
        if (jedis instanceof Jedis){
            result = ((Jedis)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
        }else if (jedis instanceof JedisCluster){
            result = ((JedisCluster)this.jedis).eval(script, Collections.singletonList(LOCK_PREFIX + key), Collections.singletonList(request));
        }else {
            //throw new RuntimeException("instance is error") ;
            return false ;
        }

        if (UNLOCK_MSG.equals(result)){
            return true ;
        }else {
            return false ;
        }
    }

A

lua スクリプトは、値が等しいかどうかを判断するためにここで使用され、値が等しい場合にのみ del コマンドが実行されます。

lua を使用すると、ここで 2 つの操作のアトミック性を確保することもできます。

したがって、上記の 4 つの基本機能も満たすことができます。

  • Redis を使用すると、パフォーマンスを確保できます。

  • ブロッキング ロックと非ブロッキング ロックについては上記を参照してください。

  • タイムアウト メカニズムを使用してデッドロックを解決します。

  • Redis は、可用性を向上させるためにクラスターのデプロイメントをサポートしています。

  • #使用方法

私自身が完全な実装を持っており、運用環境で使用されています。興味のある友人は、すぐに使用できます:

maven 依存関係:

<dependency>
    <groupId>top.crossoverjie.opensource</groupId>
    <artifactId>distributed-redis-lock</artifactId>
    <version>1.0.0</version>
</dependency>

構成 Bean:

@Configuration
public class RedisLockConfig {

    @Bean
    public RedisLock build(){
        RedisLock redisLock = new RedisLock() ;
        HostAndPort hostAndPort = new HostAndPort("127.0.0.1",7000) ;
        JedisCluster jedisCluster = new JedisCluster(hostAndPort) ;
        // Jedis 或 JedisCluster 都可以
        redisLock.setJedisCluster(jedisCluster) ;
        return redisLock ;
    }

}

使用法:

    @Autowired
    private RedisLock redisLock ;

    public void use() {
        String key = "key";
        String request = UUID.randomUUID().toString();
        try {
            boolean locktest = redisLock.tryLock(key, request);
            if (!locktest) {
                System.out.println("locked error");
                return;
            }


            //do something

        } finally {
            redisLock.unlock(key,request) ;
        }

    }

使い方は非常に簡単です。ここでの主な目的は、Spring を使用して RedisLock シングルトン Bean の管理を支援することです。そのため、ロックを解放するときは、キーとリクエストを手動で渡す必要があります (コンテキスト全体に RedisLock インスタンスが 1 つしかないため) (API は特にエレガントです)。

新しい RedisLock を作成し、キーを渡してロックを使用するたびにリクエストすることもでき、ロックを解除するときに非常に便利です。ただし、RedisLock インスタンスは自分で管理する必要があります。それぞれに独自の長所と短所があります。

単一テスト

このプロジェクトに取り組んでいる間、

単一テスト

について言及する必要があります。 このアプリケーションはサードパーティ コンポーネント (Redis) に強く依存しているため、単一のテストではこの依存関係を除外する必要があります。たとえば、別のパートナーがプロジェクトをフォークし、単一のテストをローカルで実行しようとしましたが、結果は実行に失敗しました:

    Redis の IP とポートがそれらのものと一致していない可能性があります。単一のテストで。
  1. Redis 自体にも問題がある可能性があります。
  2. 学生の環境に Redis がない可能性もあります。
  3. したがって、これらの外部の不安定な要因を排除し、作成したコードのみをテストすることが最善です。

したがって、単一のテスト ツール

Mock

を導入できます。 アイデアは非常に単純です。つまり、依存するすべての外部リソースをブロックするということです。例: データベース、外部インターフェイス、外部ファイルなど。

使用方式也挺简单,可以参考该项目的单测:

    @Test
    public void tryLock() throws Exception {
        String key = "test";
        String request = UUID.randomUUID().toString();
        Mockito.when(jedisCluster.set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
                Mockito.anyString(), Mockito.anyLong())).thenReturn("OK");

        boolean locktest = redisLock.tryLock(key, request);
        System.out.println("locktest=" + locktest);

        Assert.assertTrue(locktest);

        //check
        Mockito.verify(jedisCluster).set(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(),
                Mockito.anyString(), Mockito.anyLong());
    }

这里只是简单演示下,可以的话下次仔细分析分析。

它的原理其实也挺简单,debug 的话可以很直接的看出来:

Redis に基づいて分散ロックを実装する方法

这里我们所依赖的 JedisCluster 其实是一个 cglib 代理对象。所以也不难想到它是如何工作的。

比如这里我们需要用到 JedisCluster 的 set 函数并需要它的返回值。

Mock 就将该对象代理了,并在实际执行 set 方法后给你返回了一个你自定义的值。

这样我们就可以随心所欲的测试了,完全把外部依赖所屏蔽了

总结

至此一个基于 Redis 的分布式锁完成,但是依然有些问题。

  • 如在 key 超时之后业务并没有执行完毕但却自动释放锁了,这样就会导致并发问题。

  • 就算 Redis 是集群部署的,如果每个节点都只是 master 没有 slave,那么 master 宕机时该节点上的所有 key 在那一时刻都相当于是释放锁了,这样也会出现并发问题。就算是有 slave 节点,但如果在数据同步到 salve 之前 master 宕机也是会出现上面的问题。

感兴趣的朋友还可以参考 Redisson 的实现。

以上がRedis に基づいて分散ロックを実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。