面試官必問的分布式鎖面試題,你答得上來嗎?
一、面試聊聊-分布式鎖,如何回答?
要分析分布式鎖這個問題,我們根據黃金圈法則來分析。
黃金圈法則是由美國營銷顧問西蒙·斯涅克(Simon Sinek)提出的一種思維模型,用于幫助人們更好地理解和傳達信息。黃金圈法則由三個圈組成,分別是:
- 為什么(Why):為什么要做這件事?這是黃金圈的核心,是一切的起點。
- 怎么做(How):怎么做這件事?這是黃金圈的中間部分,是實現目標的方法。
- 做什么(What):做什么這件事?這是黃金圈的外圍部分,是具體的行為。
使用3w分析問題思路來分析分布式鎖,可以從以下幾個方面進行分析:
What:分布式鎖是什么?
分布式鎖是控制分布式系統之間同步訪問共享資源的機制。在分布式系統中,常常需要協調他們的動作。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那么訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分布式鎖。
回答時:先說一下概念。分布式鎖是用于在分布式系統中控制對共享資源的訪問,以避免數據競爭和并發問題。
How:如何實現分布式鎖?
分布式鎖的實現方法有很多,常見的有以下幾種:
- 數據庫鎖:使用數據庫中的行鎖或表鎖來實現分布式鎖。
- 文件鎖:使用文件來實現分布式鎖。
- Zookeeper鎖:使用Zookeeper來實現分布式鎖。
- Redis鎖:使用Redis來實現分布式鎖。
- 消息隊列鎖:使用消息隊列來實現分布式鎖。
回答時:說一下分布式鎖以上的實現方式。
Why:為什么需要分布式鎖?
一些需要分布式鎖的場景:
- 分布式數據庫事務:在分布式系統中,通常需要使用分布式事務來保證數據的一致性。在分布式事務中,通常會使用分布式鎖來保證事務的執行順序。
- 分布式資源分配:在分布式系統中,通常需要使用分布式鎖來分配共享資源。例如,在搶購場景中,需要使用分布式鎖來保證同一商品只能被一個用戶購買。
- 分布式數據同步:在分布式系統中,通常需要使用分布式鎖來保證數據的同步。例如,在訂單系統中,需要使用分布式鎖來保證同一訂單只能被一個系統修改。
回答時:說一下分布式鎖的應用場景。
二、深入聊聊-分布式鎖在項目中的應用
1、Redisson實現分布式鎖
Redisson 是一個基于 Redis 的 Java 分布式框架。Redisson 提供了豐富的功能,包括分布式鎖、分布式集合、分布式隊列等。
以下是使用 Redisson 實現分布式鎖的示例:
@Autowired
private RedissonClient redissonClient;
public String lock() {
// 獲取鎖
RLock lock = redissonClient.getLock("lock");
boolean acquired = lock.tryLock(10,-1,TimeUnit.SECONDS);
if (acquired) {
// 獲取鎖成功,執行業務邏輯
return "獲取鎖成功,執行業務邏輯...";
} else {
// 獲取鎖失敗,重試
return "獲取鎖失敗,重試...";
}
}
public String unlock() {
// 釋放鎖
RLock lock = redissonClient.getLock("lock");
lock.unlock();
return "釋放鎖成功...";
}
另外,redisson支持鎖續期。即在鎖鍵值過期后任務還沒執行完成,此時需要把鎖鍵值的時間自動延長。
Redisson提供了的續期機制,只要客戶端加鎖成功,就會啟動一個Watch Dog。可以看到源代碼的實現leaseTime不設置為-1時開啟監聽。如果任務沒完成就調用scheduleExpirationRenewal續期方法。
tryLock() 方法用于嘗試獲取分布式鎖,該方法有三個參數:
- key:鎖的鍵值。
- waitTime:等待獲取鎖的時間,單位為毫秒。
- leaseTime:鎖的過期時間,單位為毫秒。
waitTime 參數表示客戶端最多等待多長時間來獲取鎖。如果在 waitTime 時間內沒有獲取到鎖,則會返回 false。
leaseTime 參數表示鎖的過期時間。如果鎖在 leaseTime 時間內沒有被釋放,則會自動釋放。如果 leaseTime 設置為 -1,則表示鎖的過期時間由 renew() 方法來控制。這樣,在業務邏輯執行過程中,可以定期調用 lock.renew() 方法來續期鎖的過期時間。
tryLock() 方法的返回值是一個 Boolean 值,表示是否成功獲取到鎖。如果成功獲取到鎖,則返回 true。否則,返回 false。
2、Springboot+Lettuce
在 SpringBoot 2.7 中,可以通過spring-boot-starter-data-redis 默認依賴是Lettuce。那么Lettuce是如何實現分布式鎖呢
- 在 Spring Boot 項目中添加 spring-boot-starter-data-redis 依賴。
- 定義一個 RedisLock 類來封裝分布式鎖的相關操作。
- 在 ServiceImpl類中使用 RedisLock 類來獲取和釋放分布式鎖。
以下是 SpringBoot+Lettuce 實現分布式鎖的完整代碼:
@Component
public class RedisLock {
private static final String LOCK_SCRIPT = "if redis.call('exists', KEYS[1]) == 0 then\n" +
" redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]);\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end";
private final RedisTemplate<String, String> redisTemplate;
public RedisLock(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
//獲得鎖
public boolean acquireLock(String key, long timeout) {
String uuid = UUID.randomUUID().toString();
Object result = redisTemplate.execute(new DefaultRedisScript(LOCK_SCRIPT, Long.class), Arrays.asList(key), uuid, timeout);
return result != null && (long) result == 1;
}
//釋放鎖
public void releaseLock(String key, String uuid) {
redisTemplate.delete(key);
}
}
@Service
public class TestServcieImpl implements TestServcie{
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 加鎖腳本
private static final String LOCK_SCRIPT = "if redis.call ('setnx', KEYS[1], ARGV[1]) == 1 then return redis.call ('expire', KEYS[1], ARGV[2]) else return 0 end";
// 解鎖腳本
private static final String UNLOCK_SCRIPT = "if redis.call ('get', KEYS[1]) == ARGV[1] then return redis.call ('del', KEYS[1]) else return 0 end";
// 加鎖方法
public boolean lock(String key, String value, Long expire) {
RedisScript<Long> redisScript = new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value, String.valueOf(expire));
return result.equals(Long.valueOf(1));
}
// 解鎖方法
public boolean unlock(String key, String value) {
RedisScript<Long> redisScript = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
Long result = stringRedisTemplate.execute(redisScript, Collections.singletonList(key), value);
return result.equals(Long.valueOf(1));
}
}
在上述 demo 中,我們使用 RedisLock 類來封裝分布式鎖的相關操作。acquireLock() 方法用于獲取分布式鎖,releaseLock() 方法用于釋放分布式鎖。
在 ServcieImpl 類中,我們使用 RedisLock 類來獲取和釋放分布式鎖。lock() 方法用于獲取鎖,unlock() 方法用于釋放鎖。不像Redisson封裝好了相應的方法,Lettuuce如果要實現鎖續期就需要自己寫監聽器及相應的lua腳本。
三、小結
1、什么是Lua?
可以看到不論是Redisson還是Lettuce實現分布式鎖都使用的Lua腳本,那我們先來了解一下什么是Lua腳本語言。
Lua 是一個小巧的腳本語言,由巴西里約熱內盧天主教大學(Pontifical Catholic University of Rio de Janeiro)里的一個研究小組于 1993 年開發的。Lua 使用標準 C 語言編寫并以源代碼形式開放,幾乎在所有操作系統和平臺上都能編譯運行。Lua 腳本可以調用 C/C++ 的函數,也可以被 C/C++ 代碼調用,所以 Lua 在應用程序中可以被廣泛應用。
Lua 的特點如下:
- 小巧靈活:Lua 的核心只有 200K,非常小巧,可以方便地嵌入到應用程序中。Lua 的語法也非常簡單,易于學習和使用。
- 可擴展性:Lua 可以調用 C/C++ 的函數,也可以被 C/C++ 代碼調用,所以 Lua 可以很容易地擴展到應用程序的其他部分。
- 高性能:Lua 的運行效率非常高,可以滿足大多數應用程序的需求。
Lua 在游戲開發、Web 開發、嵌入式系統等領域都有廣泛的應用。
以下是 Lua 的一些典型應用:
- 游戲開發:Lua 常用于游戲中的腳本編寫,用于實現游戲的邏輯和特效。
- Web 開發:Lua 可以用于 Web 開發,用于實現動態頁面和游戲。
- 嵌入式系統:Lua 可以用于嵌入式系統的開發,用于實現控制邏輯和用戶界面。
Lua 是一款非常實用的腳本語言,在眾多領域都有廣泛的應用。
2、為什么使用Lua?
- Redis 實現分布式鎖中,獲取鎖、釋放鎖為什么要使用 Lua 腳本?
- 使用 Lua 腳本的主要原因是為了保證操作的原子性,避免出現并發問題或誤解鎖的情況。
- 使用 setnx 命令獲取鎖,然后使用 expire 命令設置過期時間,這兩個命令之間可能會發生網絡延遲或者其他異常,導致鎖沒有正確設置過期時間,從而造成死鎖。
- 使用 del 命令釋放鎖,需要先判斷鎖是否屬于當前客戶端,否則可能會誤解其他客戶端的鎖。
使用 Lua 腳本可以將判斷和刪除鎖的操作合并為一個原子操作,避免了這些問題。Lua 腳本在 Redis 服務器端執行,不會受到網絡延遲或者客戶端故障的影響,也不會被其他命令打斷,因此可以保證操作的原子性。
- 為什么說 Redis 命令沒有原子性?
- Redis 命令本身是單線程執行的,所以單個命令是具有原子性的。
- 但是如果要實現分布式鎖的功能,通常需要多個命令組合起來執行,例如 setnx + expire 或者 get + del。
- 這些命令組合在執行過程中可能會被其他客戶端發送的命令打斷,導致數據不一致或者邏輯錯誤。
因此,Redis 命令沒有原子性是指多個命令組合起來執行時沒有原子性。
以下是使用 Lua 腳本實現分布式鎖的示例:
-- 獲取鎖
function acquire_lock(key, uuid, timeout)
local value = redis.call("GET", key)
if value == nil then
redis.call("SET", key, uuid, "NX", "PX", timeout)
return 1
else
return 0
end
end
-- 釋放鎖
function release_lock(key, uuid)
redis.call("DEL", key)
end
上述腳本實現了簡單的 SETNX 和 DEL 操作,可以保證同一時刻只有一個客戶端可以獲取到鎖。
在實際使用中,可以根據具體的業務場景來調整 Lua 腳本的實現。