redis是一個高性能key-value內存數(shù)據(jù)庫。在日常開發(fā)中使用redis最常見的就是當做緩存,基于redis的特殊數(shù)據(jù)結構和相關特性,redis的應用場景還很多,比如實現(xiàn)分布式鎖、延遲消息、消息隊列等功能。本文將介紹redis的基本數(shù)據(jù)類型及其應用場景、redis的過期策略、持久化、集群、以及在日常開發(fā)中的使用。
基本數(shù)據(jù)類型
| 數(shù)據(jù)類型 | value | 操作 | 運用場景 |
|---|---|---|---|
| string | 可以使字符串、整數(shù)或浮點數(shù) | 對整個字符串或者字符串的其中一部分進行操作;對整數(shù)和浮點數(shù)執(zhí)行自增(increment)或者自減(decrement)操作 | 計數(shù)器(瀏覽數(shù))、分布式全局唯一id |
| list | 一個鏈表,鏈表上每個節(jié)點都包含了一個字符串 | 從鏈表的兩端推入或者彈出元素;根據(jù)偏移量對鏈表進行修剪(trim);讀取單個或多個元素;根據(jù)值查找或者移除元素。 | 消息隊列、用戶列表 |
| set | 包含字符串的無序收集器,并且被包含的每個字符串都是獨一無二、各不相同 | 添加、獲取、移動單個元素;檢查元素是否存在于集合中;計算交集、并集、差集;從集合里面隨機獲取元素 | 抽獎活動、電商商品篩選 |
| hash | 包含鍵值對的無序散列表 | 添加、獲取、刪除單個鍵值對;獲取所有的鍵值對 | 用戶信息等發(fā)展對象 |
| zset | 字符串成員(member)與浮點數(shù)分值(score)之間的有序映射,元素的排序順序由分值的大小決定 | 添加、獲取、刪除單個元素;根據(jù)分值范圍(range)或者成員來獲取元素 | 排行榜、好友列表 |
過期策略
?redis所有的數(shù)據(jù)結構都可以設置過期時間,時間一到,就會自動刪除。如果redis中k有很大數(shù)據(jù)量的key,redis又是用什么機制保證能夠高效的刪除過期的key呢。
?redis采用定期刪除策略+惰性刪除的形式來刪除過期key。對于設置過期時間的key,redis會單獨維護一個字典存放這個key,然后redis默認每10秒掃描過期字典中的key,但并不是掃描所有的key值,而是
- 隨機抽選20個key
- 刪除其中過期的key
- 如果其中過期key的數(shù)量超過1/4,重復步驟1
?同時,為了保證過期掃描不會出現(xiàn)循環(huán)過度,導致線程卡死現(xiàn)象,算法還增加了掃描時間的上限,默認不會超過 25ms。所謂惰性策略就是在客戶端訪問這個 key 的時候,redis 對 key 的過期時間進行檢查,如果過期了就立即刪除。
淘汰策略
?當實際內存超出 redis配置的最大使用內存時,redis 提供了幾種可選策略 (maxmemory-policy) 來讓用戶自己決定該如何騰出新的空間以繼續(xù)提供讀寫服務。
noeviction 不會繼續(xù)服務寫請求 (DEL 請求可以繼續(xù)服務),讀請求可以繼續(xù)進行。這樣可以保證不會丟失數(shù)據(jù),但是會讓線上的業(yè)務不能持續(xù)進行。這是默認的淘汰策略。
volatile-lru 嘗試淘汰設置了過期時間的 key,最少使用的 key 優(yōu)先被淘汰。沒有設置過期時間的 key 不會被淘汰,這樣可以保證需要持久化的數(shù)據(jù)不會突然丟失。
volatile-ttl 跟上面一樣,除了淘汰的策略不是 LRU,而是 key 的剩余壽命 ttl 的值,ttl 越小越優(yōu)先被淘汰。
volatile-random 跟上面一樣,不過淘汰的 key 是過期 key 集合中隨機的 key。
allkeys-lru 區(qū)別于 volatile-lru,這個策略要淘汰的 key 對象是全體的 key 集合,而不只是過期的 key 集合。這意味著沒有設置過期時間的 key 也會被淘汰。
allkeys-random 跟上面一樣,不過淘汰的策略是隨機的 key。
持久化
| 策略 | 存儲方式 | 生成方式 |
|---|---|---|
| 快照(RDB) | 快照是全量備份,快照是內存數(shù)據(jù)的二進制序列化形式,在存儲上非常緊湊。 | RDB是通過Redis主進程fork子進程,讓子進程執(zhí)行磁盤 IO 操作來進行 RDB 持久化。RDB記錄的數(shù)據(jù)。 |
| AOF日志 | AOF 日志是連續(xù)的增量備份。AOF 日志記錄的是內存數(shù)據(jù)修改的指令記錄文本。AOF 日志在長期的運行過程中會變的無比龐大,數(shù)據(jù)庫重啟時需要加載 AOF 日志進行指令重放,這個時間就會無比漫長。所以需要定期進行 AOF 重寫,給 AOF 日志進行瘦身。 | AOF 日志存儲的是 Redis 服務器的順序指令序列,AOF 日志只記錄對內存進行修改的指令記錄。AOF記錄的是指令。 |
?redis4.0采用RDB與AOF混合使用,這里的 AOF 日志不再是全量的日志,而是自持久化開始到持久化結束的這段時間發(fā)生的增量 AOF 日志,通常這部分 AOF 日志很小。于是在 redis 重啟的時候,可以先加載 rdb 的內容,然后再重放增量 AOF 日志就可以完全替代之前的 AOF 全量文件重放,重啟效率因此大幅得到提升。
使用
緩存
?使用redis當緩存是日常開發(fā)中最常使用的方式,利用redis的高性能把熱點數(shù)據(jù)和一些穩(wěn)定不變的數(shù)據(jù)放入緩存中,減少db的壓力,提高系統(tǒng)的性能和接口的響應速度。
讀寫策略
?將數(shù)據(jù)放入緩存中,就會涉及一個問題,緩存和數(shù)據(jù)庫的一致性。選擇什么樣的緩存讀寫策略能夠減少緩存與數(shù)據(jù)庫的數(shù)據(jù)不一致的情況少發(fā)生。
Cache Aside(旁路緩存)策略
- 讀:
從緩存中讀取數(shù)據(jù);
如果緩存命中,則直接返回數(shù)據(jù);
如果緩存不命中,則從數(shù)據(jù)庫中查詢數(shù)據(jù);
查詢到數(shù)據(jù)后,將數(shù)據(jù)寫入到緩存中,并且返回給用戶。 - 寫:
更新數(shù)據(jù)庫;
刪除緩存;
?寫的時候為什么不是更新緩存而是刪除緩存,因為更新緩存很容易出現(xiàn)緩存不一致的問題。因為更新數(shù)據(jù)庫和更新緩存并不是一個原子操作,下面舉例說明一下。
?比如現(xiàn)在需要更新商品表中的單價,初始為20。操作A將單價改為25,將數(shù)據(jù)庫中的單價從20修改25,同時操作B也將單價改為30,將數(shù)據(jù)庫的單價從25修改為30,并且把緩存中的數(shù)據(jù)修改為30,然后操作A將緩存中的數(shù)據(jù)修改25。此時緩存中的數(shù)據(jù)為25,數(shù)據(jù)庫為30出現(xiàn)可緩存不一致的問題。
- A更新數(shù)據(jù)庫單據(jù)從20為25;
- B更新數(shù)據(jù)庫從25到30;
- B更新緩存為30;
- A更新緩存為25;
?更新之后去刪除緩存,并發(fā)去更新的數(shù)據(jù)就不會出現(xiàn)緩存不一致的問題,因為每次更新都是去刪除key值,讀取的時候都是加載最新的數(shù)據(jù)。
?但是旁路緩存還是會出現(xiàn)緩存不一致的問題,只是出現(xiàn)的幾率不高。假如緩存中商品不存在,請求A從數(shù)據(jù)庫中讀取到商品單價20,還沒有放入緩存的時候,這時候請求B將數(shù)據(jù)庫中商品單價修改為25,然后請求A將單據(jù)20放入緩存。只不過這種情況很少出現(xiàn),因為寫入緩存,是比寫入數(shù)據(jù)庫快很多的。如果非要保證數(shù)據(jù)庫和緩存的一致性,可以使用分布式鎖去實現(xiàn)操作數(shù)據(jù)庫和緩存的原子性,但這樣性能會丟失很多,也失去使用緩存的初衷。
- A從數(shù)據(jù)庫中讀取單價20;
- B更新數(shù)據(jù)庫從20到25;
- B刪除緩存;
- A更新緩存為20;
緩存問題
| 問題 | 描述 | 解決方案 |
|---|---|---|
| 緩存穿透 | 緩存穿透是指查詢一個不存在的數(shù)據(jù)。會導致每次請求都會到存儲層去,失去了緩存的意義。 | 1、布隆過濾器:將所有可能存在的數(shù)據(jù)哈希到一個足夠大的bitmap中,一個一定不存在的數(shù)據(jù)會被 這個bitmap攔截掉,從而避免了對底層存儲系統(tǒng)的查詢壓力。2、如果一個查詢返回的數(shù)據(jù)為空(不管是數(shù) 據(jù)不存在,還是系統(tǒng)故障),我們仍然把這個空結果進行緩存,但它的過期時間會很短,最長不超過五分鐘。 |
| 緩存雪崩 | 緩存雪崩是指在我們設置緩存時采用了相同的過期時間,導致緩存在某一時刻同時失效,請求全部轉發(fā)到DB,DB瞬時壓力過重雪崩。 | 緩存失效時間分散開,比如我們可以在原有的失效時間基礎上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重復率就會降低,就很難引發(fā)集體失效的事件。 |
| 緩存擊穿 | 對于一些設置了過期時間的key,如果這些key可能會在某些時間點被超高并發(fā)地訪問,是一種非?!盁狳c”的數(shù)據(jù)。這個時候,需要考慮一個問題:緩存被“擊穿”的問題,這個和緩存雪崩的區(qū)別在于這里針對某一key緩存,前者則是很多key。 | 在緩存失效的時候(判斷拿出來的值為空),不是立即去load db,而是先使用緩存工具的某些帶成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一個mutex key,當操作返回成功時,再進行l(wèi)oad db的操作并回設緩存;否則,就重試整個get緩存的方法。 |
分布式鎖
?使用redis作為分布式鎖,利用redis的指令setnx(set if not exists)當key設置值不存在時,設置值成功的機制實現(xiàn)。
| 問題 | 解決方案 |
|---|---|
| 當客戶端爭取到鎖之后掛掉,導致鎖無法釋放 | 設置key的過期時間 |
| setnx 和 expire并不是原子操作有可能還沒來得及設置過期時間客戶端掛掉 | reids2.8后新的指令和將其合并為一條set key value [ex seconds] [px milliseconds] [nx xx] |
| 過期時間時長的問題,可能key已經過期,但是內部操作還沒執(zhí)行完成,新的請求又可以取獲取鎖 | 1、保證業(yè)務不會超過這個過期時間 2、遇到這種需要阻塞等待的業(yè)務場景推薦使用zk實現(xiàn)分布式鎖 |
延遲消息
redis2.8之后推出鍵空間通知事件
利用redis的key過期通知機制,實現(xiàn)延遲消息,比如30分鐘關閉訂單、延遲通知等功能。
-
配置key過期事件
修改redis.config文件,開啟 notify-keyspace-events Ex 配置,redis推送key過期時間,redis默認情況下會禁用所有通知,所以將notify-keyspace-events ""修改為notify-keyspace-events "Ex"。開啟之后redis會以發(fā)布/訂閱的形式,當key過期時候,會推送key值過期的消息。客戶端訂閱到指定key之后,可以解析key的所代表的的業(yè)務含義,做相關的業(yè)務操作。
# Redis can notify Pub/Sub clients about events happening in the key space.
# This feature is documented at http://redis.io/topics/notifications
#
# For instance if keyspace events notification is enabled, and a client
# performs a DEL operation on key "foo" stored in the Database 0, two
# messages will be published via Pub/Sub:
#
# PUBLISH __keyspace@0__:foo del
# PUBLISH __keyevent@0__:del foo
#
# It is possible to select the events that Redis will notify among a set
# of classes. Every class is identified by a single character:
#
# K Keyspace events, published with __keyspace@<db>__ prefix.
# E Keyevent events, published with __keyevent@<db>__ prefix.
# g Generic commands (non-type specific) like DEL, EXPIRE, RENAME, ...
# $ String commands
# l List commands
# s Set commands
# h Hash commands
# z Sorted set commands
# x Expired events (events generated every time a key expires)
# e Evicted events (events generated when a key is evicted for maxmemory)
# A Alias for g$lshzxe, so that the "AKE" string means all the events.
#
# The "notify-keyspace-events" takes as argument a string that is composed
# of zero or multiple characters. The empty string means that notifications
# are disabled.
#
# Example: to enable list and generic events, from the point of view of the
# event name, use:
#
# notify-keyspace-events Elg
#
# Example 2: to get the stream of the expired keys subscribing to channel
# name __keyevent@0__:expired use:
#
# notify-keyspace-events Ex
#
# By default all notifications are disabled because most users don't need
# this feature and the feature has some overhead. Note that if you don't
# specify at least one of K or E, no events will be delivered.
notify-keyspace-events "Ex"
-
代碼示例
引入jedis
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
事件監(jiān)聽
public class KeyExpireListener extends JedisPubSub {
@Override
public void onPMessage(String pattern, String channel, String message) {
System.out.println("expired_event key : "+message);
// 拿到定義的key之后做具體的業(yè)務
}
}
事件訂閱
public class RedisSubscribe {
private Jedis jedis;
private JedisPubSub jedisPubSub;
private String topic;
public RedisSubscribe(Jedis jedis,JedisPubSub jedisPubSub,String topic){
this.jedis = jedis;
this.jedisPubSub = jedisPubSub;
this.topic = topic;
}
public void subscribe(){
//訂閱會阻塞線程,新開線程監(jiān)聽事件
new Thread(()->{
jedis.psubscribe(jedisPubSub, topic);
}).start();
}
}
測試
public static void main(String[] args) throws Exception{
Jedis jedis = new Jedis("127.0.0.1",6379);
String topic = "__keyevent@0__:expired";
RedisSubscribe redisSubscribe = new RedisSubscribe(new Jedis("127.0.0.1",6379), new KeyExpireListener(), topic);
redisSubscribe.subscribe();
String orderId = "123";
jedis.set("order:close:"+orderId, orderId);
jedis.expire("order:close:"+orderId, 5);
}
?雖然這種方式可以實現(xiàn)業(yè)務功能,但是不能保證消息的可靠性,推薦還是使用專業(yè)的消息隊列實現(xiàn)。
總結
以上是關于redis的一些總結,平時使用還是需要多學習內部原理。