Redis分布式鎖故障,我忍不住想爆粗...
文章來源:https://c1n.cn/OZvGN
背景
問題分析
解決方案
總結(jié)
背景
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)是項目中自定義的 Redis 分布式鎖報錯,并且該異常是在最近需求上線后突然出現(xiàn),并且伴隨該異常出現(xiàn)的,還有需求涉及的業(yè)務(wù)數(shù)據(jù)出現(xiàn)部分錯亂的問題。
問題分析
//切面
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é),只看整體邏輯,問題不大。
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)驗的大佬看到這段代碼,估計會忍不住爆粗,但咱先不管,先看錯誤位置。
異常信息可以看出,currentValue 的值為字符串“null”,即 String.valueOf(objVal) 中的 objVal 對象為 null,也就是在 Redis 中,key 對應(yīng)的 value 不存在。
此時思考一下,key 對應(yīng)的 value 不存在,無非以下兩種情況:
key 被主動刪除
key 過期了
繼續(xù)跟著代碼往上走,發(fā)現(xiàn)前面執(zhí)行了 setNx 命令,并且返回 setNxResult 表示是否成功。
正常來說,當(dāng) setNxResult 為 false 的時候,加鎖失敗,此時代碼時不應(yīng)該往下走的,但在本段代碼中,卻繼續(xù)往下走!
問了下相關(guān)同事,說是為了做可重入鎖......(弱弱吐槽下,可重入鎖也不是這樣干的啊...)
其實分析到這,已經(jīng)可以知道是什么原因?qū)е碌漠惓9收狭耍瓷厦嬲f的,key 被主動刪除、key 過期導(dǎo)致。
下面假設(shè)有兩個線程,對同一個 key 加鎖,分別對應(yīng)以上兩種情況:
①key 被主動刪除的情況,發(fā)生于分布式鎖加鎖邏輯執(zhí)行完后,調(diào)用 unlock 方法,見以上 RedisLockAspect 類中 finally 部分,如下圖:


解決方案
從上面的代碼看來,這已經(jīng)不是簡單的 Long.parseLong("null") 問題了,這是整個 Redis 分布式鎖實現(xiàn)的問題。
并且該分布式鎖在整個項目中大量使用,可想而知其實問題非常嚴(yán)重,如果只是解決 Long.parseLong("null") 的問題,無疑就是隔靴撓癢,沒有任何意義的。
一般情況下,自定義 Redis 分布式鎖容易出現(xiàn)以下幾大問題:
setNx 鎖釋放問題
setNx Expire 原子性問題
鎖過期問題
多線程釋放鎖問題
可重入問題
大量失敗時自旋鎖問題
主從架構(gòu)下鎖數(shù)據(jù)同步問題
結(jié)合以上故障代碼,可以發(fā)現(xiàn)項目中的 Redis 分布式鎖實現(xiàn)幾乎未對 Redis 分布式鎖問題進(jìn)行考慮。
以下為主要問題以及對應(yīng)解決方案:
setNx 和 expire 原子操作:使用 Lua 腳本,在一次 Lua 腳本命令中,執(zhí)行 setNx 與 expire 命令,保證原子性。
鎖過期問題:為防止鎖自動過期,可在鎖過期前,定時對鎖過期時間進(jìn)行續(xù)期。
可重入問題:可重入設(shè)計粒度需到線程級別,可在鎖上加上線程唯一 id。
鎖自旋問題:參考 JDK 中 AQS 設(shè)計,實現(xiàn)獲取鎖時最大等待時長。
對于項目中的問題以及每個問題的解決方案實現(xiàn),baidu 一下就有大量參考,此處不再介紹。
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è)置超時時間,防止無限自旋。默認(rèn)啟用看門狗功能(自動對鎖進(jìn)行續(xù)期)
lockSuccess = lock.tryLock(waitTime);
//執(zhí)行業(yè)務(wù)邏輯
pjp.proceed();
}finally {
//解鎖,防止釋放其他線程鎖
if (lock.isLocked() && lock.isHeldByCurrentThread() && lockSuccess){
lock.unlock();
}
}
}
}
使用 Redisson 可以快速解決目前項目中 Redis 分布式鎖存在的問題。除此之外,對于 Redis 主從架構(gòu)下數(shù)據(jù)同步導(dǎo)致的鎖問題,對應(yīng)的解決方案 RedLock,也提供了相應(yīng)的實現(xiàn)。
https://github.com/liulongbiao/redisson-doc-cn
總結(jié)
對于分布式鎖來說,可實現(xiàn)方案其實遠(yuǎn)遠(yuǎn)不止 Redis 這個實現(xiàn)途徑,比如基于 Zookeeper、基于 Etcd 等方案。
但其實對于目的來說,都是殊途同歸,重點在于,如何安全、正確的使用這些方案,保證業(yè)務(wù)正常。
對于研發(fā)團隊來說,針對類似的問題,需要對技術(shù)小伙伴進(jìn)行培訓(xùn),不斷提升技術(shù),更需要重視 codereview 工作,及時識別風(fēng)險,避免發(fā)生故障造成嚴(yán)重?fù)p失(本次故障造成臟數(shù)據(jù)修復(fù)耗時一個多星期)。
敬畏技術(shù),忠于業(yè)務(wù)。
完
我的新書《深入理解Java核心技術(shù)》已經(jīng)上市了,上市后一直蟬聯(lián)京東暢銷榜中,目前正在6折優(yōu)惠中,想要入手的朋友千萬不要錯過哦~長按二維碼即可購買~
長按掃碼享受6折優(yōu)惠
往期推薦

為防止被00后整頓,一公司招聘要求員工不能起訴公司

4年工作經(jīng)驗,多線程間的5種通信方式都說不出來,你敢信?

社招兩年半10個公司28輪面試面經(jīng)
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
