ホームページ  >  記事  >  データベース  >  Redis を使用して安全で信頼性の高い分散ロックを実装する

Redis を使用して安全で信頼性の高い分散ロックを実装する

青灯夜游
青灯夜游転載
2021-04-19 10:20:321968ブラウズ

この記事では、Redis を使用して安全で信頼性の高い分散ロックを実装する方法を紹介し、分散ロック実装の主な要素とよくある誤解について説明します。一定の参考値があるので、困っている友達が参考になれば幸いです。

Redis を使用して安全で信頼性の高い分散ロックを実装する

# 同時シナリオでは、複数のプロセスまたはスレッドが読み取りと書き込みのためにリソースを共有する場合、リソースへのアクセスは相互に排他的であることが保証される必要があります。スタンドアロン システムでは、Java 同時実行パッケージの API や同期キーワードなどを使用して問題を解決できますが、分散システムではこれらの方法は適用できなくなり、分散ロックを自分で実装する必要があります。 。

一般的な分散ロック実装ソリューションには、データベース ベース、Redis ベース、Zookeeper ベースなどがあります。 Redis トピックの一部として、この記事では Redis に基づく分散ロックの実装について説明します。 [関連する推奨事項: Redis ビデオ チュートリアル ]

分析と実装


問題分析

分散ロックと JVM 組み込みロックの目的は同じです。アプリケーションが予期された順序で共有リソースにアクセスまたは操作できるようにし、複数のスレッドが同じリソースを同時に操作してシステムに問題が発生するのを防ぐことです。無秩序かつ制御不能に走ること。製品在庫の控除やクーポンの控除などのシナリオでよく使用されます。

理論的には、ロックのセキュリティと有効性を確保するには、分散ロックは少なくとも次の条件を満たす必要があります。

  • 相互排他性: 同時に、スレッドがロックを取得できるのは 1 つだけです;
  • デッドロックなし: スレッドがロックを取得した後、解放されることが保証されている必要があります。スレッドがロックを取得した後にアプリケーションがダウンした場合でも、デッドロックが発生する可能性があります。限られた時間内にリリース;
  • 追加 ロックとロック解除は同じスレッド内にある必要があります;

実装の観点から、分散ロックは大きく 3 つのステップに分かれています:

  • a-リソースの操作権を取得する;
  • b-リソースに対して操作を実行する;
  • c-リソースの操作権を解放する;

Java の組み込みロックか分散ロックかは問題ではありません。どの分散実装ソリューションが使用されるかは、2 つのステップ a と c によって異なります。 Redis は、次の理由により分散ロックの実装に適しています:

  • Redis は、コマンド処理フェーズでシングル スレッド処理を使用します。同じキーを同時に処理できるのは 1 つのスレッドだけであるため、マルチスレッドの競合状態の問題はありません。
  • SET キー値 NX PX ミリ秒 コマンドは、キーが存在しない場合に有効期限付きキーを追加して、セキュリティ ロックのサポートを提供します。
  • Lua スクリプトと DEL コマンドは、安全なロック解除のための信頼できるサポートを提供します。

#コードの実装

#Maven の依存関係
    #
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
      	<version>${your-spring-boot-version}</version>
    </dependency>
  • #設定ファイル
    次の内容をスタンドアロン Redis インスタンスの application.properties に追加します。
  • spring.redis.database=0
    spring.redis.host=localhost
    spring.redis.port=6379
RedisConfig

@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;
    }
}
  • RedisLock
@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(&#39;get&#39;,KEYS[1]) == ARGV[1] then return redis.call(&#39;del&#39;,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");
    }
}
  • 安全上のリスク

おそらく多くの学生 (私を含む) が、安全と思われる上記の実装方法を日常業務で使用しています。

Use

set

コマンド
    NX
  • PX ロックするオプション。ロックの相互排他を確保し、デッドロックを回避します。他のスレッドのロックを解除するために、lua スクリプトを使用してロックを解除します。 ; ロックおよびロック解除コマンドはすべてアトミック操作です;
  • 実際、上記の実装が安定するには前提条件があります: Redis のスタンドアロン バージョン、AOF 永続性の有効化モードを変更し、
  • appendfsync=always
  • を設定します。

しかし、センチネル モードとクラスター モードでは問題が発生する可能性があります。なぜでしょうか? Sentinel モードとクラスター モードはマスター/スレーブ アーキテクチャに基づいており、マスターとスレーブの間でコマンドの伝播を通じてデータの同期が実現され、コマンドの伝播は非同期です。

したがって、マスター ノードのデータは正常に書き込まれても、スレーブ ノードに通知される前にマスター ノードがダウンしてしまう可能性があります。

スレーブ ノードがフェイルオーバーを通じて新しいマスター ノードに昇格すると、他のスレッドが正常に再ロックする機会があり、その結果、分散ロックの相互排他条件が満たされなくなります。

公式 RedLock

クラスター モードでは、クラスター内のすべてのノードが安定して実行され、フェイルオーバーが発生しない場合、セキュリティが保証されます。ただし、100% の安定性を保証できるシステムはなく、Redis に基づく分散ロックではフォールト トレランスを考慮する必要があります。


マスター/スレーブ同期は非同期レプリケーションの原理に基づいているため、センチネル モードとクラスター モードは本質的にこの条件を満たすことができません。このため、Redis の作成者は、RedLock (Redis Distribute Lock) というソリューションを特別に提案しました。


設計アイデア

公式ドキュメントによると、RedLockの設計アイデアが紹介されています。 最初に環境要件について話しましょう。N (N>=3) 個の独立してデプロイされた Redis インスタンスが必要です。相互間にマスター/スレーブ レプリケーション、フェイルオーバー、その他のテクノロジは必要ありません。

ロックを取得するために、クライアントは次のプロセスに従います:

  • 获取当前时间(毫秒)作为开始时间start;
  • 使用相同的key和随机value,按顺序向所有N个节点发起获取锁的请求。当向每个实例设置锁时,客户端会使用一个过期时间(小于锁的自动释放时间)。比如锁的自动释放时间是10秒,这个超时时间应该是5-50毫秒。这是为了防止客户端在一个已经宕机的实例浪费太多时间:如果Redis实例宕机,客户端尽快处理下一个实例。
  • 客户端计算加锁消耗的时间cost(cost=start-now)。只有客户端在半数以上实例加锁成功,并且整个耗时小于整个有效时间(ttl),才能认为当前客户端加锁成功。
  • 如果客户端加锁成功,那么整个锁的真正有效时间应该是:validTime=ttl-cost。
  • 如果客户端加锁失败(可能是获取锁成功实例数未过半,也可能是耗时超过ttl),那么客户端应该向所有实例尝试解锁(即使刚刚客户端认为加锁失败)。

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 サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。