一文認識redis及其使用場景

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值,而是

  1. 隨機抽選20個key
  2. 刪除其中過期的key
  3. 如果其中過期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)可緩存不一致的問題。

  1. A更新數(shù)據(jù)庫單據(jù)從20為25;
  2. B更新數(shù)據(jù)庫從25到30;
  3. B更新緩存為30;
  4. 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ù)庫和緩存的原子性,但這樣性能會丟失很多,也失去使用緩存的初衷。

  1. A從數(shù)據(jù)庫中讀取單價20;
  2. B更新數(shù)據(jù)庫從20到25;
  3. B刪除緩存;
  4. 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的一些總結,平時使用還是需要多學習內部原理。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容