この記事では、Redis を使用して安全で信頼性の高い分散ロックを実装する方法を紹介し、分散ロック実装の主な要素とよくある誤解について説明します。一定の参考値があるので、困っている友達が参考になれば幸いです。
# 同時シナリオでは、複数のプロセスまたはスレッドが読み取りと書き込みのためにリソースを共有する場合、リソースへのアクセスは相互に排他的であることが保証される必要があります。スタンドアロン システムでは、Java 同時実行パッケージの API や同期キーワードなどを使用して問題を解決できますが、分散システムではこれらの方法は適用できなくなり、分散ロックを自分で実装する必要があります。 。
一般的な分散ロック実装ソリューションには、データベース ベース、Redis ベース、Zookeeper ベースなどがあります。 Redis トピックの一部として、この記事では Redis に基づく分散ロックの実装について説明します。 [関連する推奨事項: Redis ビデオ チュートリアル ]
分散ロックと JVM 組み込みロックの目的は同じです。アプリケーションが予期された順序で共有リソースにアクセスまたは操作できるようにし、複数のスレッドが同じリソースを同時に操作してシステムに問題が発生するのを防ぐことです。無秩序かつ制御不能に走ること。製品在庫の控除やクーポンの控除などのシナリオでよく使用されます。
理論的には、ロックのセキュリティと有効性を確保するには、分散ロックは少なくとも次の条件を満たす必要があります。
実装の観点から、分散ロックは大きく 3 つのステップに分かれています:
Java の組み込みロックか分散ロックかは問題ではありません。どの分散実装ソリューションが使用されるかは、2 つのステップ a と c によって異なります。 Redis は、次の理由により分散ロックの実装に適しています:
SET キー値 NX PX ミリ秒
コマンドは、キーが存在しない場合に有効期限付きキーを追加して、セキュリティ ロックのサポートを提供します。 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>${your-spring-boot-version}</version> </dependency>
spring.redis.database=0 spring.redis.host=localhost spring.redis.port=6379
@Configuration public class RedisConfig { // 自己定义了一个 RedisTemplate @Bean @SuppressWarnings("all") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) throws UnknownHostException { // 我们为了自己开发方便,一般直接使用 <String, Object> RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory(factory); // Json序列化配置 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // String 的序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); // hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }
@Service public class RedisLock { @Resource private RedisTemplate<String, Object> redisTemplate; /** * 加锁,最多等待maxWait毫秒 * * @param lockKey 锁定key * @param lockValue 锁定value * @param timeout 锁定时长(毫秒) * @param maxWait 加锁等待时间(毫秒) * @return true-成功,false-失败 */ public boolean tryAcquire(String lockKey, String lockValue, int timeout, long maxWait) { long start = System.currentTimeMillis(); while (true) { // 尝试加锁 Boolean ret = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, timeout, TimeUnit.MILLISECONDS); if (!ObjectUtils.isEmpty(ret) && ret) { return true; } // 计算已经等待的时间 long now = System.currentTimeMillis(); if (now - start > maxWait) { return false; } try { Thread.sleep(200); } catch (Exception ex) { return false; } } } /** * 释放锁 * * @param lockKey 锁定key * @param lockValue 锁定value * @return true-成功,false-失败 */ public boolean releaseLock(String lockKey, String lockValue) { // lua脚本 String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"; DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class); Long result = redisTemplate.opsForValue().getOperations().execute(redisScript, Collections.singletonList(lockKey), lockValue); return result != null && result > 0L; } }
@SpringBootTest class RedisDistLockDemoApplicationTests { @Resource private RedisLock redisLock; @Test public void testLock() { redisLock.tryAcquire("abcd", "abcd", 5 * 60 * 1000, 5 * 1000); redisLock.releaseLock("abcd", "abcd"); } }
set
コマンドPX
ロックするオプション。ロックの相互排他を確保し、デッドロックを回避します。
他のスレッドのロックを解除するために、lua スクリプトを使用してロックを解除します。 ;
ロックおよびロック解除コマンドはすべてアトミック操作です; しかし、センチネル モードとクラスター モードでは問題が発生する可能性があります。なぜでしょうか? Sentinel モードとクラスター モードはマスター/スレーブ アーキテクチャに基づいており、マスターとスレーブの間でコマンドの伝播を通じてデータの同期が実現され、コマンドの伝播は非同期です。
したがって、マスター ノードのデータは正常に書き込まれても、スレーブ ノードに通知される前にマスター ノードがダウンしてしまう可能性があります。
スレーブ ノードがフェイルオーバーを通じて新しいマスター ノードに昇格すると、他のスレッドが正常に再ロックする機会があり、その結果、分散ロックの相互排他条件が満たされなくなります。
公式 RedLockロックを取得するために、クライアントは次のプロセスに従います:
RedLock的设计思路延续了Redis内部多种场景的投票方案,通过多个实例分别加锁解决竞态问题,虽然加锁消耗了时间,但是消除了主从机制下的安全问题。
官方推荐Java实现为Redisson,它具备可重入特性,按照RedLock进行实现,支持独立实例模式、集群模式、主从模式、哨兵模式等;API比较简单,上手容易。示例如下(直接通过测试用例):
@Test public void testRedLock() throws InterruptedException { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); final RedissonClient client = Redisson.create(config); // 获取锁实例 final RLock lock = client.getLock("test-lock"); // 加锁 lock.lock(60 * 1000, TimeUnit.MILLISECONDS); try { // 假装做些什么事情 Thread.sleep(50 * 1000); } catch (Exception ex) { ex.printStackTrace(); } finally { //解锁 lock.unlock(); } }
Redisson封装的非常好,我们可以像使用Java内置的锁一样去使用,代码简洁的不能再少了。关于Redisson源码的分析,网上有很多文章大家可以找找看。
分布式锁是我们研发过程中常用的的一种解决并发问题的方式,Redis是只是一种实现方式。
关键的是要弄清楚加锁、解锁背后的原理,以及实现分布式锁需要解决的核心问题,同时考虑我们所采用的中间件有什么特性可以支撑。了解这些后,实现起来就不是什么问题了。
学习了RedLock的思想,我们是不是也可以在自己的应用程序内实现了分布式锁呢?欢迎沟通!
更多编程相关知识,请访问:编程入门!!
以上がRedis を使用して安全で信頼性の高い分散ロックを実装するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。