1. 程式人生 > 實用技巧 >[快取] Redis

[快取] Redis


簡介

Redis是開源的記憶體資料庫,它可以用作資料庫、快取和訊息中介軟體。

Redis與其他key-value 快取產品有以下三個特點
Redis支援資料的持久化,可以將記憶體中的資料儲存在磁碟中,重啟的時候可以再次載入進行使用。
Redis不僅僅支援簡單的key-value型別的資料,同時還提供list,set,zset,hash等資料結構的儲存。
Redis支援資料的備份,即master-slave模式的資料備份。


資料型別

資料型別 可以儲存的值 操作
STRING 字串、整數或者浮點數 對整個字串或者字串的其中一部分執行操作對整數和浮點數執行自增或者自減操作
LIST 列表 從兩端壓入或者彈出元素 對單個或者多個元素進行修剪,只保留一個範圍內的元素
SET 無序集合 新增、獲取、移除單個元素 檢查一個元素是否存在於集合中計算交集、並集、差集從集合裡面隨機獲取元素
HASH 包含鍵值對的無序散列表 新增、獲取、移除單個鍵值對獲取所有鍵值對檢查某個鍵是否存在
ZSET 有序集合 新增、獲取、刪除元素

String

> set hello world
OK
> get hello
"world"
> del hello
(integer) 1
> get hello
(nil)

List

> rpush list-key item
(integer) 1
> rpush list-key item2
(integer) 2
> rpush list-key item
(integer) 3

> lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"

> lindex list-key 1
"item2"

> lpop list-key
"item"

> lrange list-key 0 -1
1) "item2"
2) "item"

Set

> sadd set-key item
(integer) 1
> sadd set-key item2
(integer) 1
> sadd set-key item3
(integer) 1
> sadd set-key item
(integer) 0

> smembers set-key
1) "item"
2) "item2"
3) "item3"

> sismember set-key item4
(integer) 0
> sismember set-key item
(integer) 1

> srem set-key item2
(integer) 1
> srem set-key item2
(integer) 0

> smembers set-key
1) "item"
2) "item3"

Hash

> hset hash-key sub-key1 value1
(integer) 1
> hset hash-key sub-key2 value2
(integer) 1
> hset hash-key sub-key1 value1
(integer) 0

> hgetall hash-key
1) "sub-key1"
2) "value1"
3) "sub-key2"
4) "value2"

> hdel hash-key sub-key2
(integer) 1
> hdel hash-key sub-key2
(integer) 0

> hget hash-key sub-key1
"value1"

> hgetall hash-key
1) "sub-key1"
2) "value1"

Zset

> zadd zset-key 728 member1
(integer) 1
> zadd zset-key 982 member0
(integer) 1
> zadd zset-key 982 member0
(integer) 0

> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"

> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"

> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0

> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"

使用場景

計數器

可以對 String 進行自增自減運算,從而實現計數器功能。
Redis 這種記憶體型資料庫的讀寫效能非常高,很適合儲存頻繁讀寫的計數量。
排行榜
jedis.zadd(prefixKey + key, score, member);
訂單號生成等 String orderId = dateStr + goodsObjectId + goodsId + redisObjectManager.addOnce(dateStr);
點贊功能
當有使用者為一篇文章點贊時,除了要對該文章的 votes 欄位進行加 1 操作,還必須記錄該使用者已經對該文章進行了點贊,防止使用者點贊次數超過 1。可以建立文章的已投票使用者集合來進行記錄。

購物車

快取

將熱點資料放到記憶體中,設定記憶體的最大使用量以及淘汰策略來保證快取的命中率。

查詢表

例如 DNS 記錄就很適合使用 Redis 進行儲存。
查詢表和快取類似,也是利用了 Redis 快速的查詢特性。但是查詢表的內容不能失效,而快取的內容可以失效,因為快取不作為可靠的資料來源。

訊息佇列

List 是一個雙向連結串列,可以通過 lpush 和 rpop 寫入和讀取訊息
不過最好使用 Kafka、RabbitMQ 等訊息中介軟體。

會話快取

可以使用 Redis 來統一儲存多臺應用伺服器的會話資訊。
當應用伺服器不再儲存使用者的會話資訊,也就不再具有狀態,一個使用者可以請求任意一個應用伺服器,從而更容易實現高可用性以及可伸縮性。

分散式鎖實現

在分散式場景下,無法使用單機環境下的鎖來對多個節點上的程序進行同步。
可以使用 Redis 自帶的 SETNX 命令實現分散式鎖,除此之外,還可以使用官方提供的 RedLock 分散式鎖實現。

其它

Set 可以實現交集、並集等操作,從而實現共同好友等功能。
ZSet 可以實現有序性操作,從而實現排行榜等功能。


資料淘汰策略

策略 描述
volatile-lru 從已設定過期時間的資料集中挑選最近最少使用的資料淘汰
volatile-ttl 從已設定過期時間的資料集中挑選將要過期的資料淘汰
volatile-random 從已設定過期時間的資料集中任意選擇資料淘汰
allkeys-lru 從所有資料集中挑選最近最少使用的資料淘汰
allkeys-random 從所有資料集中任意選擇資料進行淘汰
noeviction 禁止驅逐資料

持久化

RDB 持久化

將某個時間點的所有資料都存放到硬碟上。
可以將快照複製到其它伺服器從而建立具有相同資料的伺服器副本。
如果系統發生故障,將會丟失最後一次建立快照之後的資料。
如果資料量很大,儲存快照的時間會很長。

AOF 持久化

將寫命令新增到 AOF 檔案(Append Only File)的末尾。
使用 AOF 持久化需要設定同步選項,從而確保寫命令同步到磁碟檔案上的時機。這是因為對檔案進行寫入並不會馬上將內容同步到磁碟上,而是先儲存到緩衝區,然後由作業系統決定什麼時候同步到磁碟。有以下同步選項:

選項 同步頻率
always 每個寫命令都同步
everysec 每秒同步一次
no 讓作業系統來決定何時同步
always 選項會嚴重減低伺服器的效能;
everysec 選項比較合適,可以保證系統崩潰時只會丟失一秒左右的資料,並且 Redis 每秒執行一次同步對伺服器效能幾乎沒有任何影響;
no 選項並不能給伺服器效能帶來多大的提升,而且也會增加系統崩潰時資料丟失的數量。

隨著伺服器寫請求的增多,AOF 檔案會越來越大。Redis 提供了一種將 AOF 重寫的特性,能夠去除 AOF 檔案中的冗餘寫命令。


高可用

哨兵模式

監控(Monitoring):哨兵會不斷地檢查主節點和從節點是否運作正常。
自動故障轉移(Automatic failover):當主節點不能正常工作時,哨兵會開始自動故障轉移操作,它會將失效主節點的其中一個從節點升級為新的主節點,並讓其他從節點改為複製新的主節點。
配置提供者(Configuration provider):客戶端在初始化時,通過連線哨兵來獲得當前 Redis 服務的主節點地址。
通知(Notification):哨兵可以將故障轉移的結果傳送給客戶端。


常用命令

#ttl
Redis TTL 命令以秒為單位返回 key 的剩餘過期時間。
#expire
Redis Expire 命令用於設定 key 的過期時間。key 過期後將不再可用。每次重新獲取過期時間都會重置。
#PERSIST
Redis PERSIST 命令用於移除給定 key 的過期時間,使得 key 永不過期。
#Pttl
Redis Pttl 命令以毫秒為單位返回 key 的剩餘過期時間。

常見問題

快取穿透

簡介:快取同一時間大面積的失效,所以,後面的請求都會落到資料庫上,造成資料庫短時間內承受大量請求而崩掉。
事前:儘量保證整個 redis 叢集的高可用性,發現機器宕機儘快補上。選擇合適的記憶體淘汰策略。
事中:本地ehcache快取 + hystrix限流&降級,避免MySQL崩掉
事後:利用 redis 持久化機制儲存的資料儘快恢復快取

快取穿透

簡介:一般是黑客故意去請求快取中不存在的資料,導致所有的請求都落到資料庫上,造成資料庫短時間內承受大量請求而崩掉。
解決辦法: 有很多種方法可以有效地解決快取穿透問題,最常見的則是採用布隆過濾器,將所有可能存在的資料雜湊到一個足夠大的bitmap中,一個一定不存在的資料會被 這個bitmap攔截掉,從而避免了對底層儲存系統的查詢壓力。另外也有一個更為簡單粗暴的方法(我們採用的就是這種),如果一個查詢返回的資料為空(不管是數 據不存在,還是系統故障),我們仍然把這個空結果進行快取,但它的過期時間會很短,最長不超過五分鐘。

序列化與反序列化

java.lang.ClassNotFoundException問題有:
序列化和反序列化的serialVersionUID不一致。
反序列的物件和反序列化的物件所屬的包名不一致。
示例:
同一個類不同專案,在用redis序列化儲存之後,反序列化物件為空,找了半天問題,原因是
序列化專案此類的包名
com.iminer.business.domain.order.xxx.class
另一個專案此類的包名
com.iminer.domain.order.xxx.class
這樣的結果是反序列化的時候找不到類,所以取不到值。

快取一致性

Cache Aside Pattern
在一開始先科普下最經典的快取+資料庫讀寫的模式,就是 Cache Aside Pattern。
讀的時候,先讀快取,快取沒有的話,就讀資料庫,然後取出資料後放入快取,同時返回響應。
更新的時候,先更新資料庫,然後再刪除快取。

分散式鎖加鎖解鎖

1 基於Redis實現分散式鎖

  • 獲取鎖
    SET mylock userId NX PX 10000複製程式碼
    mylock為鎖對應的key
    userId為唯一的使用者標識,用於刪除時校驗
    NX表示只有當key不存在時才能set成功,確保只有一個客戶端能夠請求成功
    PX 10000表示這個鎖有一個10秒的自動過期時間
  • 釋放鎖
    當業務完成後刪除key來釋放鎖,可以執行以下lua指令碼:
    if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1])else return 0end複製程式碼
    執行以上指令碼時,需要將mylock作為KEYS[1]傳進去,將userId作為ARGV[1]傳進去
  • 注意點
    必須要給鎖加一個過期時間:這樣即使中間系統異常了,等過期時間到了,也可以自動釋放鎖,防止出現死鎖現象
    獲取鎖時不能分成先設定key,再設定過期時間兩步去執行,錯誤示例如下:
 # 當key不存在時設定值    setnx mylock userId    # 設定過期時間    expire mylock 10複製程式碼

這樣會存在一個問題,如果系統在執行完setnx之後異常了,expire指令就無法執行,同樣會出現死鎖現象

有必要將value設定為一個唯一的使用者標識,用於保證所要釋放的鎖是自己建立的,因為在極端的情況下會出現下列情況:

A成功獲取了鎖
A在某個操作上被阻塞了很久
A的鎖到達過期時間
B獲取了鎖
A從阻塞中恢復了,執行釋放鎖操作,把B的鎖釋放了,導致B操作不受保護

釋放鎖操作需要保證操作時原子性的,需要通過Lua指令碼來實現。它將GET、判斷是否相同、DEL三個步驟以一個原子性的方式去完成。如果按邏輯分開執行同樣會出現類似上面的問題:

A先判斷當前鎖的值,確定了是自己建的鎖,準備釋放鎖了
因為網路問題或者系統卡頓導致A被阻塞了
A的鎖過期了
B獲取鎖
A從阻塞中恢復了
A呼叫DEL釋放了B的鎖
從上面的描述可以看出來,當出現系統阻塞或者網路延遲等情況下,可能業務還沒有執行完成,鎖就過期自動釋放了,這時它的業務操作時不受保護的。

2 基於Redisson
3 資料庫樂觀鎖
4 ZooKeeper 的分散式鎖
ZooKeeper的分散式鎖主要是通過建立臨時有序節點的方式實現的:
發起加鎖請求,在ZooKeeper中建立一個臨時有序節點
判斷自己建立的節點是否是最小序號的
如果是最小的,則成功獲取鎖
如果不是最小的,則在它的上一節點加上一個監聽器
處理完業務後,釋放鎖,即刪除對應的節點
ZooKeeper通知監聽這個節點的監聽器,你的前面已經沒有其他節點了,你可以獲取鎖了
對應節點獲取鎖