JAva+Redis分散ロックの実装

4744 ワード

前言
分布式ロックには一般的に3つの実現方式がある:1.データベース楽観ロック;2.Redisベースの分散ロック;3.ZooKeeperベースの分散ロック.このブログでは、Redisに基づいて分散ロックを実現する第2の方法について説明します.このブログでは、Redis分散ロックの実装について様々なブログが紹介されていますが、彼らの実装には様々な問題があり、子弟の誤解を避けるために、Redis分散ロックを正しく実装する方法について詳しく説明します.
しんらいせい
まず、分散ロックが使用可能であることを確認するために、少なくともロックの実装が以下の4つの条件を満たしていることを確認します.
1、反発性.任意の時点で、ロックを保持できるクライアントは1つしかありません.2、デッドロックは発生しない.ロックを保持している間にクラッシュし、アクティブにロックを解除しなくても、後続の他のクライアントがロックを追加できることを保証します.3、許容誤差がある.ほとんどのRedisノードが正常に動作している限り、クライアントはロックとロック解除を行うことができます.4、ベルを解くには人を結ばなければならない.ロックとロック解除は同じクライアントでなければなりません.クライアントは自分で他の人のロックを解除することはできません.
コード実装
まず依存を導入する

    redis.clients
    jedis
    2.9.0

ツールクラスコード
import java.util.Collections;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

/**
 * Redis    
 * 
 * @author hxj
 * @date 2019 02 16 
 */
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";
	private static final Long RELEASE_SUCCESS = 1L;

	private static JedisPool jedisPool = null; 

	//            ,    8;     -1,      ;  pool           MAX_TOTAL jedis  ,   pool    exhausted(  )。
	private static int MAX_TOTAL = 1024;
	//     pool         idle(   ) jedis  ,     8。
	private static int MAX_IDLE = 200;
	//            ,    ,    -1,      。        ,     JedisConnectionException;
	private static int MAX_WAIT = 10000;
	private static int TIMEOUT = 10000;
	//  borrow  jedis   ,      validate  ;   true,    jedis       ;
	private static boolean TEST_ON_BORROW = true;
	// Redis   IP
	private static String ADDR = "127.0.0.1";
	// Redis    
	private static int PORT = 6379;
	//     
	private static String AUTH = "zxys";

	/**
	 *    Redis   
	 */
	static {
		try {
			JedisPoolConfig config = new JedisPoolConfig();
			config.setMaxTotal(MAX_TOTAL);
			config.setMaxIdle(MAX_IDLE);
			config.setMaxWaitMillis(MAX_WAIT);
			config.setTestOnBorrow(TEST_ON_BORROW);

			jedisPool = new JedisPool(config, ADDR, PORT, TIMEOUT, AUTH);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 *   Jedis  
	 * @return
	 */
	public synchronized static Jedis getJedis() {
		try {
			if (jedisPool != null) {
				Jedis resource = jedisPool.getResource();
				return resource;
			} else {
				return null;
			}
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
	}
	
	/**
	 *   jedis  
	 * @param jedis
	 */
	@SuppressWarnings("deprecation")
	public static void returnResource(final Jedis jedis) {
		if (jedis != null) {
			jedisPool.returnResource(jedis);
			System.out.println("    ");
		}
	}

	/**
	 *         
	 * 
	 * @param jedis      Redis   
	 * @param lockKey     
	 * @param requestId      
	 * @param expireTime     
	 * @return       
	 */
	public static boolean tryGetDistributedLock(String lockKey, String requestId, int expireTime) {
		Jedis jedis = getJedis();//  jedis  
		String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
		returnResource(jedis);//    
		if (LOCK_SUCCESS.equals(result)) {
			return true;
		}
		return false;
	}

	/**
	 *       
	 * 
	 * @param jedis     Redis   
	 * @param lockKey    
	 * @param requestId     
	 * @return       
	 */
	public static boolean releaseDistributedLock(String lockKey, String requestId) {
		Jedis jedis = getJedis();//  jedis  
		String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
		Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
		returnResource(jedis);//    
		if (RELEASE_SUCCESS.equals(result)) {
			return true;
		}
		return false;
	}

}
まとめると、1、上記のset()メソッドを実行すると、2つの結果しか得られません.1.現在ロックが存在しない場合は、ロック操作を行い、ロックに有効期間を設定し、valueはロックされたクライアントを表します.2.ロックは既に存在し、何もしない.ロックコードは、信頼性に記載されている3つの条件を満たしています.まず、set()にはNXパラメータが追加され、keyが存在する場合、関数が正常に呼び出されないことを保証することができます.つまり、ロックを保持し、反発性を満たすクライアントは1つしかありません.次に、ロックに期限切れが設定されているため、ロックの所有者がその後クラッシュしてロックを解除しなくても、ロックは期限切れになると自動的にロックを解除(すなわちkeyが削除される)し、デッドロックは発生しません.最後に、valueはrequestIdとして割り当てられ、ロックされたクライアント要求IDを表すため、クライアントがロックを解除したときに同じクライアントであるかどうかを検証することができます.Redisスタンドアロン配置のシナリオのみを考慮するため,フォールトトレランスはしばらく考慮しない.2、ロック解除は2行のコードで済みます!1行目のコードは、簡単なLuaスクリプトコード(では、なぜLua言語を使用して実現するのでしょうか?上記の操作が原子的であることを確保するため)を書き、2行目のコードは、jedis.eval()メソッドにLuaコードを転送し、パラメータKEYS[1]をlockKey、ARGV[1]をrequestIdとして割り当てます.eval()メソッドはLuaコードをRedisサービス側に渡して実行する.3、最后に、何も変わらないわけにはいきません.よく考えてまとめて、皆さんの意见を歓迎します.