微服務-分佈式鎖(二)-Redis方案

1 LUA+SETNX+EXPIRE

先用setnx來搶鎖,如果搶到之後,再用expire給鎖設置一個過期時間,防止鎖忘記了釋放。

  • setnx(key, value)

    setnx 的含義就是 SET if Not Exists,該方法是原子的。如果 key 不存在,則設置當前 keyvalue 成功,返回 1;如果當前 key 已經存在,則設置當前 key 失敗,返回 0

  • expire(key, seconds)

    expire 設置過期時間,要注意的是 setnx 命令不能設置 key 的超時時間,只能通過 expire() 來對 key 設置。

1.1 使用Lua腳本(SETNX+EXPIRE)

可以使用Lua腳本來保證原子性(包含setnx和expire兩條指令),加解鎖代碼如下:

/**
 * 使用Lua腳本,腳本中使用setnex+expire命令進行加鎖操作
 */
public boolean lock(Jedis jedis, String key, String uniqueId, int seconds) {
    String luaScript = "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then" +
            		    "redis.call('expire',KEYS[1],ARGV[2]) return 1 else return 0 end";
    Object result = jedis.eval(luaScript, Collections.singletonList(key),
            Arrays.asList(uniqueId, String.valueOf(seconds)));
    return result.equals(1L);
}

/**
 * 使用Lua腳本進行解鎖操縱,解鎖的時候驗證value值
 */
public boolean unlock(Jedis jedis, String key, String value) {
    String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then " +
           			    "return redis.call('del',KEYS[1]) else return 0 end";
    return jedis.eval(luaScript, Collections.singletonList(key), Collections.singletonList(value)).equals(1L);
}

1.2 STW

如果在寫文件過程中,發生了 FullGC,並且其時間跨度較長, 超過了鎖超時的時間, 那麼分佈式就自動釋放了。在此過程中,client2 搶到鎖,寫了文件。client1 的FullGC完成後,也繼續寫文件,注意,此時 client1 的並沒有佔用鎖,此時寫入會導致文件數據錯亂,發生線程安全問題。這就是STW導致的鎖過期問題。STW導致的鎖過期問題,如下圖所示:

STW導致的鎖過期問題,大概的解決方案有:

  • 方案一: 模擬CAS樂觀鎖的方式,增加版本號(如下圖中的token)

​ 此方案如果要實現,需要調整業務邏輯,與之配合,所以會入侵代碼。

  • 方案二:watch dog自動延期機制

    客戶端1加鎖的鎖key默認生存時間才30秒,如果超過了30秒,客戶端1還想一直持有這把鎖,怎麼辦呢?簡單!只要客戶端1一旦加鎖成功,就會啓動一個watch dog看門狗,它是一個後臺線程,會每隔10秒檢查一下,如果客戶端1還持有鎖key,那麼就會不斷的延長鎖key的生存時間。Redission採用的就是這種方案, 此方案不會入侵業務代碼。

2 SET-NX-EX

方案SET key value [EX seconds] [PX milliseconds] [NX|XX]

  • EX second :設置鍵的過期時間爲 second 秒。 SET key value EX second 效果等同於 SETEX key second value
  • PX millisecond :設置鍵的過期時間爲 millisecond 毫秒。 SET key value PX millisecond 效果等同於 PSETEX key millisecond value
  • NX :只在鍵不存在時,纔對鍵進行設置操作。 SET key value NX 效果等同於 SETNX key value
  • XX :只在鍵已經存在時,纔對鍵進行設置操作

客戶端執行以上的命令:

  • 如果服務器返回 OK ,那麼這個客戶端獲得鎖
  • 如果服務器返回 NIL ,那麼客戶端獲取鎖失敗,可以在稍後再重試

① 加鎖:使用redis命令 set key value NX EX max-lock-time 實現加鎖

Jedis jedis = new Jedis("127.0.0.1", 6379);
private static final String SUCCESS = "OK";

 /**
  * 加鎖操作
  * @param key 鎖標識
  * @param value 客戶端標識
  * @param timeOut 過期時間
  */
 public Boolean lock(String key,String value,Long timeOut){
     String var1 = jedis.set(key,value,"NX","EX",timeOut);
     if(LOCK_SUCCESS.equals(var1)){
         return true;
     }
     return false;
 }
  • 加鎖操作 jedis.set(key,value,"NX","EX",timeOut)【保證加鎖的原子操作】
  • keyrediskey值作爲鎖的標識,value在作爲客戶端的標識,只有key-value都比配纔有刪除鎖的權利【保證安全性】
  • 通過timeout設置過期時間保證不會出現死鎖【避免死鎖】
  • NX:只有這個key不存才的時候纔會進行操作,if not exists
  • EX:設置key的過期時間爲秒,具體時間由第5個參數決定,過期時間設置的合理有效期需要根據業務具體決定,總的原則是任務執行time*3

② 解鎖:使用redis命令 EVAL 實現解鎖

Jedis jedis = new Jedis("127.0.0.1", 6379);
private static final String SUCCESS = "OK";

 /**
  * 加鎖操作
  * @param key 鎖標識
  * @param value 客戶端標識
  * @param timeOut 過期時間
  */
 public Boolean lock(String key,String value,Long timeOut){
     String var1 = jedis.set(key,value,"NX","EX",timeOut);
     if(LOCK_SUCCESS.equals(var1)){
         return true;
     }
     return false;
 }
  • luaScript 這個字符串是個lua腳本,代表的意思是如果根據key拿到的value跟傳入的value相同就執行del,否則就返回0【保證安全性】
  • jedis.eval(String,list,list);這個命令就是去執行lua腳本,KEYS的集合就是第二個參數,ARGV的集合就是第三參數【保證解鎖的原子操作】

③ 重試

如果在業務中去拿鎖如果沒有拿到是應該阻塞着一直等待還是直接返回,這個問題其實可以寫一個重試機制,根據重試次數和重試時間做一個循環去拿鎖,當然這個重試的次數和時間設多少合適,是需要根據自身業務去衡量的。

/**
 * 重試機制
 * @param key 鎖標識
 * @param value 客戶端標識
 * @param timeOut 過期時間
 * @param retry 重試次數
 * @param sleepTime 重試間隔時間
 * @return
 */
public Boolean lockRetry(String key,String value,Long timeOut,Integer retry,Long sleepTime){
    Boolean flag = false;
    try {
        for (int i=0;i<retry;i++){
            flag = lock(key,value,timeOut); 
            if(flag){
                break; 
            } 
            Thread.sleep(sleepTime); 
        } 
    }catch (Exception e){ 
        e.printStackTrace(); 
    } 
    return flag; 
}

3 Redisson

Redisson是一個在Redis的基礎上實現的Java駐內存數據網格(In-Memory Data Grid)。它不僅提供了一系列的分佈式的Java常用對象,還實現了可重入鎖(Reentrant Lock)、公平鎖(Fair Lock、聯鎖(MultiLock)、 紅鎖(RedLock)、 讀寫鎖(ReadWriteLock)等,還提供了許多分佈式服務。

3.1 特性功能

  • 支持 Redis 單節點(single)模式、哨兵(sentinel)模式、主從(Master/Slave)模式以及集羣(Redis Cluster)模式
  • 程序接口調用方式採用異步執行和異步流執行兩種方式
  • 數據序列化,Redisson 的對象編碼類是用於將對象進行序列化和反序列化,以實現對該對象在 Redis 裏的讀取和存儲
  • 單個集合數據分片,在集羣模式下,Redisson 爲單個 Redis 集合類型提供了自動分片的功能
  • 提供多種分佈式對象,如:Object Bucket,Bitset,AtomicLong,Bloom Filter 和 HyperLogLog 等
  • 提供豐富的分佈式集合,如:Map,Multimap,Set,SortedSet,List,Deque,Queue 等
  • 分佈式鎖和同步器的實現,可重入鎖(Reentrant Lock),公平鎖(Fair Lock),聯鎖(MultiLock),紅鎖(Red Lock),信號量(Semaphonre),可過期性信號鎖(PermitExpirableSemaphore)等
  • 提供先進的分佈式服務,如分佈式遠程服務(Remote Service),分佈式實時對象(Live Object)服務,分佈式執行服務(Executor Service),分佈式調度任務服務(Schedule Service)和分佈式映射歸納服務(MapReduce)

3.2 Watch dog

總體的Redisson框架的分佈式鎖類型大致如下:

  • 可重入鎖
  • 公平鎖
  • 聯鎖
  • 紅鎖
  • 讀寫鎖
  • 信號量
  • 可過期信號量
  • 閉鎖(/倒數閂)

 

3.3 實現方案

添加依賴

<!-- 方式一:redisson-java -->
<dependency>	
    <groupId>org.redisson</groupId>	
    <artifactId>redisson</artifactId>	
    <version>3.11.4</version>	
</dependency>

<!-- 方式二:redisson-springboot -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.11.4</version>
</dependency>

定義接口

import org.redisson.api.RLock;
import java.util.concurrent.TimeUnit;

public interface DistributedLocker {

    RLock lock(String lockKey);

    RLock lock(String lockKey, int timeout);

    RLock lock(String lockKey, TimeUnit unit, int timeout);

    boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime);

    void unlock(String lockKey);

    void unlock(RLock lock);
    
}

實現分佈式鎖

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import java.util.concurrent.TimeUnit;

public class RedissonDistributedLocker implements DistributedLocker{

    private RedissonClient redissonClient;

    @Override
    public RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        return lock;
    }

    @Override
    public RLock lock(String lockKey, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime, TimeUnit.SECONDS);
        return lock;
    }

    @Override
    public RLock lock(String lockKey, TimeUnit unit ,int timeout) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(timeout, unit);
        return lock;
    }

    @Override
    public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
            return lock.tryLock(waitTime, leaseTime, unit);
        } catch (InterruptedException e) {
            return false;
        }
    }

    @Override
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }

    @Override
    public void unlock(RLock lock) {
        lock.unlock();
    }

    public void setRedissonClient(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }
    
}

3.4 高可用的RedLock(紅鎖)原理

RedLock算法思想是不能只在一個redis實例上創建鎖,應該是在多個redis實例上創建鎖,n / 2 + 1,必須在大多數redis節點上都成功創建鎖,才能算這個整體的RedLock加鎖成功,避免說僅僅在一個redis實例上加鎖而帶來的問題。

更多JAVA、高併發、微服務、架構、解決方案、中間件的總結在:https://github.com/yu120/lemon-guide

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