學(xué)習(xí)高可靠Redis分布式鎖實(shí)現(xiàn)思路
一、分布式鎖的必要性
在單體應(yīng)用時(shí)代,我們使用ReentrantLock或synchronized就能解決線程安全問題。但當(dāng)系統(tǒng)拆分為分布式架構(gòu)后(目前大多數(shù)公司應(yīng)該不會(huì)只是單體應(yīng)用了),跨進(jìn)程的共享資源競(jìng)爭(zhēng)就成了必須要解決的問題。
分布式鎖由此應(yīng)運(yùn)而生,但是必須解決三大核心問題:
- 競(jìng)態(tài)條件:多人操作共享資源,順序不可控
- 鎖失效:鎖自動(dòng)過期但業(yè)務(wù)未執(zhí)行完,其他客戶端搶占資源 / 加鎖成功但未設(shè)置過期時(shí)間,服務(wù)宕機(jī)導(dǎo)致死鎖
- 鎖誤刪:客戶端A釋放了客戶端B持有的鎖。
二、核心實(shí)現(xiàn)解析(附源碼)
2.1 原子性加鎖
local lockKey = KEYS[1] -- 鎖的鍵名,如"order_lock_123"
local lockSecret = ARGV[1] -- 鎖的唯一標(biāo)識(shí)(建議UUID)
local expireTime = tonumber(ARGV[2]) -- 過期時(shí)間(單位:秒)
-- 參數(shù)有效性校驗(yàn)
if not expireTime or expireTime <= 0 then
return "0" -- 參數(shù)非法直接返回失敗
end
-- 原子操作:SET lockKey lockSecret NX EX expireTime
local result = redis.call("set", lockKey, lockSecret, "NX", "EX", expireTime)
return result and "1" or "0" -- 成功返回"1",失敗返回"0"
設(shè)計(jì)思路:
- lockSecret 使用客戶端唯一標(biāo)識(shí)(推薦SnowflakeID)
- 參數(shù)校驗(yàn):防止傳入非法過期時(shí)間
- 原子性:?jiǎn)蚊钔瓿?判斷+設(shè)置+過期"操作
2.2 看門狗續(xù)期機(jī)制
local lockKey = KEYS[1] -- 鎖的鍵名
local lockSecret = ARGV[1] -- 鎖標(biāo)識(shí)
local expireTime = tonumber(ARGV[2]) -- 新的過期時(shí)間
-- 參數(shù)校驗(yàn)
if not expireTime or expireTime <= 0 then
return "0"
end
-- 獲取當(dāng)前鎖的值
local storedSecret = redis.call("get", lockKey)
-- 續(xù)期邏輯
if storedSecret == lockSecret then
-- 值匹配則延長(zhǎng)過期時(shí)間
local result = redis.call("expire", lockKey, expireTime)
return result == 1 and "1" or "0" -- 續(xù)期成功返回"1"
else
-- 鎖不存在或值不匹配
return "0"
end
// 定時(shí)續(xù)約線程
watchdogExecutor.scheduleAtFixedRate(() -> {
locks.entrySet().removeIf(entry -> entry.getValue().isCancelled());
for (Entry<String, Lock> entry : locks.entrySet()) {
if (!entry.getValue().isCancelled()) {
String result = redisTemplate.execute(RENEWAL_SCRIPT,
Collections.singletonList(key),
lock.value, "30");
if ("0".equals(result)) lock.cancel();
}
}
}, 0, 10, TimeUnit.SECONDS);
設(shè)計(jì)思路:
- 續(xù)期間隔=過期時(shí)間/3(如30s過期則10s續(xù)期)
- 異步線程池需單獨(dú)配置
- 雙重校驗(yàn)鎖狀態(tài)(內(nèi)存標(biāo)記+Redis實(shí)際值)
2.3 安全釋放鎖
local lockKey = KEYS[1] -- 鎖的鍵名
local lockSecret = ARGV[1] -- 要釋放的鎖標(biāo)識(shí)
-- 獲取當(dāng)前鎖的值
local storedSecret = redis.call("get", lockKey)
-- 校驗(yàn)鎖歸屬
if storedSecret == lockSecret then
-- 值匹配則刪除Key
return redis.call("del", lockKey) == 1 and "1" or "0"
else
-- 值不匹配
return "0"
end
設(shè)計(jì)思路:
- 校驗(yàn)value避免誤刪其他線程的鎖
三、源碼
package org.example.tao.util;
import com.alibaba.fastjson2.JSON;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import javax.annotation.PreDestroy;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class RedisUtils {
static class Lock {
private final String value;
private volatile boolean isCancelled = false;
public Lock(String value) {
this.value = value;
}
public boolean isCancelled() {
return isCancelled;
}
public void cancel() {
isCancelled = true;
}
}
private static final String LOCK_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local expireTime = tonumber(ARGV[2]) -- 動(dòng)態(tài)過期時(shí)間\n" + "if not expireTime or expireTime <= 0 then\n" + " return \"0\"\n" + "end\n" + "local result = redis.call(\"set\", lockKey, lockSecret, \"NX\", \"EX\", expireTime)\n" + "return result and \"1\" or \"0\"";
private static final String RELEASE_LOCK_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local storedSecret = redis.call(\"get\", lockKey)\n" + "if storedSecret == lockSecret then\n" + " return redis.call(\"del\", lockKey) == 1 and \"1\" or \"0\"\n" + "else\n" + " return \"0\"\n" + "end";
private static final String RENEWAL_LUA = "local lockKey = KEYS[1]\n" + "local lockSecret = ARGV[1]\n" + "local expireTime = tonumber(ARGV[2])\n" + "if not expireTime or expireTime <= 0 then\n" + " return \"0\"\n" + "end\n" + "local storedSecret = redis.call(\"get\", lockKey)\n" + "if storedSecret == lockSecret then\n" + " local result = redis.call(\"expire\", lockKey, expireTime)\n" + " return result == 1 and \"1\" or \"0\"\n" + "else\n" + " return \"0\"\n" + "end";
private final String defaultExpireTime = "30";
private final RedisTemplate<String, String> redisTemplate;
private final Map<String, Lock> locks = new ConcurrentHashMap<>();
private final ScheduledExecutorService watchdogExecutor = Executors.newScheduledThreadPool(1);
public RedisUtils(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
watchdogExecutor.scheduleAtFixedRate(() -> {
try {
System.out.println("watchdogExecutor 執(zhí)行中... locks => " + JSON.toJSONString(locks));
locks.entrySet().removeIf(entry -> entry.getValue().isCancelled());
for (Map.Entry<String, Lock> entry : locks.entrySet()) {
String key = entry.getKey();
Lock lock = entry.getValue();
if (!lock.isCancelled()) {
RedisScript<String> redisScript = RedisScript.of(RENEWAL_LUA, String.class);
String result = redisTemplate.execute(redisScript, Collections.singletonList(key), lock.value, defaultExpireTime);
if (Objects.equals(result, "0")) {
lock.cancel(); // 移除已經(jīng)釋放的鎖
}
}
}
} catch (Exception e) {
System.err.println("看門狗任務(wù)執(zhí)行失敗: " + e.getMessage());
}
}, 0, 10, TimeUnit.SECONDS);
}
public boolean acquireLock(String key, String value) {
RedisScript<String> redisScript = RedisScript.of(LOCK_LUA, String.class);
String result = redisTemplate.execute(redisScript, Collections.singletonList(key), value, defaultExpireTime);
if (Objects.equals(result, "1")) {
locks.put(key, new Lock(value));
return true;
}
return false;
}
public boolean acquireLockWithRetry(String key, String value, int maxRetries, long retryIntervalMillis) {
int retryCount = 0;
while (retryCount < maxRetries) {
boolean result = this.acquireLock(key, value);
if (result) {
locks.put(key, new Lock(value));
return true;
}
retryCount++;
try {
Thread.sleep(retryIntervalMillis);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
return false;
}
public boolean releaseLock(String key, String value) {
RedisScript<String> redisScript = RedisScript.of(RELEASE_LOCK_LUA, String.class);
String result = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
if (Objects.equals(result, "1")) {
Lock lock = locks.get(key);
if (lock != null) {
lock.cancel();
}
return true;
}
return false;
}
@PreDestroy
public void shutdown() {
watchdogExecutor.shutdown();
try {
if (!watchdogExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
watchdogExecutor.shutdownNow();
}
} catch (InterruptedException e) {
watchdogExecutor.shutdownNow();
}
}
}
四、如何使用
4.1 配置類
@Configuration
public class AppConfig {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Bean
public RedisUtils init() {
return new RedisUtils(redisTemplate);
}
}
4.2 使用
@RestController
@RequestMapping("/users")
public class UserController {
@Resource
private RedisUtils redisUtils;
@PostMapping("/test2")
public Boolean test2(@RequestBody Map<String, String> map) {
boolean res;
if (Objects.equals(map.get("lockFlag"), "true")) {
res = redisUtils.acquireLock(map.get("key"), map.get("value"));
} else {
res = redisUtils.releaseLock(map.get("key"), map.get("value"));
}
return res;
}
}
后記
還是免責(zé)聲明,僅供學(xué)習(xí)參考
本文來自博客園,作者:帥氣的濤啊,轉(zhuǎn)載請(qǐng)注明原文鏈接:http://www.rzrgm.cn/handsometaoa/p/18723172

浙公網(wǎng)安備 33010602011771號(hào)