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)是項(xiàng)目中自定義的 Redis 分布式鎖報(bào)錯(cuò),并且該異常是在最近需求上線后突然出現(xiàn),并且伴隨該異常出現(xiàn)的,還有需求涉及的業(yè)務(wù)數(shù)據(jù)出現(xiàn)部分錯(cuò)亂的問題。
問題分析
//切面
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)驗(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 部分,如下圖:


解決方案
從上面的代碼看來,這已經(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 一下就有大量參考,此處不再介紹。
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)。
https://github.com/liulongbiao/redisson-doc-cn
總結(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ù)。
歡迎添加小編微信,進(jìn)入交流群
推薦閱讀:
