<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Redis分布式鎖故障,我忍不住想爆粗...

          共 7104字,需瀏覽 15分鐘

           ·

          2022-07-04 12:03

          文章來源:https://c1n.cn/OZvGN


          目錄
          • 背景

          • 問題分析

          • 解決方案

          • 總結(jié)


          背景


          企微報(bào)警群里連續(xù)發(fā)出生產(chǎn)環(huán)境報(bào)錯(cuò)警告,報(bào)錯(cuò)核心信息如下:
          redis setNX error java.lang.NumberFormatExceptionFor 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)。


          更多使用文檔詳見官方文檔:
          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)入交流群


          推薦閱讀:


          瀏覽 40
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲日韩欧美在线观看 | 三级片欧美网站 | 爽爽网站 | 国产精品无码专区AV在线播放 | 国产夜色精品一区二区AV |