<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>

          java緩存一致性問題及解決方案

          共 12528字,需瀏覽 26分鐘

           ·

          2021-05-19 02:37

          點擊上方藍色字體,選擇“標星公眾號”

          優(yōu)質(zhì)文章,第一時間送達

          java緩存一致性問題及解決方案:使用緩存,肯定會存在一致性問題;

          讀取緩存步驟一般沒有什么問題,但是一旦涉及到數(shù)據(jù)更新:數(shù)據(jù)庫和緩存更新,就容 易出現(xiàn)緩存(Redis)和數(shù)據(jù)庫(MySQL)間的數(shù)據(jù)一致性問題。

           

          一、討論一致性問題之前,先來看一個更新的操作順序問題:

          先刪除緩存,再更新數(shù)據(jù)庫

          問題:同時有一個請求 A 進行更新操作,一個請求 B 進行查詢操作。可能出現(xiàn):

          (1)請求 A 進行寫操作(key = 1 value = 2),先刪除緩存 key = 1 value = 1

          (2)請求 B 查詢發(fā)現(xiàn)緩存不存在

          (3)請求 B 去數(shù)據(jù)庫查詢得到舊值 key = 1 value = 1

          (4)請求 B 將舊值寫入緩存 key = 1 value = 1

          (5)請求 A 將新值寫入數(shù)據(jù)庫 key = 1 value = 2

          緩存中數(shù)據(jù)永遠都是臟數(shù)據(jù)

           

          我們比較推薦操作順序:

          先刪除緩存,再更新數(shù)據(jù)庫,再刪緩存(雙刪,第二次刪可異步延時)

           

          public void write(String key,Object data){
              redis.delKey(key);
              db.updateData(data);
              Thread.sleep(500);
              redis.delKey(key);
          }

          接下來,看一看緩存同步的一些方案,見下圖:

           

           

          1、 數(shù)據(jù)實時同步更新

          更新數(shù)據(jù)庫同時更新緩存,使用緩存工具類和或編碼實現(xiàn)。

          優(yōu)點:數(shù)據(jù)實時同步更新,保持強一致性

          缺點:代碼耦合,對業(yè)務(wù)代碼有侵入性

           

          2、 數(shù)據(jù)準實時更新

          準一致性,更新數(shù)據(jù)庫后,異步更新緩存,使用觀察者模式/發(fā)布訂閱/MQ 實現(xiàn);

          優(yōu)點:數(shù)據(jù)同步有較短延遲 ,與業(yè)務(wù)解耦

           

          缺點:實現(xiàn)復雜,架構(gòu)較重

          3 、緩存失效機制

          弱一致性,基于緩存本身的失效機制

          優(yōu)點:實現(xiàn)簡單,無須引入額外邏輯

          缺點:有一定延遲,存在緩存擊穿/雪崩問題

           

          4、 定時任務(wù)更新

          最終一致性,采用任務(wù)調(diào)度框架,按照一定頻率更新

          優(yōu)點:不影響正常業(yè)務(wù)

          優(yōu)點:不保證一致性,依賴定時任務(wù)

          二、 緩存擊穿、緩存雪崩及解決方案

          1 、緩存擊穿

          緩存擊穿是指緩存中沒有但數(shù)據(jù)庫中有的數(shù)據(jù)(一般是緩存時間到期),這時由于 并發(fā)用戶特別多,同時讀緩存沒讀到數(shù)據(jù),又同時去數(shù)據(jù)庫去取數(shù)據(jù),引起數(shù)據(jù)庫壓力

          瞬間增大,造成過大壓力

           

          2 、緩存雪崩

          緩存雪崩是指緩存中數(shù)據(jù)大批量到過期時間,而查詢數(shù)據(jù)量巨大,引起數(shù)據(jù)庫壓 力過大甚至 down 機。和緩存擊穿不同的是,緩存擊穿指并發(fā)查同一條數(shù)據(jù),緩存雪崩

          是不同數(shù)據(jù)都過期了,很多數(shù)據(jù)都查不到從而查數(shù)據(jù)庫。

           

          解決方案:

          1)單體服務(wù):此時需要對數(shù)據(jù)庫的查詢操作,加鎖 ---- lock (因考慮到是對同一個參數(shù)數(shù)值上 一把鎖,此處 synchronized 機制無法使用) 加鎖的標準流程代碼如下:

           

          /**
           * 解決緩存雪崩和擊穿方案
           */
          @Service("provincesService")
          public class ProvincesServiceImpl3 extends ProvincesServiceImpl implements ProvincesService{
              private static final Logger logger = LoggerFactory.getLogger(ProvincesServiceImpl3.class);
              @Resource
              private CacheManager cm;//使用注解緩存
              private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();//線程安全的
           
              private static final String CACHE_NAME = "province";
           
              public Provinces detail(String provinceid) {
                  // 1.從緩存中取數(shù)據(jù)
                  Cache.ValueWrapper valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);
                  if (valueWrapper != null) {
                      logger.info("緩存中得到數(shù)據(jù)");
                      return (Provinces) (valueWrapper.get());
                  }
           
                  //2.加鎖排隊,阻塞式鎖---100個線程走到這里---同一個sql的取同一把鎖
                  doLock(provinceid);//32個省,最多只有32把鎖,1000個線程
                  try{//第二個線程進來了
                      // 一次只有一個線程
                       //雙重校驗,不加也沒關(guān)系,無非是多刷幾次庫
                      valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);//第二個線程,能從緩存里拿到值?
                      if (valueWrapper != null) {
                          logger.info("緩存中得到數(shù)據(jù)");
                          return (Provinces) (valueWrapper.get());//第二個線程,這里返回
                      }
           
                      Provinces provinces = super.detail(provinceid);
                      // 3.從數(shù)據(jù)庫查詢的結(jié)果不為空,則把數(shù)據(jù)放入緩存中,方便下次查詢
                      if (null != provinces){
                          cm.getCache(CACHE_NAME).put(provinceid, provinces);
                      }
                      return provinces;
                  }catch(Exception e){
                      return null;
                  }finally{
                      //4.解鎖
                      releaseLock(provinceid);
                  }
              }
           
              private void releaseLock(String userCode) {
                  ReentrantLock oldLock = (ReentrantLock) locks.get(userCode);
                  //查詢鎖是否存在和查詢當前線程是否保持此鎖
                  if(oldLock !=null && oldLock.isHeldByCurrentThread()){
                      oldLock.unlock();
                  }
              }
           
              private void doLock(String lockcode) {//給一個搜索條件,對應(yīng)一個鎖
                  //provinceid有不同的值,參數(shù)多樣化
                  //provinceid相同的,加一個鎖,---- 不是同一個key,不能用同一個鎖
                  ReentrantLock newLock = new ReentrantLock();//創(chuàng)建一個鎖
                  Lock oldLock = locks.putIfAbsent(lockcode, newLock);//若已存在,則newLock直接丟棄
                  if(oldLock == null){
                      newLock.lock();//首次加鎖,成功取鎖,執(zhí)行
                  }else{
                      oldLock.lock();//阻塞式等待取鎖
                  }
              }
          }

          2}  集群或微服務(wù)場景下:

          此場景下的鎖換成分布式鎖(redis或zk等);同時設(shè)置多次取鎖功能;

          /**
           * 解決緩存雪崩和擊穿方案
           */
          @Service("provincesService")
          public class ProvincesServiceImpl5 extends ProvincesServiceImpl implements ProvincesService{
              private static final Logger logger = LoggerFactory.getLogger(ProvincesServiceImpl3.class);
              @Resource
              private CacheManager cm;//使用注解緩存
           
              @Autowired
              private RedisTemplate<String, Object> redisTemplate;
              private ConcurrentHashMap<String, Lock> locks = new ConcurrentHashMap<>();//線程安全的
           
              private static final String CACHE_NAME = "province";
           
              public Provinces detail(String provinceid) throws Exception{
                  // 1.從緩存中取數(shù)據(jù)
                  Cache.ValueWrapper valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);
                  if (valueWrapper != null) {
                      logger.info("緩存中得到數(shù)據(jù)");
                      return (Provinces) (valueWrapper.get());
                  }
           
                  //2.加鎖排隊,阻塞式鎖---100個線程走到這里---同一個sql的取同一把鎖
                 //32個省,最多只有32把鎖,1000個線程
                  boolean flag=false;
                  flag = RedisUtil.setNX(provinceid, 3000);
                  //如果首次沒有取到鎖,可以取10次
                  if(!flag){
                      for(int i=0;i<10;i++){
                          Thread.sleep(200);
                          flag = RedisUtil.setNX(provinceid, 3000);//分布式鎖
                          if(flag){
                              break;
                          }
                      }
                  }
                  //如果首次沒有取到鎖,一直取直到取到為止
               /*   if(!flag){
                      for (;;){
                          Thread.sleep(200);
                          flag = RedisUtil.setNX(provinceid, 3000);//分布式鎖
                          if(flag){
                              break;
                          }
                      }
                  }*/
                  try{//第二個線程進來了
                      // 一次只有一個線程
                       //雙重校驗,不加也沒關(guān)系,無非是多刷幾次庫
                      valueWrapper = cm.getCache(CACHE_NAME).get(provinceid);//第二個線程,能從緩存里拿到值?
                      if (valueWrapper != null) {
                          logger.info("緩存中得到數(shù)據(jù)");
                          return (Provinces) (valueWrapper.get());//第二個線程,這里返回
                      }
                      Provinces provinces = super.detail(provinceid);
                      // 3.從數(shù)據(jù)庫查詢的結(jié)果不為空,則把數(shù)據(jù)放入緩存中,方便下次查詢
                      if (null != provinces){
                          cm.getCache(CACHE_NAME).put(provinceid, provinces);
                      }
                      return provinces;
                  }catch(Exception e){
                      return null;
                  }finally{
                      //4.解鎖
                      RedisUtil.releaseLock(provinceid);
                  }
              }
          }

          這里加分布式鎖解決緩存一致性問題,也解決緩存擊穿的問題;分布式鎖參考:分布式鎖使用及原理。





          版權(quán)聲明本文為博主原創(chuàng)文章,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明

          本文鏈接

          https://blog.csdn.net/nandao158/article/details/112757347






          鋒哥最新SpringCloud分布式電商秒殺課程發(fā)布

          ??????

          ??長按上方微信二維碼 2 秒





          感謝點贊支持下哈 

          瀏覽 65
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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 |