Redis詳解

2022-10-03 06:00:12

Redis介紹

    1.Redis 是一個基於記憶體的高效能 key-value 資料庫。是完全開源免費的,用C語言編寫的,遵守BSD協定

    2.Redis 特點:

      1)Redis 是基於記憶體操作的,吞吐量非常高,可以在 1s內完成十萬次讀寫操作
      2)Redis 的讀寫模組是單執行緒,每個操作都具原子性
      3)Redis 支援資料的持久化,可以將記憶體中的資料儲存在磁碟中,重啟可以再次載入,但可能會有極短時間內資料丟失
      4)Redis 支援多種資料結構,String,list,set,zset,hash等

 

針對五種資料結構的介紹【針對場景部分,有些並不適用,但是用於拓展視野

    1.字串String(用的多)

      1)常用操作

        【1】單值操作

SET key value //存入字串鍵值對
GET key //獲取一個字串鍵值
SETNX key value //存入一個不存在的字串鍵值對,有點類似原子操作,如果沒有才給存入,有則失敗
DEL key [key ...] //刪除一個鍵
EXPIRE key seconds //設定一個鍵的過期時間(秒)

        【2】批次操作

MSET key value [key value ...] //批次儲存字串鍵值對
MGET key [key ...] //批次獲取字串鍵值

        【3】原子操作

INCR key //將key中儲存的數位值加1
DECR key //將key中儲存的數位值減1
INCRBY key increment //將key所儲存的值加上increment
DECRBY key decrement //將key所儲存的值減去decrement

 

 

      2)應用場景

        【1】單值快取

SET  key  value     
GET  key    

        【2】物件快取

//這兩種情況要區分,你對這個物件的操作是整體多還是屬性值多,因為java中使用的話物件的資料型別需要序列化【儲存和取出都要】
//而且分開存的話有助於在不同地方使用不同屬性值,但是卻要取出整個物件的局面。(雖然分開存消耗記憶體更多,但是從傳輸角度來想,有可能消耗更小,但都是分場景的)
1)  SET  user:1  value(json格式資料)
2)  MSET  user:1:name  zhuge   user:1:balance  1888  //針對物件的值分開儲存的批次操作
    MGET  user:1:name   user:1:balance 

        【3】分散式鎖實現(下面僅僅是範例,這個實現其實要考慮的問題很多,可檢視 Redis高並行分散式鎖詳解

SETNX  product:10001  true      //返回1代表獲取鎖成功
SETNX  product:10001  true      //返回0代表獲取鎖失敗
。。。執行業務操作
DEL  product:10001          //執行完業務釋放鎖

SET product:10001 true  ex  10  nx  //防止程式意外終止導致死鎖

        【4】計數器

INCR article:readcount:{文章id}   
GET article:readcount:{文章id} 

        【5】實現分散式session共用(可檢視 分散式Session的實現詳解 )

spring session + redis實現session共用

        【6】分散式全域性ID 

INCRBY  orderId  1000       //redis批次生成序列號提升效能

 

    2.雜湊hash

      1)常用操作

        【1】單值操作

HSET  key  field  value             //儲存一個雜湊表key的鍵值
HGET  key  field                //獲取雜湊表key對應的field鍵值
HSETNX  key  field  value       //儲存一個不存在的雜湊表key的鍵值

        【2】批次操作

HMSET  key  field  value [field value ...]  //在一個雜湊表key中儲存多個鍵值對
HMGET  key  field  [field ...]      //批次獲取雜湊表key中多個field鍵值
HDEL  key  field  [field ...]       //刪除雜湊表key中的field鍵值
HLEN  key               //返回雜湊表key中field的數量
HGETALL  key                //返回雜湊表key中所有的鍵值

        【3】原子操作

HINCRBY  key  field  increment      //為雜湊表key中field鍵的值加上增量increment

 

      2)應用場景

        【1】物件快取(由於redis設定過期時間只針對頂級key型別,而不支援對hash型別內部,故塞得多了容易造成bigKey問題

HMSET  user  {userId}:name  zhuge  {userId}:balance  1888
HMSET  user  1:name  zhuge  1:balance  1888
HMGET  user  1:name  1:balance  

        【2】電商購物車

1)以使用者id為key   //以使用者ID為key,避免多個使用者儲存在一個hash裡面(避免bigKey)//針對未登入的可以構建虛擬ID,對登入時的資料進行合併
2)商品id為field
3)商品數量為value
4)可以針對key設定過期時間  //設定過期時間可以在不用的時候,redis自己回收

購物車操作
hset cart:1001 10088 1  //新增商品
hincrby cart:1001 10088 1  //增加數量
hlen cart:1001  //商品總數 
hdel cart:1001 10088  //刪除商品
hgetall cart:1001  //獲取購物車所有商品

 

      3)優缺點

優點
    1)同類資料歸類整合儲存,方便資料管理
    2)相比string操作消耗記憶體與cpu更小
    3)相比string儲存更節省空間

缺點
    1)過期功能不能使用在field上,只能用在key上。//(這也是容易造成bigKey問題的本質,設定過期時間是為了讓redis自己去回收,設定不了就只能靠自己去回收,不回收容易造成記憶體擠爆,也容易出現阻塞請求的情況)
    2)Redis叢集架構下不適合大規模使用。//(因為構建叢集的本質是平攤請求和資料,提高處理量和扛並行,如果hash的值會被儲存在某個節點中,如果值很大,那麼容易出現請求傾斜,那麼這個結點容易被打掛)

 

    3.列表list(用的多)

      1)常用操作

LPUSH  key  value [value ...]       //將一個或多個值value插入到key列表的表頭(最左邊)
RPUSH  key  value [value ...]       //將一個或多個值value插入到key列表的表尾(最右邊)
LPOP  key           //移除並返回key列表的頭元素
RPOP  key           //移除並返回key列表的尾元素
LRANGE  key  start  stop        //返回列表key中指定區間內的元素,區間以偏移量start和stop指定
BLPOP  key  [key ...]  timeout  //從key列表表頭彈出一個元素,若列表中沒有元素,阻塞等待                  timeout秒,如果timeout=0,一直阻塞等待
BRPOP  key  [key ...]  timeout  //從key列表表尾彈出一個元素,若列表中沒有元素,阻塞等待                  timeout秒,如果timeout=0,一直阻塞等待

 

      2)應用場景

        【1】常用資料結構(應對分散式下某些資料結構的要求)

Stack(棧) = LPUSH + LPOP
Queue(佇列)= LPUSH + RPOP
Blocking MQ(阻塞佇列)= LPUSH + BRPOP

        【2】微博訊息和微信公號訊息(這種更多體現在即時通訊軟體上面)

你關注了A,B等大V
1)A發微博,訊息ID為10018
LPUSH  msg:{你-ID}  10018
2)B發微博,訊息ID為10086
LPUSH  msg:{你-ID} 10086
3)檢視最新微博訊息
LRANGE  msg:{你-ID}  0  4

 

    4.集合set(比較重要)

      1)常用操作

SADD  key  member  [member ...]         //往集合key中存入元素,元素存在則忽略,若key不存在則新建
SREM  key  member  [member ...]         //從集合key中刪除元素
SMEMBERS  key                   //獲取集合key中所有元素
SCARD  key                  //獲取集合key的元素個數
SISMEMBER  key  member          //判斷member元素是否存在於集合key中
SRANDMEMBER  key  [count]           //從集合key中隨機選出count個元素,元素不從key中刪除
SPOP  key  [count]              //從集合key中隨機選出count個元素,元素從key中刪除

      2)運算操作

SINTER  key  [key ...]              //交集運算,多個集合共有的元素的集合
SINTERSTORE  destination  key  [key ..]     //將交集結果存入新集合destination中
SUNION  key  [key ..]               //並集運算,將元素彙總成一個集合
SUNIONSTORE  destination  key  [key ...]        //將並集結果存入新集合destination中
SDIFF  key  [key ...]               //差集運算,相當於第一個集合減去後面多個集合的並集
SDIFFSTORE  destination  key  [key ...]     //將差集結果存入新集合destination中

      3)應用場景

        【1】微信抽獎小程式

1)點選參與抽獎加入集合
SADD key {userlD}
2)檢視參與抽獎所有使用者
SMEMBERS key      
3)抽取count名中獎者
SRANDMEMBER key [count] / SPOP key [count]

        【2】微信微博點贊,收藏,標籤

1) 點贊
SADD  like:{訊息ID}  {使用者ID}
2) 取消點贊
SREM like:{訊息ID}  {使用者ID}
3) 檢查使用者是否點過贊
SISMEMBER  like:{訊息ID}  {使用者ID}
4) 獲取點讚的使用者列表
SMEMBERS like:{訊息ID}
5) 獲取點贊使用者數 
SCARD like:{訊息ID}

        【3】集合操作實現微博微信關注模型

1) A關注的人: 
ASet-> {C, D}
2) B關注的人:
BSet--> {A, E, C, D}
3) C關注的人: 
CSet-> {A, B, E, D, F)

//重點通過A與關注他的B【但A沒有關注B】
4) A,B共同關注: 
SINTER ASet BSet--> {C, D}
5) A關注的人也關注他(B): 
SISMEMBER CSet B 
SISMEMBER DSet B
6) A可能認識的人: 
SDIFF BSet ASet->{A, E}

        【4】集合操作實現電商商品篩選

SADD  brand:huawei  P40
SADD  brand:xiaomi  mi-10
SADD  brand:iPhone iphone12
SADD os:android  P40  mi-10
SADD cpu:brand:intel  P40  mi-10
SADD ram:8G  P40  mi-10  iphone12

SINTER  os:android  cpu:brand:intel  ram:8G   {P40,mi-10}

 

    5.有序集合zset

      1)常用操作

ZADD key score member [[score member]…] //往有序集合key中加入帶分值元素
ZREM key member [member …]      //從有序集合key中刪除元素
ZSCORE key member           //返回有序集合key中元素member的分值
ZINCRBY key increment member        //為有序集合key中元素member的分值加上increment 
ZCARD key               //返回有序集合key中元素個數
ZRANGE key start stop [WITHSCORES]  //正序獲取有序集合key從start下標到stop下標的元素,WITHSCORES引數的作用:就是查詢結果帶上分數
ZREVRANGE key start stop [WITHSCORES]   //倒序獲取有序集合key從start下標到stop下標的元素 

      2)集合操作

ZUNIONSTORE destkey numkeys key [key ...]   //並集計算
ZINTERSTORE destkey numkeys key [key ...]   //交集計算

      3)應用場景

        【1】Zset集合操作實現排行榜

1)點選新聞
ZINCRBY  hotNews:20190819  1  守護香港ID  //針對單條資料,集合名,瀏覽次數,文章ID
2)展示當日排行前十
ZREVRANGE  hotNews:20190819  0  9  WITHSCORES 
3)七日搜尋榜單計算
ZUNIONSTORE  hotNews:20190813-20190819  7  hotNews:20190813  hotNews:20190814... hotNews:20190819
4)展示七日排行前十
ZREVRANGE hotNews:20190813-20190819  0  9  WITHSCORES

 

針對Redis的探索

  1)Redis是單執行緒嗎?

    1.Redis並不是真正意義上的單執行緒,Redis的單執行緒主要是指Redis的網路IO和鍵值對讀寫是由一個執行緒來完成的,這也是 Redis 對外提供鍵值儲存服務的主要流程。

    2.但 Redis 的其他功能,比如持久化、非同步刪除、叢集資料同步等,其實是由額外的執行緒執行的。

 

  2)Redis 單執行緒為什麼還能這麼快?

    1.因為它所有的資料都在記憶體中所有的運算都是記憶體級別的運算,而且單執行緒避免了多執行緒的切換效能損耗問題。正因為 Redis 是單執行緒,所以要小心使用 Redis 指令,對於那些耗時的指令(比如keys),一定要謹慎使用,一不小心就可能會導致 Redis 卡頓

    2.展示

      【1】keys:全量遍歷鍵,用來列出所有滿足特定正則字串規則的key,當redis資料量比較大時,效能比較差,要避免使用

keys  *   //展示全部key值
keys  ab*c  //展示全部符合正則匹配的key值

      【2】scan:漸進式遍歷鍵。(常用這種替代keys指令

SCAN cursor [MATCH pattern] [COUNT count]
範例:SCAN 0  MATCH  test*key  COUNT  100

        說明:scan 引數提供了三個引數,第一個是 cursor 整數值(hash桶的索引值),第二個是 key 的正則模式,第三個是一次遍歷的key的數量(參考值,底層遍歷的數量不一定),並不是符合條件的結果數量。第一次遍歷時,cursor 值為 0,然後將返回結果中第一個整數值作為下一次遍歷的 cursor。一直遍歷到返回的 cursor 值為 0 時結束。

        注意:但是scan並非完美無瑕, 如果在scan的過程中如果有鍵的變化(增加、 刪除、 修改) ,那麼遍歷效果可能會碰到如下問題: 新增的鍵可能沒有遍歷到, 遍歷出了重複的鍵等情況, 也就是說scan並不能保證完整的遍歷出來所有的鍵, 這些是我們在開發時需要考慮的

        範例展示:

             

        範例分析:   

          這個和底層實現有關,Redis的底層相當於一個HashMap(將資料雜湊儲存到key中儲存一樣),scan 每次去遍歷的時候會去遍歷其中儲存資料的一個key值,一次最多拿三個,並且返回下次的遊標,便於下次獲取。

          如果一共有36個資料分別散落於4個key為key1,key2,key3,key4中,其中key1有8個資料,key2有7個,key3有10個,key4有11個。那麼先去掃描key1的槽,

第三次拿只會拿到2個資料,然後key1槽掃描完就不會再掃描了,而會去掃描key2的槽。按此邏輯走完全部的槽。這也是為什麼我們需要在開發的時候注意的。

 

  3)Redis 單執行緒如何處理那麼多的並行使用者端連線?

    1.Redis的IO多路複用:redis利用epoll來實現IO多路複用,將連線資訊和事件放到佇列中,依次放到檔案事件分派器,事件分派器將事件分發給事件處理器。

    2.連線數是存在限制的:

# 檢視redis支援的最大連線數,在redis.conf檔案中可修改,# maxclients 10000
127.0.0.1:6379> CONFIG GET maxclients
    ##1) "maxclients"
    ##2) "10000"

 

  4)Redis 真的可以在 1s內完成十萬次讀寫操作嗎?

    其實不是的,這個與當前的伺服器有關,大多數應該是幾萬。Redis 自帶了一個叫 redis-benchmark 的工具(放在Redis的src目錄下)來模擬 N 個使用者端同時發出 M 個請求:

[root@node1 bin]# redis-benchmark --help
Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]

 -h <hostname>          指定伺服器主機名 (預設 127.0.0.1)
 -p <port>              指定伺服器埠 (預設 6379)
 -s <socket>            指定伺服器 socket
 -a <password>          指定redis密碼
 -c <clients>           指定並行連線數 (預設 50)
 -n <requests>          指定請求數 (預設 100000)
 -d <size>              SET/GET 命令的值bytes單位 預設是2
 --dbnum <db>           指定redis的某個資料庫,預設是0資料庫
 -k <boolean>           指定是否保持連線 1是保持連線 0是重新連線,預設為 1
 -r <keyspacelen>       指定get/set的隨機值的範圍。
 -P <numreq>            管道請求測試,預設0沒有管道測試
 -e                     如果有錯誤,輸出到標準輸出上。
 -q                     靜默模式,只顯示query/秒的值
 --csv                  指定輸出結果到csv檔案中
 -l                     生成迴圈,永久執行測試
 -t <tests>             僅執行以逗號分隔的測試命令列表

 

理解Redis對Lua指令碼的操作

  1.介紹

    1)Redis在2.6推出了指令碼功能,允許開發者使用Lua語言編寫指令碼傳到Redis中執行。使用指令碼的好處如下:

      【1】減少網路開銷:本來5次網路請求的操作,可以用一個請求完成,原先5次請求的邏輯放在redis伺服器上完成。使用指令碼,減少了網路往返時延。與管道類似。

      【2】原子操作:Redis會將整個指令碼作為一個整體執行,中間不會被其他命令插入。管道不是原子的,不過redis的批次操作命令(類似mset)是原子的

      【3】替代redis的事務功能:redis自帶的事務功能很雞肋,而redis的lua指令碼幾乎實現了常規的事務功能,官方推薦如果要使用redis的事務功能可以用redis lua替代。

  2.簡單使用

    1)從Redis2.6.0版本開始,通過內建的Lua直譯器,可以使用EVAL命令對Lua指令碼進行求值。EVAL命令的格式如下:

      範例程式碼

格式:EVAL script numkeys key [key ...] arg [arg ...]
範例:eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

      範例結果【展示瞭如何進行傳參】

           

      範例說明

        【1】script引數是一段Lua指令碼程式,它會被執行在Redis伺服器上下文中,這段指令碼不必定義為一個Lua函數。numkeys引數用於指定鍵名引數的個數。

        【2】鍵名引數 key [key ...] 從EVAL的第三個引數開始算起,表示在指令碼中所用到的那些Redis鍵(key),這些鍵名引數可以在 Lua中通過全域性變數KEYS陣列,用1為基址的形式存取( KEYS[1] , KEYS[2] ,以此類推)。

        【3】在命令的最後,那些不是鍵名引數的附加引數 arg [arg ...] ,可以在Lua中通過全域性變數ARGV陣列存取,存取的形式和KEYS變數類似( ARGV[1] 、 ARGV[2] ,諸如此類)。

        【4】其中 "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是被求值的Lua指令碼,數位2指定了鍵名引數的數量, key1和key2是鍵名引數,分別使用 KEYS[1] 和 KEYS[2] 存取,而最後的 first 和 second 則是附加引數,可以通過 ARGV[1] 和 ARGV[2] 存取它們。

 

    2)在 Lua 指令碼中,可以使用redis.call()函數來執行Redis命令(使用Jedis呼叫):

jedis.set("product_stock_10016", "15");  //初始化商品10016的庫存
String script = " local count = redis.call('get', KEYS[1]) " +
                " local a = tonumber(count) " +
                " local b = tonumber(ARGV[1]) " +
                " if a >= b then " +
                "   redis.call('set', KEYS[1], a-b) " +
                "   return 1 " +
                " end " +
                " return 0 ";
Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));
System.out.println(obj);

 

    注意:不要在Lua指令碼中出現死迴圈和耗時的運算,否則redis會阻塞,將不接受其他的命令, 所以使用時要注意不能出現死迴圈、耗時的運算。redis是單程序、單執行緒執行指令碼。管道不會阻塞redis。  

 

Redis快取設計中存在的問題

  1.快取穿透

    1)說明:

      【1】快取穿透是指查詢一個根本不存在的資料, 快取層和儲存層都不會命中, 通常出於容錯的考慮, 如果從儲存層查不到資料則不寫入快取層。

      【2】快取穿透將導致不存在的資料每次請求都要到儲存層去查詢, 失去了快取保護後端儲存的意義。

      【3】造成快取穿透的基本原因有兩個:

          第一, 自身業務程式碼或者資料出現問題。

          第二, 一些惡意攻擊、 爬蟲等造成大量空命中。

    2)處理:

      【1】快取空物件

//主體邏輯
product = productDao.get(productId);
if (product != null) {
    redisUtil.set(productCacheKey, JSON.toJSONString(product),genProductCacheTimeout(), TimeUnit.SECONDS);
} else {
    redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
}

//空快取的過期時間獲取,這個時間不宜過大,一分鐘左右即可,時間過大容易快取大量空資料,消耗Redis儲存資源。
//時間過小容易很快過期,但是我們可以進行讀延期,你一直查,我們一直延期,你不查了,過一段時間就會失效
private Integer genEmptyCacheTimeout() {
    return 60 + new Random().nextInt(30);
}

 

      【2】布隆過濾器

          1.布隆過濾器介紹

            1)對於惡意攻擊,向伺服器請求大量不存在的資料造成的快取穿透,還可以用布隆過濾器先做一次過濾,對於不存在的資料布隆過濾器一般都能夠過濾掉,不讓請求再往後端傳送。當布隆過濾器說某個值存在時,這個值可能不存在;當它說不存在時,那就肯定不存在。

            2)布隆過濾器就是一個大型的位陣列和幾個不一樣的無偏 hash 函數。所謂無偏就是能夠把元素的 hash 值算得比較均勻。

            3)向布隆過濾器中新增 key 時,會使用多個 hash 函數對 key 進行 hash 算得一個整數索引值然後對位陣列長度進行取模運算得到一個位置,每個 hash 函數都會算得一個不同的位置。再把位陣列的這幾個位置都置為 1 就完成了 add 操作。

            4)向布隆過濾器詢問 key 是否存在時,跟 add 一樣,也會把 hash 的幾個位置都算出來,看看位陣列中這幾個位置是否都為 1,只要有一個位為 0,那麼說明布隆過濾器中這個key 不存在。如果都是 1,這並不能說明這個 key 就一定存在,只是極有可能存在,因為這些位被置為 1 可能是因為其它的 key 存在所致。如果這個位陣列長度比較大,存在概率就會很大,如果這個位陣列長度比較小,存在概率就會降低。

            5)這種方法適用於資料命中不高、 資料相對固定、 實時性低(通常是資料集較大) 的應用場景, 程式碼維護較為複雜, 但是快取空間佔用很少。

          2.圖示

               

 

          3.用redisson實現布隆過濾器

            1)引入依賴:

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.6.5</version>
</dependency>

            2)範例虛擬碼:

public class RedissonBloomFilter {

    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        //構造Redisson
        RedissonClient redisson = Redisson.create(config);

        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
        //初始化布隆過濾器:預計元素為100000000L,誤差率為3%,根據這兩個引數會計算出底層的bit陣列大小
        bloomFilter.tryInit(100000000L,0.03);
        //將zhuge插入到布隆過濾器中
        bloomFilter.add("abc");

        //判斷下面號碼是否在布隆過濾器中
        System.out.println(bloomFilter.contains("bcd"));//false
        System.out.println(bloomFilter.contains("cde"));//false
        System.out.println(bloomFilter.contains("abc"));//true
    }
}

            3)使用布隆過濾器需要把所有資料提前放入布隆過濾器,並且在增加資料時也要往布隆過濾器裡放,布隆過濾器快取過濾虛擬碼:

//初始化布隆過濾器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆過濾器:預計元素為100000000L,誤差率為3%
bloomFilter.tryInit(100000000L,0.03);
        
//把所有資料存入布隆過濾器
void init(){
    for (String key: keys) {
        bloomFilter.put(key);
    }
}

String get(String key) {
    // 從布隆過濾器這一級快取判斷下key是否存在
    Boolean exist = bloomFilter.contains(key);
    if(!exist){
        return "";
    }
    // 從快取中獲取資料
    String cacheValue = cache.get(key);
    // 快取為空
    if (StringUtils.isBlank(cacheValue)) {
        // 從儲存中獲取
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        // 如果儲存資料為空, 需要設定一個過期時間(300秒)
        if (storageValue == null) {
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    } else {
        // 快取非空
        return cacheValue;
    }
}

 

          3.注意:布隆過濾器不能刪除資料,如果要刪除得重新初始化資料。

 

  2.快取失效(擊穿)

    1)說明:由於大批次快取在同一時間失效可能導致大量請求同時穿透快取直達資料庫,可能會造成資料庫瞬間壓力過大甚至掛掉,對於這種情況我們在批次增加快取時最好將這一批資料的快取過期時間設定為一個時間段內的不同時間。造成的原因是:我們為了便捷,提供了批次生產與批次修改操作,那麼容易出現設定的過期時間一直問題。

    2)處理:

      【1】同一獲取過期時間的入口,針對獲取過期時間採取新增隨機時間錯開時間段。

public static final Integer PRODUCT_CACHE_TIMEOUT = 60 * 60 * 24; //設定為1天或者更少,這種一般要考慮凌晨時期的空窗期(沒人使用)
private Integer genProductCacheTimeout() {
    //這對過期時間新增隨機時間
    return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60; 
}

 

      【2】對於獲取資料步驟,要對快取增加讀延期

public static final String EMPTY_CACHE = "{}";
private Product getProductFromCache(String productCacheKey) {

    String productStr = redisUtil.get(productCacheKey);
    if (!StringUtils.isEmpty(productStr)) {
        if (EMPTY_CACHE.equals(productStr)) {
            redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);  //讀延期,延長過期時間
            return null;
        }
        product = JSON.parseObject(productStr, Product.class);
        redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); //讀延期,延長過期時間
    }
    return product;
}

 

  3.快取雪崩

    1)介紹:

      【1】快取雪崩指的是快取層支撐不住或宕掉後, 流量直接打向後端儲存層。

      【2】由於快取層承載著大量請求, 有效地保護了儲存層, 但是如果快取層由於某些原因不能提供服務(比如超大並行過來,快取層支撐不住,或者由於快取設計不好,類似大量請求存取bigkey,導致快取能支撐的並行急劇下降), 於是大量請求都會打到儲存層, 儲存層的呼叫量會暴增, 造成儲存層也會級聯宕機的情況。

      【3】總的來說,就是Redis設定中存在 maxclients  10000 屬性值限制(設定能連上redis的最大使用者端連線數量。預設是10000個使用者端連線。由於redis不區分連線是使用者端連線還是內部開啟檔案或者和slave連線等,所以maxclients最小建議設定到32。如果超過了maxclients,redis會給新的連線傳送’max number of clients reached’,並關閉連線。)

      

    2)處理:

      【1】保證快取層服務高可用性,比如使用Redis Sentinel或Redis Cluster。(即使用叢集增加雪崩的上限,增加雪崩的難度

      【2】依賴隔離元件為後端限流熔斷並降級。比如使用Sentinel或Hystrix限流降級元件最好結合 redis-benchmark 的工具,壓測部署在伺服器上叢集能抗住多少並行)。比如服務降級,我們可以針對不同的資料採取不同的處理方式。當業務應用存取的是非核心資料(例如電商商品屬性,使用者資訊等)時,暫時停止從快取中查詢這些資料,而是直接返回預定義的預設降級資訊、空值或是錯誤提示資訊;當業務應用存取的是核心資料(例如電商商品庫存)時,仍然允許查詢快取,如果快取缺失,也可以繼續通過資料庫讀取。

      【3】提前演練。 在專案上線前, 演練快取層宕掉後, 應用以及後端的負載情況以及可能出現的問題, 在此基礎上做一些預案設定。

 

  4.熱點快取key重建優化

    1)介紹:

      【1】使用「快取+過期時間」的策略既可以加速資料讀寫, 又保證資料的定期更新, 這種模式基本能夠滿足絕大部分需求。 但是有兩個問題如果同時出現, 可能就會對應用造成致命的危害:

        1.當前key是一個熱點key(例如一個熱門的娛樂新聞),並行量非常大。

        2.重建快取不能在短時間完成, 可能是一個複雜計算, 例如複雜的SQL、 多次IO、 多個依賴等。

      【2】在快取失效的瞬間, 有大量執行緒來重建快取, 造成後端負載加大, 甚至可能會讓應用崩潰。

 

    2)處理(主要就是要避免大量執行緒同時重建快取):

      【1】利用互斥鎖來解決,此方法只允許一個執行緒重建快取, 其他執行緒等待重建快取的執行緒執行完, 重新從快取獲取資料即可。

      【2】採用DCL【雙重檢查鎖(double-checked locking)】,可以在完成重建後加快返回速度。

      【3】程式碼展示:

product = getProductFromCache(productCacheKey);
if (product != null) {
    return product;
}
//DCL
RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
hotCacheLock.lock();
try {
    product = getProductFromCache(productCacheKey);
    if (product != null) {
        return product;
    }

    product = productDao.get(productId);
    if (product != null) {
        redisUtil.set(productCacheKey, JSON.toJSONString(product),genProductCacheTimeout(), TimeUnit.SECONDS);
    } else {
        redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
    }

} finally {
    hotCacheLock.unlock();
}
return product;

 

  5.快取與資料庫雙寫不一致 

    1)介紹:在大並行下,同時運算元據庫與快取會存在資料不一致性問題【這種採用雙刪是解決不了的

      【1】雙寫不一致情況

               

 

      【2】讀寫並行不一致

             

 

    2)處理:

      【1】對於並行機率很小的資料(如個人維度的訂單資料、使用者資料等),這種幾乎不用考慮這個問題,很少會發生快取不一致,可以給快取資料加上過期時間,每隔一段時間觸發讀的主動更新即可。

      【2】就算並行很高,如果業務上能容忍短時間的快取資料不一致(如商品名稱,商品分類選單等),快取加上過期時間依然可以解決大部分業務對於快取的要求。

      【3】如果不能容忍快取資料不一致,可以通過加分散式讀寫鎖保證並行讀寫或寫寫的時候按順序排好隊,讀讀的時候相當於無鎖

      【4】也可以用阿里開源的canal通過監聽資料庫的binlog紀錄檔及時的去修改快取,但是引入了新的中介軟體,增加了系統的複雜度。

  6.總結

    【1】以上都是針對讀多寫少的情況加入快取提高效能,如果寫多讀多的情況又不能容忍快取資料不一致,那就沒必要加快取了,可以直接運算元據庫。

    【2】如果資料庫抗不住壓力,還可以把快取作為資料讀寫的主記憶體儲,非同步將資料同步到資料庫,資料庫只是作為資料的備份。

    【3】放入快取的資料應該是對實時性、一致性要求不是很高的資料。

    【4】不要為了用快取,同時又要保證絕對的一致性做大量的過度設計和控制,增加系統複雜性!(如延遲雙刪加上休眠時間,這就很不可取)

 

檢視redis服務執行資訊

   1.Info:檢視redis服務執行資訊,分為 9 大塊,每個塊都有非常多的引數,這 9 個塊分別是: 

    1.Server 伺服器執行的環境引數 

    2.Clients 使用者端相關資訊 

    3.Memory 伺服器執行記憶體統計資料 

    4.Persistence 持久化資訊 

    5.Stats 通用統計資料 

    6.Replication 主從複製相關資訊 

    7.CPU CPU 使用情況 

    8.Cluster 叢集資訊 

    9.KeySpace 鍵值對統計數量資訊

  2.範例

          

  3.核心引數說明

connected_clients:2                  # 正在連線的使用者端數量

instantaneous_ops_per_sec:789        # 每秒執行多少次指令

used_memory:929864                   # Redis分配的記憶體總量(byte),包含redis程序內部的開銷和資料佔用的記憶體
used_memory_human:908.07K            # Redis分配的記憶體總量(Kb,human會展示出單位)
used_memory_rss_human:2.28M          # 向作業系統申請的記憶體大小(Mb)(這個值一般是大於used_memory的,因為Redis的記憶體分配策略會產生記憶體碎片)
used_memory_peak:929864              # redis的記憶體消耗峰值(byte)
used_memory_peak_human:908.07K       # redis的記憶體消耗峰值(KB)

maxmemory:0                         # 設定中設定的最大可使用記憶體值(byte),預設0,不限制,一般設定為機器實體記憶體的百分之七八十,需要留一部分給作業系統
maxmemory_human:0B                  # 設定中設定的最大可使用記憶體值
maxmemory_policy:noeviction         # 當達到maxmemory時的淘汰策略