ホームページ データベース Redis Redis 分散ロックの正しい実装の概要

Redis 分散ロックの正しい実装の概要

Dec 11, 2019 pm 05:20 PM
redis

Redis 分散ロックの正しい実装の概要

分散ロックには通常 3 つの実装方法があります:

1. データベースのオプティミスティック ロック;

2. Redis ベースの分散ロック;

3. ZooKeeper に基づく分散ロック。

この記事では、Redis に基づいて分散ロックを実装する 2 番目の方法を紹介します。インターネット上には、Redis 分散ロックの実装を紹介するさまざまなブログが存在しますが、その実装にはさまざまな問題があるため、読者の誤解を避けるために、このブログでは Redis 分散ロックを正しく実装する方法を詳しく紹介します。

信頼性

まず、分散ロックを確実に利用できるようにするには、少なくともロックの実装が次の条件を満たしていることを確認する必要があります。次の 4 つの条件を同時に満たす:

1. 相互排他性。常に 1 つのクライアントだけがロックを保持できます。

2. デッドロックは発生しません。クライアントがロックをアクティブにロック解除せずにロックを保持している間にクラッシュした場合でも、その後他のクライアントがそのロックをロックできることが保証されます。

3. フォールトトレラント。ほとんどの Redis ノードが正常に実行されている限り、クライアントはロックおよびロック解除を行うことができます。

4. ベルの結び目を解くには、ベルも縛らなければなりません。ロックとロック解除は同じクライアントで行う必要があり、他のクライアントが追加したロックをクライアント自身がロック解除することはできません。

コードの実装

コンポーネントの依存関係

まず、Jedis オープン ソース コンポーネントを導入する必要があります。 Maven: 次のコードを pom.xml ファイルに追加します:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>
ログイン後にコピー

ロック コード

正しい姿勢

トークは安いです、コードを見せてください。最初にコードを示し、次にこの方法で実装する理由をゆっくりと説明します。

public class RedisTool {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {

        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);

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

    }

}
ログイン後にコピー

ご覧のとおり、ロックに必要なコードは jedis.set(String key, String value, String nxxx) の 1 行だけです。 , String expx, int time )、この set() メソッドには合計 5 つの仮パラメータがあります:

最初のパラメータは key です。key は一意であるため、key をロックとして使用します。

2 つ目は value です。渡すのは requestId です。多くの子供には理解できないかもしれません。ロックとしてのキーがあれば十分ではないでしょうか。なぜ value を使用する必要があるのでしょうか?理由は、上で信頼性について説明したときに、分散ロックはベルのロックを解除するための 4 番目の条件を満たしている必要があり、ベルを持っている人がベルを結んだ人である必要があるためです。ロックを追加すると、ロックを解除するときの基盤ができます。 requestId は、UUID.randomUUID().toString() メソッドを使用して生成できます。

3 番目のパラメータは nxxx です。このパラメータに NX を入力します。これは、SET IF NOT EXIST を意味します。つまり、キーが存在しない場合はセット操作を実行します。キーがすでに存在する場合は、いいえを実行します。操作が実行されます;

4 番目のパラメータは expx です。渡すパラメータは PX です。これは、このキーに有効期限設定を追加することを意味します。特定の時間は 5 番目のパラメータによって決まります。

5 番目のパラメータは時間で、4 番目のパラメータに対応し、キーの有効期限を表します。

一般に、上記の set() メソッドを実行すると、次の 2 つの結果しか得られません: 1. 現在ロックがありません (キーが存在しません)。その後、ロック操作を実行し、ロックの有効期間を設定します。 、値はロックされたクライアントを表します。 2. ロックはすでに存在するため、操作は実行されません。

注意深い子供たちは、ロック コードが信頼性で説明されている 3 つの条件を満たしていることに気づくでしょう:

1. まず、set() で NX パラメータが追加され、キーがすでに存在する場合、関数は正常に呼び出されません。つまり、1 つのクライアントのみがロックを保持でき、相互排他が満たされます。

2. 次に、ロックの有効期限を設定しているため、後でロック所有者がクラッシュしてロックが解除されなかった場合でも、ロックは自動的にロック解除されます (つまり、キーは削除されます)。有効期限に達するとデッドロックが発生します。

3. 最後に、ロックされたクライアントのリクエスト識別を表す requestId に値を代入しているため、クライアントがロックを解除しているときに、それが同じクライアントであるかどうかを検証できます。ここでは Redis スタンドアロン展開のシナリオのみを考慮するため、当面はフォールト トレランスについては考慮しません。

エラー例 1

より一般的なエラー例は、jedis.setnx() と jedis.expire() の組み合わせを使用してロックを実装することです。コードは次のとおりです。

public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {

    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }

}
ログイン後にコピー

setnx() メソッドは SET IF NOT EXIST として機能し、expire() メソッドはロックに有効期限を追加します。一見したところ、結果は前の set() メソッドと同じように見えますが、これらは 2 つの Redis コマンドであるため、アトミックではありません。setnx() の実行後にプログラムが突然クラッシュした場合、ロックは解除されます。有効期限が設定されています。するとデッドロックが発生します。インターネット上でこれを実装する人がいる理由は、jedis の以前のバージョンがマルチパラメータ set() メソッドをサポートしていないためです。

エラー例 2

public static boolean wrongGetLock2(Jedis jedis, String lockKey, int expireTime) {

    long expires = System.currentTimeMillis() + expireTime;
    String expiresStr = String.valueOf(expires);

    // 如果当前锁不存在,返回加锁成功
    if (jedis.setnx(lockKey, expiresStr) == 1) {
        return true;
    }

    // 如果锁存在,获取锁的过期时间
    String currentValueStr = jedis.get(lockKey);
    if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
        // 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
        String oldValueStr = jedis.getSet(lockKey, expiresStr);
        if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
            // 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁
            return true;
        }
    }
        
    // 其他情况,一律返回加锁失败
    return false;

}
ログイン後にコピー

この種のエラー例は、問題を見つけるのがより難しく、実装もより複雑です。実装のアイデア: jedis.setnx() コマンドを使用してロックを実装します。ここで、key はロック、value はロックの有効期限です。

実行プロセス:

1. setnx() メソッドを使用してロックを試みます。現在のロックが存在しない場合は、ロックを正常に返します。

2. ロックが既に存在する場合は、ロックの有効期限を取得し、現在の時刻と比較します。ロックの有効期限が切れている場合は、新しい有効期限を設定して、ロックを正常に返します。コードは次のとおりです。

それでは、このコードの何が問題なのでしょうか?

1、由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。

2、当锁过期的时候,如果多个客户端同时执行jedis.getSet()方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。

3、锁不具备拥有者标识,即任何客户端都可以解锁。

解锁代码

正确姿势

还是先展示代码,再带大家慢慢解释为什么这样实现:

public class RedisTool {

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {

        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";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

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

    }

}
ログイン後にコピー

可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。

那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读【解锁代码-错误示例2】 。

那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:

简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。

错误示例1

最常见的解锁代码就是直接使用jedis.del()方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。

public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}
ログイン後にコピー

错误示例2

这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:

public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
        
    // 判断加锁与解锁是不是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然不是这个客户端的,则会误解锁
        jedis.del(lockKey);
    }

}
ログイン後にコピー

如代码注释,问题在于如果调用jedis.del()方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行jedis.del()之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。

更多redis知识请关注redis数据库教程栏目。

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

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

ホットAIツール

Undresser.AI Undress

Undresser.AI Undress

リアルなヌード写真を作成する AI 搭載アプリ

AI Clothes Remover

AI Clothes Remover

写真から衣服を削除するオンライン AI ツール。

Undress AI Tool

Undress AI Tool

脱衣画像を無料で

Clothoff.io

Clothoff.io

AI衣類リムーバー

Video Face Swap

Video Face Swap

完全無料の AI 顔交換ツールを使用して、あらゆるビデオの顔を簡単に交換できます。

ホットツール

メモ帳++7.3.1

メモ帳++7.3.1

使いやすく無料のコードエディター

SublimeText3 中国語版

SublimeText3 中国語版

中国語版、とても使いやすい

ゼンドスタジオ 13.0.1

ゼンドスタジオ 13.0.1

強力な PHP 統合開発環境

ドリームウィーバー CS6

ドリームウィーバー CS6

ビジュアル Web 開発ツール

SublimeText3 Mac版

SublimeText3 Mac版

神レベルのコード編集ソフト(SublimeText3)

Redisクラスターモードの構築方法 Redisクラスターモードの構築方法 Apr 10, 2025 pm 10:15 PM

Redisクラスターモードは、シャードを介してRedisインスタンスを複数のサーバーに展開し、スケーラビリティと可用性を向上させます。構造の手順は次のとおりです。異なるポートで奇妙なRedisインスタンスを作成します。 3つのセンチネルインスタンスを作成し、Redisインスタンスを監視し、フェールオーバーを監視します。 Sentinel構成ファイルを構成し、Redisインスタンス情報とフェールオーバー設定の監視を追加します。 Redisインスタンス構成ファイルを構成し、クラスターモードを有効にし、クラスター情報ファイルパスを指定します。各Redisインスタンスの情報を含むnodes.confファイルを作成します。クラスターを起動し、CREATEコマンドを実行してクラスターを作成し、レプリカの数を指定します。クラスターにログインしてクラスター情報コマンドを実行して、クラスターステータスを確認します。作る

Redisデータをクリアする方法 Redisデータをクリアする方法 Apr 10, 2025 pm 10:06 PM

Redisデータをクリアする方法:Flushallコマンドを使用して、すべての重要な値をクリアします。 FlushDBコマンドを使用して、現在選択されているデータベースのキー値をクリアします。 [選択]を使用してデータベースを切り替え、FlushDBを使用して複数のデータベースをクリアします。 DELコマンドを使用して、特定のキーを削除します。 Redis-CLIツールを使用してデータをクリアします。

Redisキューの読み方 Redisキューの読み方 Apr 10, 2025 pm 10:12 PM

Redisのキューを読むには、キュー名を取得し、LPOPコマンドを使用して要素を読み、空のキューを処理する必要があります。特定の手順は次のとおりです。キュー名を取得します:「キュー:キュー」などの「キュー:」のプレフィックスで名前を付けます。 LPOPコマンドを使用します。キューのヘッドから要素を排出し、LPOP Queue:My-Queueなどの値を返します。空のキューの処理:キューが空の場合、LPOPはnilを返し、要素を読む前にキューが存在するかどうかを確認できます。

Centos RedisでLUAスクリプト実行時間を構成する方法 Centos RedisでLUAスクリプト実行時間を構成する方法 Apr 14, 2025 pm 02:12 PM

Centosシステムでは、Redis構成ファイルを変更するか、Redisコマンドを使用して悪意のあるスクリプトがあまりにも多くのリソースを消費しないようにすることにより、LUAスクリプトの実行時間を制限できます。方法1:Redis構成ファイルを変更し、Redis構成ファイルを見つけます:Redis構成ファイルは通常/etc/redis/redis.confにあります。構成ファイルの編集:テキストエディター(VIやNANOなど)を使用して構成ファイルを開きます:sudovi/etc/redis/redis.conf luaスクリプト実行時間制限を設定します。

Redisコマンドラインの使用方法 Redisコマンドラインの使用方法 Apr 10, 2025 pm 10:18 PM

Redisコマンドラインツール(Redis-Cli)を使用して、次の手順を使用してRedisを管理および操作します。サーバーに接続し、アドレスとポートを指定します。コマンド名とパラメーターを使用して、コマンドをサーバーに送信します。ヘルプコマンドを使用して、特定のコマンドのヘルプ情報を表示します。 QUITコマンドを使用して、コマンドラインツールを終了します。

Redisカウンターを実装する方法 Redisカウンターを実装する方法 Apr 10, 2025 pm 10:21 PM

Redisカウンターは、R​​edisキー価値ペアストレージを使用して、カウンターキーの作成、カウントの増加、カウントの減少、カウントのリセット、およびカウントの取得など、カウント操作を実装するメカニズムです。 Redisカウンターの利点には、高速速度、高い並行性、耐久性、シンプルさと使いやすさが含まれます。ユーザーアクセスカウント、リアルタイムメトリック追跡、ゲームのスコアとランキング、注文処理などのシナリオで使用できます。

Redisの有効期限ポリシーを設定する方法 Redisの有効期限ポリシーを設定する方法 Apr 10, 2025 pm 10:03 PM

Redisデータの有効期間戦略には2つのタイプがあります。周期削除:期限切れのキーを削除する定期的なスキャン。これは、期限切れの時間帯-remove-countおよび期限切れの時間帯-remove-delayパラメーターを介して設定できます。怠zyな削除:キーが読み取られたり書かれたりした場合にのみ、削除の有効期限が切れたキーを確認してください。それらは、レイジーフリーレイジーエビクション、レイジーフリーレイジーエクスピア、レイジーフリーラジーユーザーのパラメーターを介して設定できます。

Debian Readdirのパフォーマンスを最適化する方法 Debian Readdirのパフォーマンスを最適化する方法 Apr 13, 2025 am 08:48 AM

Debian Systemsでは、Directoryコンテンツを読み取るためにReadDirシステム呼び出しが使用されます。パフォーマンスが良くない場合は、次の最適化戦略を試してください。ディレクトリファイルの数を簡素化します。大きなディレクトリをできる限り複数の小さなディレクトリに分割し、Readdirコールごとに処理されたアイテムの数を減らします。ディレクトリコンテンツのキャッシュを有効にする:キャッシュメカニズムを構築し、定期的にキャッシュを更新するか、ディレクトリコンテンツが変更されたときに、頻繁な呼び出しをreaddirに削減します。メモリキャッシュ(memcachedやredisなど)またはローカルキャッシュ(ファイルやデータベースなど)を考慮することができます。効率的なデータ構造を採用する:ディレクトリトラバーサルを自分で実装する場合、より効率的なデータ構造(線形検索の代わりにハッシュテーブルなど)を選択してディレクトリ情報を保存およびアクセスする

See all articles