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
事務的區別
第一個:
MySQL
用BEGIN
、ROLLBACK
、COMMIT
,顯式開啓並控制一個新的Transaction
。事務是默認開啓的。MySQL
主要是通過樂觀鎖和悲觀鎖進行數據庫事務的併發控制。
Redis
是用MULTI
、EXEC
、DISCARD
,顯式開啓並控制一個Transaction
。Redis
中是通過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