Redis之淘汰策略、過期機制和事務控制

Redis之內存淘汰、過期機制和事務操作

一、內存淘汰策略

1.1. Redis一共有六種淘汰機制:
  • noeviction:當內存使用達到閾值時候,所有引用申請內存的命令都會報錯
  • allkeys-lru:在主鍵空間中,優先移除最近未使用的key(推薦)
  • volatile-lru:在設置了過期時間的鍵空間中,優先移除最近未使用的key
  • allkeys-random:在主鍵空間中,隨機移除某一個key
  • volatile-random:在設置了過期時間的鍵空間中,隨機移除一個key
  • volatile-ttl:在設置了過期時間的鍵空間中,具有更早過期時間的key優先移除
1.2. 如何設置淘汰策略

設置Redis的內存大小限制, 當數據達到限定的大小之後,會選擇配置的淘汰策略數據

# maxmemory <bytes>
maxmemory 100mb

配置Redis淘汰策略

# The default is:
#
maxmemory-policy allkeys-lru

二、過期策略

2.1. Redis過期的命令
expire <key> <TTL>  # 將鍵的生存時間設置爲ttl秒
pexpire <key> <TTL>   # 將鍵的生存時間設置爲ttl毫秒
expireat <key> <timestamp>  # 將鍵的過期時間設爲timestamp所指定的秒時間戳
pexpireat <key> <timestamp>  # 將鍵的過期時間設爲timestamp所指定的毫秒時間戳
ttl <key> # 返回剩餘生存時間ttl秒
pttl <key> # 返回剩餘生存時間pttl毫秒
persist <key> # 移除一個鍵的過期時間

Redis是使用定期刪除+惰性刪除兩者配合的過期策略。

2.2. 定期刪除

定期刪除指的是Redis默認每隔100ms就隨機抽取一些設置了過期時間的key,檢測這些key是否過期,如果過期了就將其刪掉。

因爲key太多,如果全盤掃描所有的key會非常耗性能,所以是隨機抽取一些key來刪除。這樣就有可能刪除不完,需要惰性刪除配合。

2.3. 惰性刪除

惰性刪除不再是Redis去主動刪除,而是在客戶端要獲取某個key的時候,Redis會先去檢測一下這個key是否已經過期,如果沒有過期則返回給客戶端,如果已經過期了,那麼Redis會刪除這個key,不會返回給客戶端。

所以惰性刪除可以解決一些過期了,但沒被定期刪除隨機抽取到的key。但有些過期的key既沒有被隨機抽取,也沒有被客戶端訪問,就會一直保留在數據庫,佔用內存,長期下去可能會導致內存耗盡。所以Redis提供了內存淘汰機制來解決這個問題。

2.4. Redis過期通知機制

要開啓Redis過期通知需要修改配置文件:redis.conf,當我們的key失效時,可以執行我們的客戶端回調監聽的方法。具體配置如下:

############################# EVENT NOTIFICATION ##############################

# Redis可以通知發佈/訂閱客戶端有關密鑰空間中發生的事件。
# This feature is documented at http://redis.io/topics/notifications
#
# 例如,如果啓用了鍵空間事件通知,
# 並且客戶端對存儲在數據庫0中的鍵“ foo”執行了DEL操作,則將通過Pub / Sub發佈兩條消息:
#
# PUBLISH __keyspace@0__:foo del
# PUBLISH __keyevent@0__:del foo
#
# 可以在一組類中選擇Redis將通知的事件。每個類都由一個字符標識:
#
#  K     keyspace事件,事件以__keyspace@<db>__爲前綴進行發佈
#  E     keyevent事件,事件以__keyevent@<db>__爲前綴進行發佈
#  g     一般性的,非特定類型的命令,比如del,expire,rename等
#  $     字符串特定命令
#  l     列表特定命令
#  s     集合特定命令
#  h     哈希特定命令
#  z     有序集合特定命令
#  x     過期事件,當某個鍵過期並刪除時會產生該事件
#  e     驅逐事件,當某個鍵因maxmemore策略而被刪除時,產生該事件
#  A     g$lshzxe的別名,因此”AKE”意味着所有事件
#
#  “notify-keyspace-events”將由零個或多個字符組成的字符串作爲參數。
# 空字符串表示已禁用通知
#
#  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
#
#  默認情況下,所有通知都被禁用,因爲大多數用戶不需要此功能,並且該功能有一些開銷。
# 請注意,如果您未指定K或E中的至少一個,則不會傳遞任何事件。
notify-keyspace-events Ex

重啓Redis之後,我們測試一下:

127.0.0.1:6379> psubscribe __keyevent@0__:expired   # 開啓失效監聽
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:expired"
3) (integer) 1

開啓另一個Redis客戶端:

127.0.0.1:6379> setex age 5 19
OK
127.0.0.1:6379> 

五秒之後開啓監聽的客戶端就會出現我們剛纔設置的key

127.0.0.1:6379> psubscribe __keyevent@0__:expired
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__keyevent@0__:expired"
3) (integer) 1
1) "pmessage"
2) "__keyevent@0__:expired"
3) "__keyevent@0__:expired"
4) "age"    # 剛纔的age
2.5. Springboot整合Redis過期監聽

需求: 處理訂單過期自動取消,比如30分鐘未支付自動更新訂單狀態。

引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

增加Redis的監聽配置類

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;

/**
 * @author 墨龍吟
 * @version 1.0.0
 * @ClassName RedisConfig.java
 * @Description Redis失效監聽器
 * @createTime 2019年12月07日 - 12:51
 */
@Configuration
public class RedisListenerConfig {

    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer listenerContainer = new RedisMessageListenerContainer();
        listenerContainer.setConnectionFactory(connectionFactory);
        return listenerContainer;
    }
}
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;

/**
 * @author 墨龍吟
 * @version 1.0.0
 * @ClassName RedisKeyExpirationListener.java
 * @Description 具體的監聽類
 * @createTime 2019年12月07日 - 12:58
 */
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
        super(listenerContainer);
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        // 裏面就可處理自己的業務了, message.toString()可以獲取失效的key
        String expiredKey= message.toString();
        System.out.println("該key :expiraKey:" + expiredKey + "失效啦~");
        // 如果符合我們定義的前綴的話,就開始處理數據
        if (expiredKey.startsWith("order:")) {
            System.out.println("拿到key爲:"+ expiredKey +" ==> 開始處理業務");
        }
    }
}

添加一個控制器使用:

@RestController
public class RedisController {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
// 使用這個需要注意,redisTemplate,這個要是用@Resource注入
//    @Resource
//    private RedisTemplate<String, Object> redisTemplate;

    @GetMapping("/set_key")
    public String setKey() {
        stringRedisTemplate.opsForValue().set("order:name", UUID.randomUUID().toString(), 5L, TimeUnit.SECONDS);
        System.out.println("設置的key");
        return "success";
    }

}

結果:

注意 :針對這樣的業務,我們也可以使用Spring + quartz定時任務,下單成功後,生成一個30分鐘後運行的任務,30分鐘後檢查訂單狀態,如果未支付,則進行處理。

2.6. 缺點與改進
2.6.1 缺點:

Redis key的失效通知機制是基於其pub/sub模式的;這個模式有個致命的缺陷是,消息通知不能持久化,假如監聽服務宕機期間,有key過期,那麼這個失效通知就被忽略了。這樣的場景,j就會出現丟失通知的情況,無法及時處理業務。

2.6.2 改進:

應當使用RabbitMq,超時自動取消訂單使用RabbitMq的死信隊列,接收死信隊列更新訂單狀態即可。

三、事務操作

事務是必須滿足4個條件(ACID)::原子性(Atomicity,或稱不可分割性)、一致性(Consistency)、隔離性(Isolation,又稱獨立性)、持久性(Durability)。

3.1. Redis事務的基本操作

ping:命令客戶端向 Redis 服務器發送一個 PING ,如果服務器運作正常的話,會返回一個 PONG,通常用於測試與服務器的連接是否仍然生效,或者用於測量延遲值。

127.0.0.1:6379> multi             # 開啓事務
OK
127.0.0.1:6379> incr user_id      # 將user_id(默認爲0)值加一
QUEUED
127.0.0.1:6379> incr user_id
QUEUED
127.0.0.1:6379> incr user_id
QUEUED
127.0.0.1:6379> ping              
QUEUED
127.0.0.1:6379> exec             # 提交事務
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG
127.0.0.1:6379> 

注意,如果不加watch,假如有另外客服端將user_id改爲100,那麼最終exec後,user_id值爲103。

使用watch監視key

127.0.0.1:6379> watch name age    # 監視 name, age 兩個key
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name tom
QUEUED
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> exec
1) OK
2) (integer) 1
127.0.0.1:6379> 

watch監視key,且事務被打斷:

# 第一個客戶端
127.0.0.1:6379> watch java java_version   # 第一個Redis客戶端監視 這兩個key
OK
127.0.0.1:6379> multi            #  開啓事務
OK 
127.0.0.1:6379> set java web     
QUEUED
# 第二個客戶端
127.0.0.1:6379> set java_version 1.8
OK
127.0.0.1:6379> 
# 返回第一個客戶端提交事務
127.0.0.1:6379> exec
(nil)    # 失敗
127.0.0.1:6379> 

unwatch取消監視key

在執行 watch命令之後, exec命令或 discard命令先被執行了的話,那麼就不需要再執行 unwatch了。

127.0.0.1:6379> watch java java_version
OK
127.0.0.1:6379> unwatch
OK
127.0.0.1:6379> 

discard取消事務:放棄執行事務塊內的所有命令。

127.0.0.1:6379> multi                 # 開啓事務
OK
127.0.0.1:6379> ping
QUEUED
127.0.0.1:6379> set name 123456
QUEUED
127.0.0.1:6379> discard               # 取消事務
OK
127.0.0.1:6379> exec                  # 提交事務失敗  
(error) ERR EXEC without MULTI
127.0.0.1:6379> 

事務內命令發生語法錯誤,整個事務命令都不執行:

127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> set email                # 錯誤命令
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> exec                     # 提交事務失敗
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> 

事務內,命令格式語法正確,但是執行出錯,其他命令正常,不會回滾:

127.0.0.1:6379> multi               # 開啓事務
OK
127.0.0.1:6379> incr age            # age加一
QUEUED
127.0.0.1:6379> get age             # 獲取age
QUEUED
127.0.0.1:6379> set name tom        # 爲name賦值爲tom
QUEUED
127.0.0.1:6379> incr name           # name 加一
QUEUED
127.0.0.1:6379> get name            # 獲取name值
QUEUED
127.0.0.1:6379> exec                # 提交事務
1) (integer) 2
2) "2"
3) OK
4) (error) ERR value is not an integer or out of range
5) "tom"
127.0.0.1:6379> 
3.2. Redis事務和MySQL事務的區別

第一個:

MySQLBEGINROLLBACKCOMMIT,顯式開啓並控制一個新的Transaction。事務是默認開啓的。MySQL主要是通過樂觀鎖和悲觀鎖進行數據庫事務的併發控制。

Redis是用MULTIEXECDISCARD,顯式開啓並控制一個TransactionRedis中是通過watch加樂觀鎖對數據庫進行併發控制。

第二個:

MySQL實現事務,是基於UNDO/REDO日誌。UNDO日誌記錄修改前的狀態,ROLLBACK基於UNDO日誌實現;ERDO記錄修改之後的狀態,COMMIT基於ERDO日誌實現。

Redis實現事務,是基於COMMANDS隊列,如果沒有開啓事務,COMMAND會立即返回執行結果,並直接寫入磁盤;如果事務開啓,COMMAND不會被立即執行,而是排入隊列並返回排隊狀態。調用EXEC纔會執行COMMANDS隊列。

3.3. Redis如何保證事務安全

Redis本身就是單線程的能夠保證線程安全問題。

四、悲觀鎖、樂觀鎖和watch解釋

  • 悲觀鎖:

    悲觀鎖(Pessimistic Lock),每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。

    傳統的關係型數據庫裏邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。

  • 樂觀鎖:

    樂觀鎖(Optimistic Lock),就是很樂觀,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,可以使用版本號等機制。

    樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量。

    樂觀鎖策略:提交版本必須大於記錄當前版本才能執行更新。

  • watch

    watch指令,類似樂觀鎖,事務提交時,如果Key的值已被別的客戶端改變。比如某個list已被別的客戶端push/pop過了,整個事務隊列都不會被執行。

    通過watch命令在事務執行之前監控了多個keys,倘若在watch之後有任何key的值發生了變化,exec命令執行的事務都將被放棄,同時返回Nullmulti-bulk應答以通知調用者事務執行失敗。

    一旦執行了exec/unwatch/discard之前加的監控鎖都會被取消掉了。

五、參考文章

  • https://blog.csdn.net/J080624/article/details/81669560
  • https://www.jianshu.com/p/5f31d77d006b
  • https://juejin.im/post/5da96c955188255a313299b7
發佈了150 篇原創文章 · 獲贊 53 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章