<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      Redis中篇--應(yīng)用

      Redis中篇--應(yīng)用

      上篇看這里:Redis上篇

      csdn地址:https://blog.csdn.net/okok__TXF/article/details/148538138

      微信公眾號(hào):https://mp.weixin.qq.com/s/aQUfCQ6OEEdF27V285A2LA

      本文示例代碼見(jiàn)GITEE倉(cāng)庫(kù)中的【redis-analysis

      地址:https://gitee.com/quercus-sp204/new-technology/tree/master/redis-analysis

      1. 緩存問(wèn)題

      Redis作為緩存的時(shí)候,會(huì)發(fā)生一些經(jīng)典問(wèn)題。下面就來(lái)分析一下:

      1.1 緩存擊穿

      緩存擊穿是指一個(gè)訪問(wèn)量非常大(熱點(diǎn))的緩存 Key,在它過(guò)期失效(Expire)的瞬間,大量并發(fā)的請(qǐng)求同時(shí)發(fā)現(xiàn)緩存失效(Cache Miss),這些請(qǐng)求會(huì)擊穿緩存層,直接去查詢底層數(shù)據(jù)庫(kù)(如 MySQL)。這會(huì)導(dǎo)致數(shù)據(jù)庫(kù)在極短時(shí)間內(nèi)承受巨大的、遠(yuǎn)超其處理能力的請(qǐng)求壓力,可能導(dǎo)致數(shù)據(jù)庫(kù)響應(yīng)變慢、連接耗盡甚至崩潰,進(jìn)而引發(fā)整個(gè)系統(tǒng)的連鎖故障。

      舉個(gè)例子,就比如倉(cāng)庫(kù)門口堆放的一件特別搶手的熱門商品剛好賣完(緩存失效),一大群等著買它的人瞬間沖進(jìn)倉(cāng)庫(kù)搶購(gòu),把倉(cāng)庫(kù)管理員(DB)累垮了。

      那么,我們可以總結(jié)出這個(gè)緩存問(wèn)題的特征:

      首先,它是針對(duì)單個(gè) Key 問(wèn)題集中在某一個(gè)特定的熱點(diǎn) Key 上, 這個(gè) Key 對(duì)應(yīng)的數(shù)據(jù)訪問(wèn)頻率非常高;

      然后呢,時(shí)機(jī)發(fā)生在在過(guò)期瞬間,在該 Key 設(shè)置的過(guò)期時(shí)間剛好到達(dá)的那一刻,這個(gè)時(shí)刻有高并發(fā)請(qǐng)求,同時(shí)這個(gè)時(shí)刻請(qǐng)求全打到數(shù)據(jù)庫(kù)了;

      我們就要防止在緩存失效瞬間,大量請(qǐng)求同時(shí)訪問(wèn)數(shù)據(jù)庫(kù)。那么我們?cè)撛趺唇鉀Q呢?

      • 方法一:互斥鎖

      該方法的核心思想:既然是大量請(qǐng)求都直接訪問(wèn)數(shù)據(jù)庫(kù)了,那我就在數(shù)據(jù)庫(kù)那一層加鎖,保證同一時(shí)刻只有一個(gè)線程訪問(wèn),這樣就減輕數(shù)據(jù)庫(kù)的壓力。

      雙檢鎖

      // 緩存擊穿,解決方式1---雙檢鎖
      public Goods getGoodsDetailById(String goodsId) {
          // 1.先從緩存里面查
          Goods goodsObj = (Goods) valueOperations.get(RedisKey.GOODS_KEY + goodsId);
          // 2.緩存沒(méi)有再查數(shù)據(jù)庫(kù)
          if (goodsObj == null) {
              synchronized ( goodsId.intern() ) {
                  goodsObj = (Goods) valueOperations.get(RedisKey.GOODS_KEY + goodsId);
              }
              if (goodsObj != null) {
                  return goodsObj;
              }
      
              Goods goods = getDbGoodsById(goodsId);
              if (goods != null) {
                  valueOperations.set(RedisKey.GOODS_KEY + goodsId, goods);
                  return goods;
              }
              return null;
          }
          return goodsObj;
      }
      

      加鎖檢查緩存(第一次檢查):當(dāng)用戶請(qǐng)求數(shù)據(jù)時(shí),首先檢查緩存中是否存在該數(shù)據(jù)。

      加鎖:如果緩存中沒(méi)有數(shù)據(jù),那么走DB。但是,在嘗試從數(shù)據(jù)庫(kù)查詢數(shù)據(jù)之前,使用本地鎖(或者分布式鎖)來(lái)確保只有一個(gè)請(qǐng)求能夠執(zhí)行數(shù)據(jù)庫(kù)查詢操作。

      數(shù)據(jù)庫(kù)查詢:如果成功獲取到鎖,那么第二次檢查緩存,如果確實(shí)緩存中沒(méi)有數(shù)據(jù), 執(zhí)行數(shù)據(jù)庫(kù)查詢操作,獲取最新的數(shù)據(jù)。

      更新緩存:將查詢到的數(shù)據(jù)寫入緩存,并設(shè)置一個(gè)合理的過(guò)期時(shí)間。

      釋放鎖:完成緩存更新后,釋放分布式鎖,以便其他請(qǐng)求可以繼續(xù)執(zhí)行。

      返回?cái)?shù)據(jù):將查詢到的數(shù)據(jù)返回給用戶。

      處理其他請(qǐng)求:對(duì)于在等待鎖釋放期間到達(dá)的請(qǐng)求,它們可以直接從緩存中獲取數(shù)據(jù),而不需要再次查詢數(shù)據(jù)庫(kù)。

      上面的問(wèn)題也很明顯,那就是假如查數(shù)據(jù)庫(kù),寫入緩存這倆步驟,如果耗時(shí)過(guò)長(zhǎng),前面的請(qǐng)求會(huì)被一直阻塞住的。治標(biāo)不治本哪

      • 方法二:key不過(guò)期

      【永不過(guò)期】設(shè)置key的時(shí)候,不讓其過(guò)期,由于是永不過(guò)期的,故需要考慮更新這個(gè)key的值;

      【邏輯過(guò)期】緩存 Key 不設(shè)置過(guò)期時(shí)間(或設(shè)置一個(gè)很長(zhǎng)的過(guò)期時(shí)間),讓其“永不過(guò)期”。但是我們可以在緩存 Value 中,額外存儲(chǔ)一個(gè)邏輯過(guò)期時(shí)間戳(例如 {value: obj, expireTime: 12345678910}),當(dāng)應(yīng)用讀取緩存時(shí),檢查 Value 中的邏輯過(guò)期時(shí)間戳,如果未過(guò)期,直接返回?cái)?shù)據(jù);如果已過(guò)期,觸發(fā)一個(gè)異步任務(wù)(如放入消息隊(duì)列、啟動(dòng)一個(gè)線程池任務(wù)都可以)去更新緩存。當(dāng)前請(qǐng)求可以直接返回已過(guò)期的舊數(shù)據(jù)(業(yè)務(wù)允許短暫不一致)或嘗試獲取互斥鎖進(jìn)行同步更新(類似方法 1)。

      此方法緩存 Key 永不失效,徹底避免“失效瞬間”的問(wèn)題,用戶請(qǐng)求基本不受緩存更新影響,延遲低(直接返回舊數(shù)據(jù)或觸發(fā)異步更新)。

      但是問(wèn)題也是非常的明顯:首先就是業(yè)務(wù)要容忍數(shù)據(jù)的不一致性;需要實(shí)現(xiàn)異步更新機(jī)制(消息隊(duì)列、線程池),增加了設(shè)計(jì)的復(fù)雜性;其次呢,如果異步更新失敗或延遲過(guò)大,用戶可能長(zhǎng)時(shí)間讀到舊數(shù)據(jù);

      • 方法三:熱點(diǎn)key探測(cè),提前刷新

      在緩存熱點(diǎn) Key 即將過(guò)期之前(比如還剩 1 分鐘時(shí)),主動(dòng)觸發(fā)一個(gè)后臺(tái)任務(wù)(定時(shí)任務(wù)、監(jiān)控線程)去查詢數(shù)據(jù)庫(kù)并更新緩存,重置其 TTL。這種方式看起來(lái)算是完美的,但是丟給了我們一個(gè)致命問(wèn)題,那就是哪些key是熱點(diǎn)key?

      1.1.1 hotKey問(wèn)題

      首先我們要明白一點(diǎn),HotKey是什么,如何定義HotKey?

      HotKey指的是 Redis 緩存中被高頻訪問(wèn)的鍵,這類鍵的訪問(wèn)量遠(yuǎn)高于其他普通鍵。這類鍵稱之為“熱鍵”。

      比如說(shuō),秒殺模塊中,可能會(huì)將商品信息緩存在Redis中,那么,某些商品的skuId可能作為key,在秒殺這段時(shí)間里面,這類key的訪問(wèn)量可能會(huì)明顯高于其他鍵;此外呢,如果有人惡意訪問(wèn)緩存,發(fā)送大量請(qǐng)求到指定key,也可能導(dǎo)致該key變成熱鍵;還有像新聞網(wǎng)站上的突發(fā)新聞、論壇的熱點(diǎn)文章帖子等等.......

      如果這個(gè)時(shí)候這個(gè)熱鍵過(guò)期了,就會(huì)造成緩存擊穿的問(wèn)題,巨量請(qǐng)求直接到達(dá)了MySQL數(shù)據(jù)庫(kù)層,很有可能會(huì)造成服務(wù)的不可用。

      如何去監(jiān)測(cè)這些熱key呢,出現(xiàn)了熱key問(wèn)題,該怎么辦?或者說(shuō),我們要如何去預(yù)防這類問(wèn)題的出現(xiàn)?

      我認(rèn)為可以由以下幾步結(jié)合起來(lái),給他來(lái)一套組合拳:

      • 系統(tǒng)功能設(shè)計(jì)之初,憑借經(jīng)驗(yàn)判斷可能的熱key:

      我丟?這種方法是可以說(shuō)的嗎?肯定可以啊,就好比說(shuō)爛了的秒殺系統(tǒng),肯定就可以比較容易判斷出哪些key可能變成熱key了吧,電商系統(tǒng)中的商品詳情頁(yè)、或者是新上的活動(dòng)促銷、社交平臺(tái)上的熱門帖子等數(shù)據(jù)通常容易成為熱點(diǎn)Key。

      • Redis客戶端代碼層面增加訪問(wèn)次數(shù)記錄:

      在Redis客戶端層面,我們手動(dòng)記錄key的訪問(wèn)次數(shù)、或者是xxx時(shí)間間隔的訪問(wèn)頻率,如果有監(jiān)控系統(tǒng),我們可以將這類數(shù)據(jù)定時(shí)或者實(shí)時(shí)上報(bào)的監(jiān)控系統(tǒng),然后就可以在監(jiān)控系統(tǒng)看到了;

      • 在Redis服務(wù)端:

      它本身提供了相應(yīng)的功能:執(zhí)行一些相關(guān)命令(如MONITOR、redis-cli --hotkeys等),通過(guò)分析這些命令,可以觀察到哪些Key被頻繁訪問(wèn),識(shí)別出熱點(diǎn)Key。

      MONITOR 命令是 Redis 提供的一種實(shí)時(shí)查看 Redis 的所有操作的方式,可以用于臨時(shí)監(jiān)控 Redis 實(shí)例的操作情況,包括讀寫、刪除等操作。由于該命令對(duì) Redis 性能的影響比較大,被逼到墻角的時(shí)候,我們可以暫時(shí)使用這個(gè)命令,得到其輸出之后,關(guān)閉它,然后對(duì)其輸出歸類分析。

      同理,--hotkeys也會(huì)增加 Redis 實(shí)例的 CPU 和內(nèi)存消耗(全局掃描),因此需要謹(jǐn)慎使用

      • 【無(wú)意之間,我在GITEE上看asyncTool的時(shí)候,在其主頁(yè)翻到了京東零售的熱點(diǎn)數(shù)據(jù)監(jiān)測(cè)】

      它的hotKey項(xiàng)目:這個(gè)就交給讀者自行查看了。

      上面好像說(shuō)的都是監(jiān)測(cè)key,檢測(cè)到了能怎么辦呢?

      在單服務(wù)器承受范圍之內(nèi),我們可以結(jié)合本節(jié)開(kāi)始前的方法三,如果設(shè)置了過(guò)期時(shí)間,可以刷新其過(guò)期時(shí)間,可以避免緩存擊穿的問(wèn)題了哇。

      在單個(gè)服務(wù)器承受范圍之外: 這就涉及到Redis主從架構(gòu)等等問(wèn)題了,本文不做探討。

      • 如果是用 : redis 主從架構(gòu),可以通過(guò)增加Redis集群中的從節(jié)點(diǎn),增加 多個(gè)讀的副本。通過(guò)對(duì)讀流量進(jìn)行 負(fù)載均衡, 將讀流量 分散到更多的從節(jié)點(diǎn) 上,減輕單個(gè)節(jié)點(diǎn)的壓力。【節(jié)選一下參考1的話】
      • 通過(guò)改變Key的結(jié)構(gòu)(如添加隨機(jī)前綴),將同一個(gè)熱點(diǎn)Key拆分成多個(gè)Key,使其分布在不同的Redis節(jié)點(diǎn)上,從而避免所有流量集中在一個(gè)節(jié)點(diǎn)上。【節(jié)選一下參考1的話】

      1.2 緩存穿透

      緩存穿透是指查詢的數(shù)據(jù)既不在緩存中,也不在數(shù)據(jù)庫(kù)中,但惡意或異常請(qǐng)求持續(xù)訪問(wèn),導(dǎo)致每次請(qǐng)求都直接穿透到數(shù)據(jù)庫(kù)的現(xiàn)象。這會(huì)顯著增加數(shù)據(jù)庫(kù)壓力,甚至引發(fā)系統(tǒng)崩潰。

      這種緩存問(wèn)題和緩存擊穿有點(diǎn)像,但是它的特征是請(qǐng)求的Key在緩存和數(shù)據(jù)庫(kù)中均不存在(如惡意構(gòu)造的非法ID:id=-1id=99999999)。

      緩存穿透:數(shù)據(jù)根本不存在(緩存和數(shù)據(jù)庫(kù)均無(wú))。

      緩存擊穿:數(shù)據(jù)存在但緩存失效(數(shù)據(jù)庫(kù)中有數(shù)據(jù))?!緹狳c(diǎn)key問(wèn)題】

      那么,緩存穿透我們?cè)撛趺唇鉀Q呢?

      • 方法一:緩存空值

      這個(gè)很容易就想到了,但是為了避免緩存被長(zhǎng)時(shí)間占用,需要給這個(gè)空值加一個(gè)比較短的過(guò)期時(shí)間,例如幾分鐘。

      假如有大量無(wú)效請(qǐng)求穿透過(guò)來(lái)時(shí),緩存內(nèi)就會(huì)有 大量的空值緩存。

      所以,我們針對(duì)這種接口,我認(rèn)為可以在查詢緩存之前,判斷那種必定無(wú)效參數(shù),直接將無(wú)效參數(shù)的請(qǐng)求給過(guò)濾掉,比如說(shuō):數(shù)據(jù)庫(kù)中id都是正數(shù),但是傳過(guò)來(lái)的id是個(gè)負(fù)數(shù),我們就可以將這種請(qǐng)求直接過(guò)濾掉了,等等。但是,這個(gè)措施也僅僅能起到一丟丟作用罷了?!菊?qǐng)求層攔截過(guò)濾一下】

      那么有什么辦法可以避免緩存大量的空value呢?

      • 方法二:布隆過(guò)濾器

      布隆過(guò)濾器的介紹見(jiàn)這篇文章:【分分鐘搞懂布隆過(guò)濾器

      地址:https://mp.weixin.qq.com/s/PmJ8nMFbxJ1qI3hGt8s9lw

      我們可以在查詢緩存前,先走一遍布隆過(guò)濾器,如果布隆過(guò)濾器說(shuō)不存在該key,則一定不存在;反之,則可能存在。

      但是,我們需要預(yù)先將數(shù)據(jù)庫(kù)中的key加載到布隆過(guò)濾器中去。這就引出了另一個(gè)問(wèn)題:那就是數(shù)據(jù)庫(kù)中key和布隆過(guò)濾器中的一致性問(wèn)題。

      下面給出布隆過(guò)濾器的簡(jiǎn)單例子:

      // 首先使用Redisson創(chuàng)建一個(gè)商品goods的布隆過(guò)濾器
      @Bean("goodsBloomFilter")
      public RBloomFilter<Object> bloomFilter() {
          RBloomFilter<Object> goodsBloomFilter = redissonClient.getBloomFilter("goodsBloomFilter");
          goodsBloomFilter.tryInit(expectedInsertions, fpp);
          //  初始化布隆過(guò)濾器
          List<Goods> goods = Goods.goods;
          for (Goods good : goods) {
              goodsBloomFilter.add(good.getGoodsId());
          }
          return goodsBloomFilter;
      }
      
      // 2.緩存穿透,解決方式 -- 布隆過(guò)濾器【本方法只用于解決緩存穿透的問(wèn)題】
      // 實(shí)際肯定也要考慮緩存擊穿的。
      public Goods getGoodsDetailById_cacheThrough(String goodsId) {
          boolean contains = goodsBloomFilter.contains(goodsId);
          if (contains) {
              // 先到緩存里面找
              Goods goodsObj = (Goods) valueOperations.get(RedisKey.GOODS_KEY + goodsId);
              if (goodsObj != null) return goodsObj;
              // 如果找不到
              Goods goods = getDbGoodsById(goodsId);
              if (goods != null) {
                  valueOperations.set(RedisKey.GOODS_KEY + goodsId, goods);
                  return goods;
              }
              return null;
          } else {
              return null;
          }
      }
      

      看過(guò)上面給的鏈接里面的文章,想必大家也知道問(wèn)題所在:那就是其存在誤判率(可通過(guò)增加哈希函數(shù)降低),同時(shí)呢需要我們定期更新合法Key集;

      現(xiàn)在,我們來(lái)對(duì)比一下:

      方案 適用場(chǎng)景 注意事項(xiàng)
      緩存空值 動(dòng)態(tài)生成的Key或臨時(shí)數(shù)據(jù) 設(shè)置短TTL,避免內(nèi)存浪費(fèi)
      請(qǐng)求層攔截 所有場(chǎng)景的初級(jí)防御 結(jié)合參數(shù)校驗(yàn)與限流機(jī)制
      布隆過(guò)濾器 海量合法Key已知的場(chǎng)景(如商品ID) 需定期更新Key集合,控制誤判率

      綜上呢,我們可以組合一下這些方案,形成一個(gè)比較有效的解決方案

      如果誤判了,怎么辦呢?

      1.3 緩存雪崩

      緩存雪崩是指因大量緩存數(shù)據(jù)在同一時(shí)間集中失效或緩存服務(wù)崩潰,導(dǎo)致海量請(qǐng)求直接涌向數(shù)據(jù)庫(kù),引發(fā)數(shù)據(jù)庫(kù)瞬時(shí)壓力激增、系統(tǒng)性能驟降甚至崩潰的現(xiàn)象。這些 Key 的大量并發(fā)請(qǐng)求,瞬間全部穿透緩存層,直接涌向底層數(shù)據(jù)庫(kù)(如 MySQL)。

      與前兩個(gè)問(wèn)題比對(duì)一下,可以發(fā)現(xiàn)其特征:涉及大量緩存 Key(可能是幾十、幾百、甚至成千上萬(wàn)),而非單個(gè) Key;這些 Key 的失效時(shí)間點(diǎn)非常集中(例如,在同一秒、同一分鐘內(nèi));在失效發(fā)生的時(shí)刻,恰好有對(duì)這些 Key 的正常業(yè)務(wù)請(qǐng)求(通常量很大)。主要還是同一時(shí)間過(guò)期key的數(shù)量太多了。

      它的成因:

      【設(shè)置相同的過(guò)期時(shí)間(TTL)】: 這是最常見(jiàn)的原因。在初始化緩存數(shù)據(jù)或在批量更新緩存時(shí),為大量 Key 設(shè)置了完全相同的 TTL(例如,都設(shè)為 24 小時(shí))。當(dāng) 24 小時(shí)后,這些 Key 同時(shí)失效。又或者是在每天凌晨執(zhí)行批量緩存刷新任務(wù),刷新后新 Key 的 TTL 相同。

      緩存服務(wù)災(zāi)難性故障:】Redis 集群宕機(jī): 主節(jié)點(diǎn)宕機(jī)且未成功切換從節(jié)點(diǎn),或哨兵/集群模式本身出現(xiàn)故障。網(wǎng)絡(luò)分區(qū): 導(dǎo)致應(yīng)用服務(wù)器無(wú)法連接 Redis 集群。大范圍重啟/升級(jí): 運(yùn)維操作導(dǎo)致整個(gè) Redis 集群或大部分節(jié)點(diǎn)不可用。

      問(wèn)題不能完全解決,我們只能盡可能的去預(yù)防這些問(wèn)題的產(chǎn)生:

      首先在key層面,采用過(guò)期時(shí)間隨機(jī)化:這個(gè)例子就不舉了吧,設(shè)置緩存的時(shí)候,將TTL給它Random,如果想要更隨機(jī),可以網(wǎng)上找一些均勻的隨機(jī)算法,然后自己diy一下就好了。對(duì)相當(dāng)核心、容易預(yù)測(cè)的key數(shù)據(jù)可采用 永不過(guò)期+異步刷新。

      其次呢,可以在Redis層面,搭建高可用的集群,來(lái)預(yù)防基礎(chǔ)設(shè)施故障;

      在緩存層面,我們可以構(gòu)建多級(jí)緩存來(lái)提升韌性: 對(duì)于性能要求極高、存在極端熱點(diǎn)的系統(tǒng),考慮引入本地緩存(caffeine)構(gòu)建多級(jí)緩存,能有效抵御 Redis 層雪崩對(duì)數(shù)據(jù)庫(kù)的沖擊。但是需解決一致性問(wèn)題。

      最后可以來(lái)一個(gè)兜底措施:比如說(shuō)熔斷降級(jí) ,來(lái)防止數(shù)據(jù)庫(kù)崩潰和雪崩擴(kuò)散;服務(wù)限流 來(lái)控制流量洪峰、從而也可以保護(hù)數(shù)據(jù)庫(kù)。

      綜合比較一下上面所說(shuō)的/緩存三個(gè)主要的問(wèn)題:

      • 緩存擊穿: 單個(gè)熱點(diǎn) Key 失效瞬間遭遇 高并發(fā)關(guān)鍵:?jiǎn)蝹€(gè)熱點(diǎn) Key 失效 + 高并發(fā)。倉(cāng)庫(kù)門口堆放的一件特別搶手的熱門商品剛好賣完(緩存失效),一大群等著買它的人瞬間沖進(jìn)倉(cāng)庫(kù)搶購(gòu),把倉(cāng)庫(kù)管理員(DB)累垮了。其他商品還能正常在門口買。
      • 緩存穿透: 查詢 數(shù)據(jù)庫(kù)中根本不存在 的數(shù)據(jù)。關(guān)鍵:數(shù)據(jù)不存在。 請(qǐng)求必定穿透緩存訪問(wèn) DB。你要找的東西壓根不存在于倉(cāng)庫(kù)(DB)里,每次都得翻遍倉(cāng)庫(kù)確認(rèn)沒(méi)有。倉(cāng)庫(kù)管理員(DB)每次都得白忙活一趟。不管門口有沒(méi)有貨架(緩存),都得進(jìn)倉(cāng)庫(kù)。
      • 緩存雪崩: 大量 Key 同時(shí)失效Redis 集群整體不可用。關(guān)鍵:大規(guī)模失效 + 高并發(fā)。倉(cāng)庫(kù)門口堆放的很多常用物品(緩存)在同一時(shí)間被清空了(同時(shí)失效),所有人(請(qǐng)求)涌進(jìn)倉(cāng)庫(kù)(數(shù)據(jù)庫(kù))找東西,把倉(cāng)庫(kù)擠爆了,并且因?yàn)閭}(cāng)庫(kù)癱瘓,導(dǎo)致整個(gè)商場(chǎng)(系統(tǒng))停擺。

      雪崩就像一場(chǎng)雪,擊穿就像這場(chǎng)雪里面的一片雪花。

      2. 其他實(shí)戰(zhàn)

      2.1 排行榜

      Redis 的 ZSET 天然支持按分值(Score)排序,完美契合排行榜需求,下面我們來(lái)模擬一下文章閱讀量前十排行榜,結(jié)合Redis實(shí)現(xiàn)的:

      Key 設(shè)計(jì)article_ranking(存儲(chǔ)文章閱讀量排行)

      成員(Member):文章標(biāo)題(如 article:鋼鐵是怎樣煉成的!),也可以放文章ID,查出前十文章ID之后,然后通過(guò)id查詢文章。

      分值(Score):文章閱讀量(整數(shù)類型)

      見(jiàn)如下代碼:

      @RequestMapping("/other")
      @RestController
      public class OtherController {
          @Resource
          private ZSetOperations<String, Object> zSetOperations;
      
          // 1.訪問(wèn)文章
          @GetMapping("/accessArticle")
          public R accessArticle() {
              // 假設(shè)有30篇文章
              List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
                      17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30);
              // 模擬100個(gè)用戶訪問(wèn)文章
              Random random = new Random();
              for ( int i = 0; i < 100; ++i ) {
                  // 生成1-30隨機(jī)數(shù)
                  int index = random.nextInt(list.size());
                  // 文章訪問(wèn)數(shù)加一
                  zSetOperations.incrementScore(RedisKey.ARTICLE_RANK_KEY, list.get(index), 1);
              }
              return R.success("訪問(wèn)ok!");
          }
      }
      
      // 1.排行榜
      @GetMapping("/rankingList")
      public R rankingList() {
          // 獲取前十
          Set<Object> range = zSetOperations.range(RedisKey.ARTICLE_RANK_KEY, 0, 9);
          if (range == null) {
              return R.fail("沒(méi)有數(shù)據(jù)!");
          }
          String str;
          for (Object o : range) {
              str = o.toString();
              String[] split = str.split(":");
              System.out.println("文章ID或者標(biāo)題:" + split[1]); // 這里可以根據(jù)id去數(shù)據(jù)庫(kù)查具體文章
          }
          return R.success().setData("rankingList", range);
      }
      

      優(yōu)化點(diǎn):如果文章過(guò)多的話,比如說(shuō)member達(dá)到了數(shù)以萬(wàn)計(jì)乃至十萬(wàn)計(jì),我們可以選擇僅保留 Top N 文章:定期清理非熱門文章(如只保留前 1000 名)

      ZREMRANGEBYRANK article_ranking 1000 -1 # 刪除1000名后的成員

      2.2 附近商家

      Redis實(shí)現(xiàn)附近的商家功能。這個(gè)問(wèn)題在LBS(基于位置的服務(wù))應(yīng)用中非常常見(jiàn),比如外賣、打車、社交等場(chǎng)景都需要查找附近的服務(wù)點(diǎn)。Redis的GEO模塊是解決這類問(wèn)題的核心工具,它在3.2版本后被引入。我們可以將商家的經(jīng)緯度實(shí)現(xiàn)導(dǎo)入Redis,然后就可以實(shí)現(xiàn)這個(gè)功能了。

      如下代碼:讀者們可以去這里轉(zhuǎn)換經(jīng)緯度:http://jingweidu.757dy.com/

      鄂州市經(jīng)緯度: 114.895003, 30.390893

      石家莊:114.515000,38.042401

      public class Shop {
          private String shopId;
          private String shopName;
          private Double longitude; // 經(jīng)度
          private Double latitude; // 緯度
          public static final List<Shop> shops = new ArrayList<>();
          static {
              // 模擬導(dǎo)入11個(gè)城市的經(jīng)緯度
              shops.add(new Shop("1", "上海鄉(xiāng)村基", 121.48, 31.22));
              shops.add(new Shop("2", "北京鄉(xiāng)村基", 116.46, 39.92));
              shops.add(new Shop("3", "廣州鄉(xiāng)村基", 113.27, 23.13));
              shops.add(new Shop("4", "深圳鄉(xiāng)村基", 114.07, 22.62));
              shops.add(new Shop("5", "杭州鄉(xiāng)村基", 120.19, 30.26));
              shops.add(new Shop("6", "南京鄉(xiāng)村基", 118.78, 32.04));
              shops.add(new Shop("7", "西安鄉(xiāng)村基", 108.95, 34.27));
              shops.add(new Shop("8", "武漢鄉(xiāng)村基", 114.31, 30.52));
              shops.add(new Shop("9", "長(zhǎng)沙鄉(xiāng)村基", 113.01, 28.21));
              shops.add(new Shop("10", "蘇州店肯德基", 120.62, 31.32));
              shops.add(new Shop("11", "天津店肯德基", 117.2, 39.13));
          }
      }
      
      @GetMapping("/importLAL")
      public R importLAL() {
          List<Shop> shops = Shop.shops;
          for (Shop shop : shops) {
              redisTemplate.opsForGeo().add(RedisKey.SHOP_KEY, new Point(shop.getLongitude(), shop.getLatitude()), shop.getShopName());
          }
          return R.success("導(dǎo)入成功!");
      }
      
      // 查詢附近
      @GetMapping("/nearbyShop")
      public R nearbyShop(@RequestParam("longitude") Double longitude, @RequestParam("latitude") Double latitude) {
          // =========1.獲取指定成員的地理位置
          //List<Point> position = redisTemplate.opsForGeo().position(RedisKey.SHOP_KEY, "上海鄉(xiāng)村基");
          // =========2.distance:計(jì)算兩個(gè)成員之間的距離(默認(rèn)以米為單位)
          //Distance distance = redisTemplate.opsForGeo().distance(RedisKey.SHOP_KEY, "上海鄉(xiāng)村基", "北京鄉(xiāng)村基");
          //double distanceInKm = distance.getValue() / 1000;
          // =========3.radiusByMember:根據(jù)給定的成員,返回與該成員距離在指定范圍內(nèi)的其他成員(按距離由近到遠(yuǎn)排序)
          // =========4.radius:根據(jù)給定的中心點(diǎn),返回與中心點(diǎn)距離在指定范圍內(nèi)的成員(按距離由近到遠(yuǎn)排序)
          // 構(gòu)建中心點(diǎn)
          Point center = new Point(longitude, latitude);
          // 構(gòu)建圓形搜索區(qū)域
          Circle circle = new Circle(center, new Distance(1000, Metrics.KILOMETERS));
          // 構(gòu)建搜索參數(shù)
          RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs
                  .newGeoRadiusArgs()
                  .includeDistance()  // 返回距離
                  .includeCoordinates()  // 返回坐標(biāo)
                  .sortAscending()  // 按距離升序
                  .limit(15);  // 限制數(shù)量
          // 執(zhí)行搜索
          GeoResults<RedisGeoCommands.GeoLocation<Object>> results = redisTemplate.opsForGeo().radius(RedisKey.SHOP_KEY, circle, args);
          if (results == null) return R.fail("沒(méi)有!");
          List<NearbyPlace> places = results.getContent().stream()
                  .map(result -> {
                      RedisGeoCommands.GeoLocation<Object> location = result.getContent();
                      Distance dist = result.getDistance();
                      Point pos = location.getPoint();
                      return new NearbyPlace(
                              location.getName().toString(),
                              pos.getX(), pos.getY(),
                              dist.getValue(),
                              dist.getUnit()
                      );
                  }).toList();
          // 可以在這里對(duì)結(jié)果處理一下,比如說(shuō)從數(shù)據(jù)庫(kù)查一下該店鋪的評(píng)分、評(píng)價(jià)之類的,等等
          return R.success().setData("places", places);
      }
      

      優(yōu)化點(diǎn):同理哦,如果商家巨多,都在同一個(gè)key里面可能會(huì)出現(xiàn)大key問(wèn)題,我們可以考慮給商家按照區(qū)域劃分到不同的key,這算是一種優(yōu)化方案。

      2.3 簽到

      Redis實(shí)現(xiàn)簽到功能的核心是使用位圖(BitMap)數(shù)據(jù)結(jié)構(gòu)。用戶簽到記錄可以按用戶ID+月份作為Key,日期作為偏移量,簽到狀態(tài)用0/1表示,這種設(shè)計(jì)非常節(jié)省空間,100萬(wàn)用戶一年的簽到數(shù)據(jù)才43MB左右。

      具體實(shí)現(xiàn)上,簽到操作使用SETBIT命令,查詢是否簽到用GETBIT,統(tǒng)計(jì)當(dāng)月簽到次數(shù)用BITCOUNT,獲取連續(xù)簽到天數(shù)則通過(guò)BITFIELD獲取位圖數(shù)據(jù)后做位運(yùn)算。

      具體代碼如下:

      // 模擬用戶簽到
      @PostMapping("/mockSign")
      public R signIn() {
          // 這里以六月舉例子,模擬6月1日-6月30日
          // 有十個(gè)用戶
          for (int i = 1; i <= 10; ++i ) {
              String key = RedisKey.SIGN_IN_KEY + ":" + i + ":202506";
              // 30天中隨機(jī)20天簽到
              for (int j = 0; j < 20; ++j) {
                  int day = (int) (Math.random() * 30) + 1;
                  redisTemplate.opsForValue().setBit(key, day, true);
              }
          }
          return R.success();
      }
      
      // 獲取用戶的簽到日歷
      @GetMapping("/signInCalendar")
      public R signInCalendar() {
          // 獲取當(dāng)前日期
          LocalDate now = LocalDate.now();
          // 當(dāng)前月有多少天
          int daysInMonth = now.lengthOfMonth();
          // 獲取用戶的簽到日歷
          List<SignCalendar> calendars = new ArrayList<>();
          for (int i = 1; i <= 10; ++i) { // 遍歷這十位用戶
              String key = RedisKey.SIGN_IN_KEY + ":" + i + ":202506"; // key
              SignCalendar calendar = new SignCalendar(i, new ArrayList<>());
              for (int j = 1; j <= daysInMonth; ++j) { // 判斷每一天
                  SignCalendarDay day = new SignCalendarDay(2025, now.getMonthValue(), j, false);
                  int offset = j - 1;
                  Boolean sign = redisTemplate.opsForValue().getBit(key, offset);
                  day.setSign(sign);
                  calendar.addDaySign(day);
              }
              calendars.add(calendar);
          }
          System.out.println(calendars);
          return R.success().setData("calendars", calendars);
      }
      

      具體詳情請(qǐng)看倉(cāng)庫(kù)項(xiàng)目。

      2.4 網(wǎng)站uv統(tǒng)計(jì)

      獨(dú)立用戶訪問(wèn)量Unique Visitor。

      Redis HyperLogLog 是用來(lái)做基數(shù)統(tǒng)計(jì)的算法,HyperLogLog 的優(yōu)點(diǎn)是,在輸入元素的數(shù)量或者體積非常非常大時(shí),計(jì)算基數(shù)所需的空間總是固定 的、并且是很小的。在 Redis 里面,每個(gè) HyperLogLog 鍵只需要花費(fèi) 12 KB 內(nèi)存,就可以計(jì)算接近 2^64 個(gè)不同元素的基 數(shù)。這和計(jì)算基數(shù)時(shí),元素越多耗費(fèi)內(nèi)存就越多的集合形成鮮明對(duì)比。

      示例請(qǐng)看如下代碼:

      @GetMapping("/mockAccess")
      public R webUv() {
          String[] keys = buildKey();
          // 記錄訪問(wèn)-- 模擬1000個(gè)用戶訪問(wèn)
          List<Integer> uids = new ArrayList<>();
          for (int i = 0; i < 1000000; ++i) uids.add( i);
          // uids.forEach(uid -> {
          //     redisTemplate.opsForHyperLogLog().add(keys[0], uid);
          //     redisTemplate.opsForHyperLogLog().add(keys[1], uid);
          //     redisTemplate.opsForHyperLogLog().add(keys[2], uid);
          // });
          redisTemplate.opsForHyperLogLog().add(keys[0],  uids.toArray());
          redisTemplate.opsForHyperLogLog().add(keys[1],  uids.toArray());
          redisTemplate.opsForHyperLogLog().add(keys[2],  uids.toArray());
          return R.success();
      }
      
      @GetMapping("/webUv")
      public R webUv() {
          String[] keys = buildKey();
          Long day = redisTemplate.opsForHyperLogLog().size(keys[0]);
          Long month = redisTemplate.opsForHyperLogLog().size(keys[1]);
          Long year = redisTemplate.opsForHyperLogLog().size(keys[2]);
          return R.success().setData("day", day)
              .setData("month", month)
              .setData("year", year);
      }
      

      我這里訪問(wèn)量測(cè)試了

      {
          "code": 200,
          "data": {
              "msg": "響應(yīng)成功!",
              "month": 1009972,
              "year": 1009972,
              "day": 1009972
          }
      }
      

      占用內(nèi)存連0.5MB都不到. 雖然訪問(wèn)量可能不是精準(zhǔn)的,但是大差不差了。實(shí)際情況地話,這些key肯定是要定期清零的,比如說(shuō),月訪問(wèn)量,一個(gè)月過(guò)去了,肯定是需要重新統(tǒng)計(jì)的。

      2.5 社交關(guān)系

      利用Redis的集合(Set)數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)關(guān)注關(guān)系,每個(gè)用戶用兩個(gè)集合分別存儲(chǔ)關(guān)注列表(following)和粉絲列表(followers),當(dāng)用戶A關(guān)注用戶B時(shí),系統(tǒng)會(huì)執(zhí)行兩條原子操作——把A加入B的粉絲集合,同時(shí)把B加入A的關(guān)注集合。

      比如要查A和B的共同關(guān)注,只需對(duì)A的關(guān)注集合和B的關(guān)注集合取交集(SINTER命令),Redis集合運(yùn)算的時(shí)間復(fù)雜度是O(N),百萬(wàn)級(jí)關(guān)系數(shù)據(jù)也能毫秒級(jí)響應(yīng),這種性能對(duì)社交場(chǎng)景至關(guān)重要。

      但社交關(guān)系不只是關(guān)注功能,搜索結(jié)果還展示了更豐富的可能性:比如用有序集合(Sorted Set)存儲(chǔ)關(guān)注時(shí)間,可以實(shí)現(xiàn)"最近關(guān)注"排序;用發(fā)布訂閱(Pub/Sub)實(shí)現(xiàn)實(shí)時(shí)動(dòng)態(tài)推送——當(dāng)用戶發(fā)帖時(shí),系統(tǒng)會(huì)向所有粉絲的推送頻道發(fā)布消息等等。

      同時(shí)需要注意的是,如果網(wǎng)站中有大V,比如說(shuō)有十萬(wàn)百萬(wàn)粉絲,同理又會(huì)出現(xiàn)大key問(wèn)題,這個(gè)時(shí)候就要注意了,可以用粉絲ID的哈希值分桶,比如把1億粉絲分散到100個(gè)分片集合中。

      本文僅給出簡(jiǎn)單例子:

      // 舉例子
      # 用戶123 關(guān)注用戶456
      SADD following:123 456  # 123的關(guān)注列表加入456
      SADD followers:456 123  # 456的粉絲列表加入123
      

      具體情況見(jiàn)代碼。

      2.6 限流計(jì)數(shù)

      見(jiàn)這篇文章:限流算法

      地址:https://blog.csdn.net/okok__TXF/article/details/148048960

      end. 參考

      1. https://mp.weixin.qq.com/s/d_JJuTGiN6Lspn0oyYxKFA 【阿里面試:緩存擊穿、緩存穿透、緩存雪崩 3大問(wèn)題,如何徹底解決?】
      2. https://mp.weixin.qq.com/s/Bt8yVVZ-F4H7KzJnZlwJMQ 【hotKey】
      3. https://mp.weixin.qq.com/s/PmJ8nMFbxJ1qI3hGt8s9lw 【布隆過(guò)濾器】
      4. https://mp.weixin.qq.com/s/HSQVnPW4aRdLP2jnTOFr4A 【分布式鎖】
      5. https://www.runoob.com/redis/redis-hyperloglog.html 【菜鳥教程】
      posted @ 2025-06-16 21:35  別來(lái)無(wú)恙?  閱讀(246)  評(píng)論(0)    收藏  舉報(bào)
      主站蜘蛛池模板: 成人特黄A级毛片免费视频| 国产亚洲精品97在线视频一| 国产午夜福利精品视频| 亚洲精品天堂成人片AV在线播放| 国产精品久久久久7777| 亚洲欧美不卡视频在线播放| 一本精品99久久精品77| 加勒比无码人妻东京热| 18禁亚洲一区二区三区| 亚洲国产精品黄在线观看| 久久精品免费无码区| 99久久精品费精品国产一区二 | 久久亚洲国产品一区二区| 韩国深夜福利视频在线观看| 免费无码观看的AV在线播放| 国产午夜福利在线视频| аⅴ天堂中文在线网| 久9视频这里只有精品试看| 广元市| 四虎成人在线观看免费| jk白丝喷浆| 久久人人97超碰精品| 中文字幕无线码中文字幕| 九九热精品在线观看| 亚洲av成人网在线观看| 美女禁区a级全片免费观看| 欧美人伦禁忌dvd放荡欲情 | 国产毛片子一区二区三区| 熟妇人妻久久精品一区二区| 亚洲日本国产精品一区| 欧美丰满熟妇性xxxx| 国产精品久久国产精麻豆99网站| 亚洲成av人片天堂网老年人| 伊伊人成亚洲综合人网香| 久久亚洲精品情侣| 日韩av中文字幕有码| 秋霞人妻无码中文字幕| 在线观看成人永久免费网站 | 亚洲免费网站观看视频| 精品国产熟女一区二区三区| 在线播放亚洲成人av|