如何設計高性能的分布式鎖


什么是分布式鎖?

在 JVM 中,在多線程並發的情況下,我們可以使用同步鎖或 Lock 鎖,保證在同一時間內,只能有一個線程修改共享變量或執行代碼塊。但現在我們的服務都是基於分布式集群來實現部署的,對於一些共享資源,在分布式環境下使用 Java 鎖的方式就失去作用了。

使用數據庫實現一個分布式鎖比較簡單易懂,直接基於數據庫實現就行了,不需要再引入第三方中間件,所以這是很多分布式業務實現分布式鎖的首選。但是數據庫實現的分布式鎖在一定程度上,存在性能瓶頸,所以我推薦使用Redis。

Redis 實現分布式鎖

Redis 實現分布式鎖的方式,是使用 SETNX+EXPIRE 組合來實現,在 Redis 2.6.12 版本之前,具體實現代碼如下:

 1 public static boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
 2 
 3     Long result = jedis.setnx(lockKey, requestId);//設置鎖
 4     if (result == 1) {//獲取鎖成功
 5         // 若在這里程序突然崩潰,則無法設置過期時間,將發生死鎖
 6         jedis.expire(lockKey, expireTime);//通過過期時間刪除鎖
 7         return true;
 8     }
 9     return false;
10 }

 

 

這種方式實現的分布式鎖,是通過 setnx() 方法設置鎖,如果 lockKey 存在,則返回失敗,否則返回成功。設置成功之后,為了能在完成同步代碼之后成功釋放鎖,方法中還需要使用 expire() 方法給 lockKey 值設置一個過期時間,確認 key 值刪除,避免出現鎖無法釋放,導致下一個線程無法獲取到鎖,即死鎖問題。

​ 如果程序在設置過期時間之前、設置鎖之后出現崩潰,此時如果 lockKey 沒有設置過期時間,將會出現死鎖問題。

在 Redis 2.6.12 版本后 SETNX 增加了過期時間參數:

 1 private static final String LOCK_SUCCESS = "OK";
 2     private static final String SET_IF_NOT_EXIST = "NX";
 3     private static final String SET_WITH_EXPIRE_TIME = "PX";
 4 
 5     /**
 6      * 嘗試獲取分布式鎖
 7      * @param jedis Redis客戶端
 8      * @param lockKey 鎖
 9      * @param requestId 請求標識
10      * @param expireTime 超期時間
11      * @return 是否獲取成功
12      */
13     public static boolean tryLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
14 
15         String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
16 
17         if (LOCK_SUCCESS.equals(result)) {
18             return true;
19         }
20         return false;
21 
22     }

我們也可以通過 Lua 腳本來實現鎖的設置和過期時間的原子性,再通過 jedis.eval() 方法運行該腳本:

1 // 加鎖腳本
2 private static final String SCRIPT_LOCK = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then redis.call('pexpire', KEYS[1], ARGV[2]) return 1 else return 0 end";
3 
4 // 解鎖腳本
5 private static final String SCRIPT_UNLOCK = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

雖然 SETNX 方法保證了設置鎖和過期時間的原子性,但如果我們設置的過期時間比較短,而執行業務時間比較長,就會存在鎖代碼塊失效的問題。我們需要將過期時間設置得足夠長,來保證以上問題不會出現。

​ 這個方案是目前最優的分布式鎖方案,但如果是在 Redis 集群環境下,依然存在問題。由於 Redis 集群數據同步到各個節點時是異步的,如果在 Master 節點獲取到鎖后,在沒有同步到其它節點時,Master 節點崩潰了,此時新的 Master 節點依然可以獲取鎖,所以多個應用服務可以同時獲取到鎖。

Redlock 算法

​ Redisson 由 Redis 官方推出。它不僅提供了一系列的分布式的 Java 常用對象,還提供了許多分布式服務。Redisson 是基於 netty 通信框架實現的,所以支持非阻塞通信,性能相對於我們熟悉的 Jedis 會好一些。

​ Redisson 中實現了 Redis 分布式鎖,且支持單點模式和集群模式。在集群模式下,Redisson 使用了 Redlock 算法,避免在 Master 節點崩潰切換到另外一個 Master 時,多個應用同時獲得鎖。我們可以通過一個應用服務獲取分布式鎖的流程,了解下 Redlock 算法的實現:

​ 具體的代碼實現如下:

  1. 首先引入 jar 包:

    <dependency>
         <groupId>org.redisson</groupId>
         <artifactId>redisson</artifactId>
         <version>3.8.2</version>
    </dependency>

     

  2. 實現 Redisson 的配置文件:
     1 @Bean
     2 public RedissonClient redissonClient() {
     3    Config config = new Config();
     4    config.useClusterServers()
     5            .setScanInterval(2000) // 集群狀態掃描間隔時間,單位是毫秒
     6            .addNodeAddress("redis://127.0.0.1:7000).setPassword("1")
     7            .addNodeAddress("redis://127.0.0.1:7001").setPassword("1")
     8            .addNodeAddress("redis://127.0.0.1:7002")
     9            .setPassword("1");
    10    return Redisson.create(config);
    11 }

     

  3. 獲取鎖操作:

     1 long waitTimeout = 10;
     2 long leaseTime = 1;
     3 RLock lock1 = redissonClient1.getLock("lock1");
     4 RLock lock2 = redissonClient2.getLock("lock2");
     5 RLock lock3 = redissonClient3.getLock("lock3");
     6 
     7 RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
     8 // 同時加鎖:lock1 lock2 lock3
     9 // 紅鎖在大部分節點上加鎖成功就算成功,且設置總超時時間以及單個節點超時時間
    10 redLock.trylock(waitTimeout,leaseTime,TimeUnit.SECONDS);
    11 ...
    12 redLock.unlock(); 

擴展閱讀:

Redis的三個框架:Jedis,Redisson,Lettuce

Jedis 地址:https://github.com/xetorthio/jedis,是Redis的Java實現客戶端,提供了比較全面的Redis命令的支持。SpringBoot1.x系列中默認采用的是jedis。

Redisson 官網地址:https://redisson.org/,實現了分布式和可擴展的Java數據結構。

Lettuce 官網地址:https://lettuce.io/,高級Redis客戶端,用於線程安全同步,異步和響應使用,支持集群,Sentinel,管道和編碼器。SpringBoot2.x系列中拋棄了原有的jedis,默認采用lettuce。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM