# 面試官:基于Redis怎么實現分布式鎖?
## 一、錯誤方案:只使用setnx(key, value, expires)
1. 不同的服務器啟動時間是不一致的;
2. key的過期時間設置太短,會有可能導致任務重復執行;
3. key的過期時間設置太長,則有可能錯過下次任務執行周期;
因此,在這種場景下,保持服務器時間基本同步的情況下,設置小于周期,且足夠大于服務器誤差時間的周期,以保證該任務只能執行一次。

但是`缺點`也非常明顯:
1. 服務器時間最早的機器會一直獲得任務的執行權;
2. 基于服務器時間基本能夠同步的條件;
3. key過期時長設置無法形成較好的標準。
> 無論是大周期任務,還是高并發場景下,都不應這樣直接使用。
## 二、基于客戶端標識的本地時間差比較
Redis中存儲的除了鎖key,還有本地隨機字符串+過期時間差。其源碼如下:
```
//保存客戶端標識
private static final ThreadLocal<String> LOCAL = new ThreadLocal<String>();
/**
*
* @param jedis
* @param lockKey 鎖key
* @param expires 過期時間 一般為 System.currentTimeMillis()+ 過期時間
* @return
*/
public static boolean getDistributedLock(Jedis jedis, String lockKey, long expires) {
//客戶端標識 在釋放鎖時 確保由設置鎖的客戶端來釋放自己的鎖
String uuid = UUID.randomUUID().toString();
LOCAL.set(uuid);
String expiresStr = uuid+"#"+expires;
// 如果當前鎖不存在,返回加鎖成功
if (jedis.setnx(lockKey, expiresStr) == 1) {
return true;
}
// 如果鎖存在,獲取鎖的過期時間
String currentValue = jedis.get(lockKey);
String currentValueStr = null==currentValue?null:currentValue.split("#")[1];
// 判斷當前鎖是否過期
if (currentValueStr != null && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {
// 鎖已過期,獲取上一個鎖的過期時間,并設置現在鎖的過期時間 此處多個客戶端會覆蓋鎖的過期時間
String oldValue = jedis.getSet(lockKey,expiresStr);
String oldValueStr = null ==oldValue?null:oldValue.split("#")[1];
// 考慮多線程并發的情況,只有一個線程的設置值和當前值相同,它才有權利加鎖
if (oldValueStr != null && oldValueStr.equals(currentValueStr)) {
//由于上面會覆蓋鎖的過期時間 此處讓獲取鎖的客戶端 重新設置為自己的過期時間
jedis.set(lockKey,expiresStr);
return true;
}
}
// 其他情況,一律返回加鎖失敗
return false;
}
/**
*
* @param jedis
* @param lockKey 鎖key
* @param value 過期時間 一般為 System.currentTimeMillis()+ 過期時間
* @return
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, long value) {
String uuid = LOCAL.get();
String valueStr = uuid+"#"+value;
//根據uuid 這個標識 讓客戶端 去釋放自己的鎖 不能釋放別人的鎖
if(valueStr.equals(jedis.get(lockKey))){
jedis.del(lockKey);
return true;
}
return false;
}
```
## 三、參考資料
1. [基于Redis的分布式鎖 基于setnx的正確實現方式](https://blog.csdn.net/wudidewu/article/details/79817125)
- 前言
- 第一部分 計算機網絡與操作系統
- 大量的 TIME_WAIT 狀態 TCP 連接,對業務有什么影響?怎么處理?
- 性能占用
- 第二部分 Java基礎
- 2-1 JVM
- JVM整體結構
- 方法區
- JVM的生命周期
- 堆對象結構
- 垃圾回收
- 調優案例
- 類加載機制
- 執行引擎
- 類文件結構
- 2-2 多線程
- 線程狀態
- 鎖與阻塞
- 悲觀鎖與樂觀鎖
- 阻塞隊列
- ConcurrentHashMap
- 線程池
- 線程框架
- 徹底搞懂AQS
- 2-3 Spring框架基礎
- Spring注解
- Spring IoC 和 AOP 的理解
- Spring工作原理
- 2-4 集合框架
- 死磕HashMap
- 第三部分 高級編程
- Socket與NIO
- 緩沖區
- Bybuffer
- BIO、NIO、AIO
- Netty的工作原理
- Netty高性能原因
- Rabbitmq
- mq消息可靠性是怎么保障的?
- 認證授權
- 第四部分 數據存儲
- 第1章 mysql篇
- MySQL主從一致性
- Mysql的數據組織方式
- Mysql性能優化
- 數據庫中的樂觀鎖與悲觀鎖
- 深度分頁
- 從一條SQL語句看Mysql的工作流程
- 第2章 Redis
- Redis緩存
- redis key過期策略
- 數據持久化
- 基于Redis分布式鎖的實現
- Redis高可用
- 第3章 Elasticsearch
- 全文查詢為什么快
- battle with mysql
- 第五部分 數據結構與算法
- 常見算法題
- 基于數組實現的一個隊列
- 第六部分 真實面試案例
- 初級開發面試材料
- 答案部分
- 現場編碼
- 第七部分 面試官角度
- 第八部分 計算機基礎
- 第九部分 微服務
- OpenFeign工作原理