分散式鎖-使用Redis實現分散式鎖
使用Redis實現分散式鎖
關於分散式鎖的實現,我的前一篇文章講解了如何使用Zookeeper實現分散式鎖。關於分散式鎖的背景此處不再做贅述,我們直接討論下如何使用Redis實現分散式鎖。
關於Redis,筆主不打算做長篇大論的介紹,只介紹下Redis優秀的特性。
- 支援豐富的資料型別,如String、List、Map、Set、ZSet等。
- 支援資料持久化,RDB和AOF兩種方式
- 支援叢集工作模式,分割槽容錯性強
- 單執行緒,順序處理命令
- 支援事務
- 支援釋出與訂閱
Redis實現分散式鎖使用了SETNX命令,我們可以參考Redis官方對此命令的介紹。
SETNX key value 將 key 的值設為 value ,當且僅當 key 不存在。 若給定的 key 已經存在,則 SETNX 不做任何動作。 SETNX 是set if not exists的簡寫。 官方介紹地址:
http://doc.redisfans.com/string/setnx.html
當然,Redis實現分散式鎖也利用了單執行緒順序處理命令的特性。程式碼實現也是比較簡單的,筆主使用了SpringBoot進行開發,SpringBoot提供了操作Redis的工具類RedisTemplate。
首先,我們需要封裝一個公共的Redis訪問工具類。該類需要注入RedisTemplate例項和ValueOperations例項,使用ValueOperations例項是因為Redis實現的分散式鎖使用了最簡單的String型別。另外,我們需要封裝3個方法,分別是setIfObsent (String key, String value)、 expire (String key, long timeout, TimeUnit unit) 、delete (String key) ,分別對應Redis的SETNX、expire、del命令。以下是Redis訪問工具類的具體實現:
/** * Redis訪問工具類 * @author zhaoheng * @date 2018年8月10日 */ @Component public class RedisDao { @Autowired private RedisTemplate redisTemplate; @Resource(name="redisTemplate") private ValueOperations<Object, Object> valOpsObj; /** * 如果key不存在,就儲存一個key-value,相當於SETNX命令 * @param key 鍵 * @param value 值,可以為空 * @return */ public boolean setIfObsent (String key, String value) { return valOpsObj.setIfAbsent(key, value); } /** * 為key設定失效時間 * @param key 鍵 * @param timeout 時間大小 * @param unit 時間單位 */ public boolean expire (String key, long timeout, TimeUnit unit) { return redisTemplate.expire(key, timeout, unit); } /** * 刪除key * @param key 鍵 */ public void delete (String key) { redisTemplate.delete(key); } }
完成了Redis訪問工具類的實現,現在需要考慮的是如何去模擬競爭分散式鎖。因為Redis本身就是支援分散式叢集的,所以只需要模擬出多執行緒處理業務場景。這裡筆主採用執行緒池來模擬,以下是測試類的具體實現:
@RestController
@RequestMapping("test")
public class TestController {
private static final Logger LOG = LoggerFactory.getLogger(TestController.class); //日誌物件
@Autowired
private RedisDao redisDao;
//定義的分散式鎖key
private static final String LOCK_KEY = "MyTestLock";
@RequestMapping(value={"testRedisLock"}, method=RequestMethod.GET)
public void testRedisLock () {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
//獲取分散式鎖
boolean flag = redisDao.setIfObsent(LOCK_KEY, "lock");
if (flag) {
LOG.info(Thread.currentThread().getName() + ":獲取Redis分散式鎖成功");
//獲取鎖成功後設置失效時間
redisDao.expire(LOCK_KEY, 2, TimeUnit.SECONDS);
try {
LOG.info(Thread.currentThread().getName() + ":處理業務開始");
Thread.sleep(1000); //睡眠1000ms模擬處理業務
LOG.info(Thread.currentThread().getName() + ":處理業務結束");
//處理業務完成後刪除鎖
redisDao.delete(LOCK_KEY);
} catch (InterruptedException e) {
LOG.error("處理業務異常:", e);
}
} else {
LOG.info(Thread.currentThread().getName() + ":獲取Redis分散式鎖失敗");
}
}
});
}
}
}
通過上面這段程式碼,可能會產生以下幾個疑問:
- 執行緒如果獲取分散式鎖失敗,為什麼不嘗試重新獲取鎖?
- 執行緒獲取分散式鎖成功後,設定了鎖的失效時間,這個失效時間長短如何確定?
- 執行緒業務處理結束後,為什麼要做刪除鎖的操作?
針對這幾個疑問,我們可以來討論下。
- 第一,Redis的SETNX命令,如果key已經存在,則不會做任何操作,所以SETNX實現的分散式鎖並不是可重入鎖。當然,也可以自己通過程式碼實現重試n次或者直至獲取到分散式鎖為止。但是,這不能保證競爭的公平性,某個執行緒會因為一直等待鎖而阻塞。因此,Redis實現的分散式鎖更適用於對共享資源一寫多讀的場景。
- 第二,分散式鎖必須設定失效時間,而且失效時間必須大於業務處理所需的時間(保證資料一致性)。所以,在測試階段儘可能準確的預測出業務正常處理所需的時間,設定失效時間是防止因為業務處理過程的某些原因導致死鎖的情況。
- 第三,業務處理結束,必須要做刪除鎖的操作。