第十二章:SpringBoot2.3.0 Redis分佈式鎖

一)分佈式鎖特性

1、互斥性:需保證任何時刻只能由一個客戶端持有鎖。

2、高可用:加鎖和解鎖操作要一致,保證命令執行的原子性。

3、容錯性:當Redis大多數節點正常使用時,客戶端可用加鎖和解鎖。或加入重試機制。

 

二)單機模式分佈式鎖

實現原理

第一步:執行setIfAbsent命令(當key不存在時,才加鎖成功, key存在, 表示已鎖),並設置10秒有效期, 防止加鎖失敗導致死鎖。

加鎖方式1:

boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(redisLockKey, "LOCK", 10, TimeUnit.SECONDS);

加鎖方式2:在獲取鎖的時候能夠保證設置Redis值和過期時間的原子性。

public String lock(final String key, final String value, final int secondsToExpire) {
    String result = stringRedisTemplate.execute(new RedisCallback<String>() {
        @Override
        public String doInRedis(RedisConnection connection) throws DataAccessException {
            Object nativeConnection = connection.getNativeConnection();

            // 設置參數
            SetParams setParams = SetParams.setParams();
            setParams.nx(); // 表示只有當鎖定資源不存在的時候才能SET成功
            setParams.ex(secondsToExpire); // expire表示鎖定資源的自動過期時間,單位是秒

            // 單機模式
            if (nativeConnection instanceof Jedis) {
                // JedisCommands需要引入Jedis的Jar, 位於redis.clients.jedis.commands.JedisCommands下
                JedisCommands commands = (JedisCommands) connection;
                return commands.set(key, "鎖定資源", setParams);
            }
            // 集羣模式
             else if (nativeConnection instanceof JedisCluster ) {
                JedisCluster cluster = (JedisCluster) connection;
                return cluster.set(key, "鎖定資源", setParams);
            }
            return null;
        }
    });
    return result;
}

第二步:執行業務邏輯。

第三步:業務邏輯執行完, 把key刪除, 相當於解鎖, 執行del命令。

stringRedisTemplate.delete(redisLockKey);

 

大多數情況下,該方式加鎖解鎖已經可以正常使用,除了集羣模式下,多個客戶端情況。

比如A客戶端請求加鎖成功,B客戶端也請求加鎖成功,但B客戶端執行效率高,把key解鎖了,如果又有一個請求A客戶端,但上一次A客戶端請求還未完成,會出現加鎖解鎖不一致問題,無法保證鎖的互斥性。

加鎖、執行業務邏輯、解鎖源碼:

// http://localhost:8080/redis/redisLock
@RequestMapping(value = "/redisLock")
public String redisLock() {
    // 初始化
    String redisLockKey = "redis_lock_key";

    // 執行setIfAbsent命令(當key不存在時,才加鎖成功, key存在, 表示已鎖),並設置10秒有效期, 防止加鎖失敗導致死鎖
    boolean isLock = stringRedisTemplate.opsForValue().setIfAbsent(redisLockKey, "LOCK", 10, TimeUnit.SECONDS);
    if (!isLock) {
        System.out.println("-----lock-----");
        return "LOCK";
    }
    System.out.println("-----lock success-----");
    System.out.println("==>begin 開始執行業務邏輯!");
    try {
        // 執行業務邏輯
        Thread.sleep(5000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("==>end 結束執行業務邏輯!");

    // 業務邏輯執行完, 把key刪除, 相當於解鎖, 執行del命令
    stringRedisTemplate.delete(redisLockKey);
    System.out.println("-----unlock success-----");
    return "S";
}

 

三)集羣模式分佈式鎖

Redis從2.6.0可使用內置的Lua解釋器,利用EVAL命令對Lua腳本進行求值。

可以通過Lua腳本來達到釋放鎖的原子操作,定義 Lua 腳本如下:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end;

 

使用LUA腳本,把判斷鎖是否存在,不存在則直接返回,存在則刪除的邏輯直接綁定。保證了Redis命令的原子性,要麼命令執行都成功,要麼命令執行都失敗。

解鎖代碼:

public String unlock(final String key, final String value) {
    List<String> keyList = new ArrayList<String>();
    keyList.add(key);
    List<String> valueList = new ArrayList<String>();
    valueList.add(value);

    // LUA腳本
    StringBuilder builder = new StringBuilder();
    builder.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
    builder.append("then ");
    builder.append("    return redis.call(\"del\",KEYS[1]) ");
    builder.append("else ");
    builder.append("    return 0 ");
    builder.append("end ");
    String lua = builder.toString();
		
    // 執行腳本
    String result = stringRedisTemplate.execute(new RedisCallback<String>() {
        @Override
        public String doInRedis(RedisConnection connection) throws DataAccessException {
            Object nativeConnection = connection.getNativeConnection();
            // 單機模式
            if (nativeConnection instanceof Jedis) {
                return (String) ((Jedis) nativeConnection).eval(lua, keyList, valueList);
            } 
            // 集羣模式
              else if (nativeConnection instanceof JedisCluster ) {
                return (String) ((JedisCluster) nativeConnection).eval(lua, keyList, valueList);
            }
            return null;
        }
    });
    return result;
}

 

識別二維碼關注個人微信公衆號

本章完結,待續,歡迎轉載!
 
本文說明:該文章屬於原創,如需轉載,請標明文章轉載來源!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章