一、介紹
互聯網的應用場景中,為了支持高並發的請求,服務都是執行的分布式部署,相同的任務可以在集群中不同的服務器上執行,並且現在的服務容器都是支持多線程,相同的任務也可能會被同一個容器多次執行,都要求執行結果都滿足冪等性的設計原則。
分布式鎖,就是為了確保在分布式的環境下,相同任務只會執行成功的執行一次,后續的執行不會對這些已經產生了變化的業務再次產生影響。
分布式鎖的實現有不少的方式,如:
- 使用RDBMS數據庫本身的表鎖或行鎖特性;
- 使用Redis做為分布式鎖;
- 使用Zookeeper做為分布式鎖;
使用RDBMS數據庫做為鎖不是筆者要討論的范疇,因為其本身的特性,不太符合在高並發下鎖的應用場景,這里以Redis作為分布式鎖做為介紹。
二、Redis
Redis本身有一些命令組支持原子性的操作,如getset、setns,這些命令可以用於分布式鎖的場景中。
1、使用getset作為分布式鎖控制實現
getSet本身是支持原子性的,在寫入新值的同時會返回舊的值,用這個寫入新值並獲取舊值做為分布式鎖的控制實現,如果返回的值不為空,那就說明前面已經有其它線程(這里的其它線程可以指當前容器中當前服務的其它線程,也指由部署在其它服務器上的應用中的線程)修改了該值,則可以認為已經有線程在對該請求正在處理,因而可以放棄后面的處理邏輯。
可以將當前系統的時間作為分布式鎖key的值,后續其它線程的請求時,將其請求的時間與獲取到的鎖對應的key的舊值進行比較,比較是否已經超過了一定的時間控制閥值,如果超過了則可以認為(這里存在誤判的可能性,因為后續邏輯的數據處理,恰好超過了這個時間比較閥值,就會導致重復執行,因而這里時間控制閥值要設置的比較合理,另外也需要合適的熔斷機制用於保證)前面的交易處理失敗(如服務恰好在設置了用於分布式鎖的key后,立即就掛了,沒有執行到后面的刪除操作)導致用於分布式鎖的key沒有被刪除掉,可以繼續處理該請求的后續交易邏輯。
這個是非常輕量級的事務控制,不會對Redis產生外部事務(應用與Redis之間的交互事務),只是需要對Redis多進一次getset操作,流程圖如下:
其中藍色部分表示獲取鎖的邏輯。
Java代碼實現如下:
setnx和getset的執行邏輯不同,getset是設置新值並返回舊值,setnx如果存在舊值時可以通過參數控制不設置值並返回0,不存在舊值時才設值並返回1。
二者處理流程上都是相同的,不同之處在於獲取鎖的實現,setnx的實現邏輯如下:
其中綠色部分為setnx獲取鎖的邏輯,這個和getset是不同的實現邏輯。
setnx獲取鎖的Java代碼實現如下:
public boolean getLock(String lockKey) { RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); JedisCommands commands = (JedisCommands) connection.getNativeConnection(); boolean con = false; do { long now = System.currentTimeMillis(); // Redis的GetSet返回的值必須是字符串,否則會拋異常,因而將其轉換為字符串 String nowTime = String.valueOf(now); con = false; // 返回1表示鎖獲取成功,返回0表示鎖取失敗 String result = commands.set(lockKey, nowTime, "NX", "PX", expire); if ("1".equals(result)) { return true; } else { String oldTime = redisTemplate.opsForValue().get(lockKey); if (null != oldTime) { // 檢查鎖lockKey的值是不是超過了設定的時間,如2秒鍾,如果超過了則繼續嘗試獲取鎖, // 直到獲取到鎖,或者數據未超期時退出,循環判斷可以解決死鎖的問題 if (now - Long.parseLong(oldTime) >= expire) {// 數據已經過期了 con = true; } } } } while (con); return false; }