記一次自定義Redis分布式鎖導(dǎo)致的生產(chǎn)事件
背景
企微報(bào)警群里連續(xù)發(fā)出生產(chǎn)環(huán)境報(bào)錯(cuò)警告,報(bào)錯(cuò)核心信息如下:
redis setNX error java.lang.NumberFormatException: For input string: "null"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.parseLong(Long.java:631)
經(jīng)異常信息定位,發(fā)現(xiàn)是項(xiàng)目中自定義的Redis分布式鎖報(bào)錯(cuò),并且該異常是在最近需求上線后突然出現(xiàn),并且伴隨該異常出現(xiàn)的,還有需求涉及的業(yè)務(wù)數(shù)據(jù)出現(xiàn)部分錯(cuò)亂的問題。
問題分析
老規(guī)矩,先貼涉及代碼
//切面
public class RedisLockAspect{
public void around(ProceedingJoinPoint pjp) {
String key = "...";
try {
//阻塞,直到獲取鎖為止
while (!JedisUtil.lock(key, timeOut)) {
Thread.sleep(10);
}
//執(zhí)行業(yè)務(wù)邏輯
pjp.proceed();
}finally {
JedisUtil.unLock(key);
}
}
}
以上為自定義Redis分布式鎖的切面,不看細(xì)節(jié),只看整體邏輯,問題不大,那再看實(shí)際加鎖方法。
public class JedisUtil{
public static boolean lock(String key, long timeOut){
long currentTimeMillis = System.currentTimeMillis();
long newExpireTime = currentTimeMillis + timeOut;
RedisConnection connection = null;
try {
connection = getRedisTemplate().getConnectionFactory().getConnection();
Boolean setNxResult = connection.setNX(key.getBytes(StandardCharsets.UTF_8), String.valueOf(newExpireTime).getBytes(StandardCharsets.UTF_8));
//位置1
if(setNxResult){
expire(key,timeOut, TimeUnit.MILLISECONDS);
return true;
}
//位置2
Object objVal = getRedisTemplate().opsForValue().get(key);
String currentValue = String.valueOf(objVal);
//位置3,異常位置為if判斷中Long.parseLong(currentValue),currentValue為null的字符串
if (currentValue != null && Long.parseLong(currentValue) < currentTimeMillis) {
String oldExpireTime = (String) getAndSet(key, String.valueOf(newExpireTime));
if (oldExpireTime != null && oldExpireTime.equals(currentValue)) {
return true;
}
}
}
return false;
}
public static void unLock(String key){
getRedisTemplate().delete(key);
}
}
有經(jīng)驗(yàn)的大佬看到這段代碼,估計(jì)會(huì)忍不住爆粗,但咱先不管,先看錯(cuò)誤位置。異常信息可以看出,currentValue的值為字符串“null”,即String.valueOf(objVal)中的objVal對(duì)象為null,也就是在Redis中,key對(duì)應(yīng)的value不存在,此時(shí)思考一下,key對(duì)應(yīng)的value不存在,無非以下兩種情況:
key被主動(dòng)刪除; key過期了。
繼續(xù)跟著代碼往上走,發(fā)現(xiàn)前面執(zhí)行了setNx命令,并且返回setNxResult表示是否成功。正常來說,當(dāng)setNxResult為false的時(shí)候,加鎖失敗,此時(shí)代碼時(shí)不應(yīng)該往下走的,但在本段代碼中,卻繼續(xù)往下走!問了下相關(guān)同事,說是為了做可重入鎖......(弱弱吐槽下,可重入鎖也不是這樣干的啊...)
其實(shí)分析到這,已經(jīng)可以知道是什么原因?qū)е碌漠惓9收狭耍瓷厦嬲f的,key被主動(dòng)刪除、key過期導(dǎo)致,下面假設(shè)有兩個(gè)線程,對(duì)同一個(gè)key加鎖,分別對(duì)應(yīng)以上兩種情況:key被主動(dòng)刪除的情況,發(fā)生于分布式鎖加鎖邏輯執(zhí)行完后,調(diào)用unlock方法,見以上RedisLockAspect類中finally部分,如下圖:
key過期的情況,主要在線程加鎖并設(shè)置過期時(shí)間后,執(zhí)行業(yè)務(wù)代碼耗費(fèi)的時(shí)間超過設(shè)置的鎖過期時(shí)間,并且在鎖過期前,未對(duì)鎖進(jìn)行續(xù)期:
解決方案
從上面的代碼看來,這已經(jīng)不是簡單的Long.parseLong("null")問題了,這是整個(gè)Redis分布式鎖實(shí)現(xiàn)的問題,并且該分布式鎖在整個(gè)項(xiàng)目中大量使用,可想而知其實(shí)問題非常嚴(yán)重,如果只是解決Long.parseLong("null")的問題,無疑就是隔靴撓癢,沒有任何意義的。一般情況下,自定義Redis分布式鎖容易出現(xiàn)以下幾大問題:
setNx鎖釋放問題; setNx Expire 原子性問題; 鎖過期問題; 多線程釋放鎖問題; 可重入問題; 大量失敗時(shí)自旋鎖問題; 主從架構(gòu)下鎖數(shù)據(jù)同步問題;
結(jié)合以上故障代碼,可以發(fā)現(xiàn)項(xiàng)目中的Redis分布式鎖實(shí)現(xiàn)幾乎未對(duì)Redis分布式鎖問題進(jìn)行考慮,以下為主要問題以及對(duì)應(yīng)解決方案:
setNx 和 expire 原子操作:使用Lua腳本,在一次Lua腳本命令中,執(zhí)行setNx 與 expire命令,保證原子性;鎖過期問題:為防止鎖自動(dòng)過期,可在鎖過期前,定時(shí)對(duì)鎖過期時(shí)間進(jìn)行續(xù)期。可重入問題:可重入設(shè)計(jì)粒度需到線程級(jí)別,可在鎖上加上線程唯一id。鎖自旋問題:參考JDK中AQS設(shè)計(jì),實(shí)現(xiàn)獲取鎖時(shí)最大等待時(shí)長。
對(duì)于項(xiàng)目中的問題以及每個(gè)問題的解決方案實(shí)現(xiàn),baidu一下就有大量參考,此處不再介紹。目前比較成熟的綜合解決方案為使用Redisson客戶端,以下為簡單偽代碼demo:
public class RedisLockAspect{
@Autowired
private Redisson redisson;
public void around(ProceedingJoinPoint pjp) {
String key = "...";
Long waitTime = 3000L;
//獲取鎖
RLock lock = redisson.getLock(key);
boolean lockSuccess = false;
try {
//加鎖設(shè)置超時(shí)時(shí)間,防止無限自旋。默認(rèn)啟用看門狗功能(自動(dòng)對(duì)鎖進(jìn)行續(xù)期)
lockSuccess = lock.tryLock(waitTime);
//執(zhí)行業(yè)務(wù)邏輯
pjp.proceed();
}finally {
//解鎖,防止釋放其他線程鎖
if (lock.isLocked() && lock.isHeldByCurrentThread() && lockSuccess){
lock.unlock();
}
}
}
}
使用Redisson可以快速解決目前項(xiàng)目中Redis分布式鎖存在的問題。除此之外,對(duì)于Redis主從架構(gòu)下數(shù)據(jù)同步導(dǎo)致的鎖問題,對(duì)應(yīng)的解決方案RedLock,也提供了相應(yīng)的實(shí)現(xiàn)。更多使用文檔詳見官方文檔github.com/liulongbiao…
總結(jié)
對(duì)于分布式鎖來說,可實(shí)現(xiàn)方案其實(shí)遠(yuǎn)遠(yuǎn)不止Redis這個(gè)實(shí)現(xiàn)途徑,比如基于Zookeeper、基于Etcd等方案,但其實(shí)對(duì)于目的來說,都是殊途同歸,重點(diǎn)在于,如何安全、正確的使用這些方案,保證業(yè)務(wù)正常。對(duì)于研發(fā)團(tuán)隊(duì)來說,針對(duì)類似的問題,需要對(duì)技術(shù)小伙伴進(jìn)行培訓(xùn),不斷提升技術(shù),更需要重視codereview工作,及時(shí)識(shí)別風(fēng)險(xiǎn),避免發(fā)生故障造成嚴(yán)重?fù)p失(本次故障造成臟數(shù)據(jù)修復(fù)耗時(shí)一個(gè)多星期)。敬畏技術(shù),忠于業(yè)務(wù)。
作者: 十年培訓(xùn)經(jīng)驗(yàn)的菜包
文章來源:https://c1n.cn/OZvGN
最近在準(zhǔn)備面試BAT,特地整理了一份面試資料,覆蓋Java核心技術(shù)分支、JVM、Java并發(fā)、SSM、微服務(wù)、數(shù)據(jù)庫、數(shù)據(jù)結(jié)構(gòu)等等。在這里,我為大家準(zhǔn)備了一份2021年最新最全的互聯(lián)網(wǎng)大廠Java面試經(jīng)驗(yàn)總結(jié)。


