<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分布式鎖故障,我忍不住想爆粗...

          共 7228字,需瀏覽 15分鐘

           ·

          2022-07-04 14:54

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


          目錄
          • 背景

          • 問題分析

          • 解決方案

          • 總結(jié)


          背景


          Hollis的新書限時折扣中,一本深入講解Java基礎(chǔ)的干貨筆記!
          企微報警群里連續(xù)發(fā)出生產(chǎn)環(huán)境報錯警告,報錯核心信息如下:
          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)是項目中自定義的 Redis 分布式鎖報錯,并且該異常是在最近需求上線后突然出現(xiàn),并且伴隨該異常出現(xiàn)的,還有需求涉及的業(yè)務(wù)數(shù)據(jù)出現(xiàn)部分錯亂的問題。


          問題分析


          老規(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é),只看整體邏輯,問題不大。


          那再看實際加鎖方法:
          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 部分,如下圖:

          ②key 過期的情況,主要在線程加鎖并設(shè)置過期時間后,執(zhí)行業(yè)務(wù)代碼耗費的時間超過設(shè)置的鎖過期時間,并且在鎖過期前,未對鎖進(jìn)行續(xù)期:

          解決方案


          從上面的代碼看來,這已經(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 一下就有大量參考,此處不再介紹。


          目前比較成熟的綜合解決方案為使用 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è)置超時時間,防止無限自旋。默認(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之道公眾號


          好文章,我在看??

          瀏覽 43
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  天天色天天日 | 天堂成人网站 | 日批后入试势动态视频免费看 | 国产无码免费看 | 大香蕉久久久久 |