設(shè)計(jì)思路
基于 Redis 的 Setnx 命令:在指定的 key 不存在時(shí),為 key 設(shè)置指定的值命贴。具體思路和實(shí)現(xiàn)步驟梢莽,詳見代碼。
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicLong;
/**
* Redis分布式鎖
*
* @author Alisallon
* Created on 2021/4/25 9:34.
*/
@Component
public class RedisLock {
/**
* 保存鎖以及過期時(shí)間,用于解決釋放鎖造成的問題
*/
private static final Map<String, Long> LOCK_MAP = new HashMap<>();
private final StringRedisTemplate redisTemplate;
public RedisLock(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 嘗試獲取分布式鎖(加鎖)
*
* @param lock 鎖名稱
* @param expire 鎖過期時(shí)間
* @return 是否獲取到
*/
public boolean lock(String lock, long expire) {
try {
AtomicLong expireAt = new AtomicLong();
Object result = redisTemplate.execute((RedisCallback<Object>) connection -> {
// 嘗試給鎖設(shè)置值(保存的是未來的過期時(shí)間)
expireAt.set(System.currentTimeMillis() + expire + 1);
Boolean acquire = connection.setNX(lock.getBytes(), String.valueOf(expireAt.get()).getBytes());
if (Optional.ofNullable(acquire).orElse(false)) {
// 設(shè)置值成功,即獲取鎖成功(加鎖成功)
return true;
}
// 設(shè)置值失敗,即沒有獲取到鎖,獲取鎖的對(duì)應(yīng)的值(過期時(shí)間)
byte[] value = connection.get(lock.getBytes());
if (Objects.nonNull(value) && value.length > 0) {
// 獲取鎖的對(duì)應(yīng)的值(過期時(shí)間)成功
long expireTime = Long.parseLong(new String(value));
// 判斷鎖是否過期
if (expireTime < System.currentTimeMillis()) {
// 鎖已經(jīng)過期,表示沒有其他程序在占用鎖(不能排除占用鎖的程序,因?yàn)檫壿嫃?fù)雜造成執(zhí)行時(shí)間太長或者程序掛掉了,還沒來得及釋放鎖)
// 這里為了防止死鎖,直接對(duì)已過期的鎖重新設(shè)置過期時(shí)間,同時(shí)獲得設(shè)置新值之前的舊過期時(shí)間
expireAt.set(System.currentTimeMillis() + expire + 1);
byte[] oldValue = connection.getSet(lock.getBytes(), String.valueOf(expireAt.get()).getBytes());
if (Optional.ofNullable(oldValue).isPresent()) {
// 重新判斷設(shè)置新值之前的舊過期時(shí)間是否真的過期,因?yàn)榭赡軙?huì)同時(shí)存在多個(gè)程序在競爭該鎖
// 如果oldValue還未過期,說明該鎖被其他程序搶走了
// 如果oldValue已過期,說明該鎖未被占用,當(dāng)前程序可以獲得該鎖
return Long.parseLong(new String(oldValue)) < System.currentTimeMillis();
}
}
}
// 鎖的對(duì)應(yīng)的值失敗,返回獲取鎖失敗
return false;
});
if (Optional.ofNullable(result).map(t -> (Boolean) result).orElse(false)) {
// 獲取鎖成功
// 在當(dāng)前程序中保存該鎖和過期時(shí)間,會(huì)在釋放鎖時(shí)使用
LOCK_MAP.put(lock, expireAt.longValue());
return true;
}
// 獲取鎖失敗
return false;
} catch (Exception e) {
// 獲取鎖異常,返回獲取鎖失敗
return false;
}
}
/**
* 釋放鎖
* 必須和上面的lock方法成對(duì)出現(xiàn)
* 需要注意,如果lock方法后面的執(zhí)行邏輯里有try-catch,一定要在finally中釋放鎖
*
* @param lock 鎖名稱
*/
public void release(String lock) {
// 當(dāng)當(dāng)前占用鎖的程序因?yàn)檫壿嫃?fù)雜造成執(zhí)行時(shí)間太長(執(zhí)行正常無誤),超過了鎖的超時(shí)時(shí)間,這時(shí)鎖可能會(huì)被其他程序搶走
// 如果直接delete,可能會(huì)把其他程序搶走的鎖釋放,并且被另一個(gè)程序搶走,這會(huì)造成多個(gè)程序同一種業(yè)務(wù)邏輯并發(fā)執(zhí)行,可能會(huì)造成數(shù)據(jù)不一致的問題
// 為了解決這個(gè)問題,引入了LOCK_MAP
// 如果LOCK_MAP中存在該鎖,需要判斷該鎖的超時(shí)時(shí)間
if (LOCK_MAP.containsKey(lock)) {
// 已存在該鎖
long expireAt = LOCK_MAP.get(lock);
if (expireAt <= System.currentTimeMillis()) {
// 該鎖已過期,此時(shí)無需手動(dòng)釋放鎖,因?yàn)樵撴i可能已經(jīng)被其他程序搶走了
// 如果釋放了鎖,可能釋放的不是本程序獲得的鎖,而是別的程序已搶走的鎖,就可能會(huì)出現(xiàn)上面說的數(shù)據(jù)不一致的問題
return;
}
// 該鎖還未過期,可以釋放鎖,因?yàn)槟苤鲃?dòng)調(diào)用release方法的一定是已獲得鎖的程序
}
try {
// 釋放鎖
redisTemplate.delete(lock);
// 當(dāng)前程序移除鎖
LOCK_MAP.remove(lock);
} catch (Exception e) {
// 釋放鎖異常,可以無視
}
}
}