はじめに
分散ロックは分散アプリケーションで広く使用されています。新しいことを理解したい場合は、まずその起源を理解する必要があります。そうすることで、それをより深く理解し、推論することもできます。
まず、分散ロックについて話すとき、当然分散アプリケーションのことを思い浮かべます。
アプリケーションを分散アプリケーションに分割する前のスタンドアロン システムでは、在庫の差し引きやチケットの販売など、いくつかの同時シナリオでパブリック リソースを読み取るときに、同期またはロックを使用するだけで実現できます。 。
しかし、アプリケーションの配布後、システムはそれまでのシングルプロセス、マルチスレッドのプログラムからマルチプロセス、マルチスレッドのプログラムに変化するため、上記の解決策では明らかに不十分です。
したがって、業界で一般的に使用されるソリューションは、通常、サードパーティのコンポーネントに依存し、独自の排他性を使用して複数のプロセスの相互排他を実現することです。例:
DB に基づく一意のインデックス。
ZK に基づいた一時的な順序付けされたノード。
Redis の NX EX
パラメータに基づきます。
ここでの説明は主に Redis に基づいています。
Redis が選択されているため、排他的である必要があります。同時に、ロックのいくつかの基本特性を備えていることが最善です。
高パフォーマンス (追加およびロック解除時の高パフォーマンス)
ブロッキング ロックはノンブロッキング ロックと併用できます。
Redis set key で使用される NX パラメーターにより、キーが存在しない場合でも確実に書き込みを成功させることができます。また、EX パラメータを追加すると、タイムアウト後にキーを自動的に削除できます。
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 コマンドを使用します。
が自分のものであるかどうかを判断することです。
現時点では、ロック機構と組み合わせて実装する必要があります。 ロックするときにパラメータを渡す必要があり、このパラメータをこのキーの値として使用することで、ロックを解除するたびに値が等しいかどうかを判断できます。 したがって、ロック解除コードを単純に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 つの操作のアトミック性を確保することもできます。
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) に強く依存しているため、単一のテストではこの依存関係を除外する必要があります。たとえば、別のパートナーがプロジェクトをフォークし、単一のテストをローカルで実行しようとしましたが、結果は実行に失敗しました:
したがって、単一のテスト ツール
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 的话可以很直接的看出来:
这里我们所依赖的 JedisCluster 其实是一个 cglib 代理对象
。所以也不难想到它是如何工作的。
比如这里我们需要用到 JedisCluster 的 set 函数并需要它的返回值。
Mock 就将该对象代理了,并在实际执行 set 方法后给你返回了一个你自定义的值。
这样我们就可以随心所欲的测试了,完全把外部依赖所屏蔽了。
至此一个基于 Redis 的分布式锁完成,但是依然有些问题。
如在 key 超时之后业务并没有执行完毕但却自动释放锁了,这样就会导致并发问题。
就算 Redis 是集群部署的,如果每个节点都只是 master 没有 slave,那么 master 宕机时该节点上的所有 key 在那一时刻都相当于是释放锁了,这样也会出现并发问题。就算是有 slave 节点,但如果在数据同步到 salve 之前 master 宕机也是会出现上面的问题。
感兴趣的朋友还可以参考 Redisson 的实现。
以上がRedis に基づいて分散ロックを実装する方法の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。