一文聊聊Redis中的過期操作和過期策略

本篇文章帶大家了解一下redis中的過期操作和過期策略,介紹一下redis中設置過期時間的四種方法、持久化中的過期鍵、過期鍵執行流程等,希望對大家有所幫助!

一文聊聊Redis中的過期操作和過期策略

如果在 Redis 中沒有過期這個概念,這就意味著我們所有寫入的鍵只要不主動刪除就會一直保存在 Redis 中,而 Redis 又是一個基于內存的數據庫,內存空間是非常有限的。【相關推薦:Redis視頻教程

過期操作

過期設置

Redis 中設置過期時間主要通過以下四種方式:

  • expire key seconds:設置 key 在 n 秒后過期。
  • pexpire key milliseconds:設置 key 在 n 毫秒后過期。
  • expireat key timestamp:設置 key 在某個時間戳(精確到秒)之后過期。
  • pexpireat key millisecondsTimestamp:設置 key 在某個時間戳(精確到毫秒)之后過期。

可用命令 ttl key (以秒為單位)或 pttl key (以毫秒為單位)來查看 key 還有多久過期。

Redis 可以使用 time 命令查詢當前時間的時間戳(精確到秒)。

字符串中幾個直接操作過期時間的方法,如下列表:

  • set key value ex seconds:設置鍵值對的同時指定過期時間(精確到秒)。
  • set key value px milliseconds:設置鍵值對的同時指定過期時間(精確到毫秒)。
  • setex key seconds valule:設置鍵值對的同時指定過期時間(精確到秒)。

移除過期時間

使用命令: persist key 可以移除鍵值的過期時間。-1 表示永不過期。

Java 實現過期操作

使用 Jedis 來實現對 Redis 的操作,代碼:

public?class?TTLTest?{ ????public?static?void?main(String[]?args)?throws?InterruptedException?{ ????????//?創建?Redis?連接 ????????Jedis?jedis?=?new?Jedis("xxx.xxx.xxx.xxx",?6379); ????????//?設置?Redis?密碼(如果沒有密碼,此行可省略) ????????jedis.auth("xxx"); ????????//?存儲鍵值對(默認情況下永不過期) ????????jedis.set("k",?"v"); ????????//?查詢?TTL(過期時間) ????????Long?ttl?=?jedis.ttl("k"); ????????//?打印過期日志 ????????//?過期時間:-1 ????????System.out.println("過期時間:"?+?ttl); ????????//?設置?100s?后過期 ????????jedis.expire("k",?100); ????????//?等待?1s?后執行 ????????Thread.sleep(1000); ????????//?打印過期日志 ????????//?執行?expire?后的?TTL=99 ????????System.out.println("執行?expire?后的?TTL="?+?jedis.ttl("k")); ????} }

更多過期操作方法,如下列表:

  • pexpire(String key, long milliseconds):設置 n 毫秒后過期。
  • expireAt(String key, long unixTime):設置某個時間戳后過期(精確到秒)。
  • pexpireAt(String key, long millisecondsTimestamp):設置某個時間戳后過期(精確到毫秒)。
  • persist(String key):移除過期時間。
public?class?TTLTest?{ ????public?static?void?main(String[]?args)?throws?InterruptedException?{ ????????//?創建?Redis?連接 ????????Jedis?jedis?=?new?Jedis("xxx.xxx.xxx.xxx",?6379); ????????//?設置?Redis?密碼(如果沒有密碼,此行可省略) ????????jedis.auth("xxx"); ????????//?存儲鍵值對(默認情況下永不過期) ????????jedis.set("k",?"v"); ????????//?查詢?TTL(過期時間) ????????Long?ttl?=?jedis.ttl("k"); ????????//?打印過期日志 ????????System.out.println("過期時間:"?+?ttl); ????????//?設置?100s?后過期 ????????jedis.expire("k",?100); ????????//?等待?1s?后執行 ????????Thread.sleep(1000); ????????//?打印過期日志 ????????System.out.println("執行?expire?后的?TTL="?+?jedis.ttl("k")); ????????//?設置?n?毫秒后過期 ????????jedis.pexpire("k",?100000); ????????//?設置某個時間戳后過期(精確到秒) ????????jedis.expireAt("k",?1573468990); ????????//?設置某個時間戳后過期(精確到毫秒) ????????jedis.pexpireAt("k",?1573468990000L); ????????//?移除過期時間 ????????jedis.persist("k"); ????} }

持久化中的過期鍵

RDB 中的過期鍵

RDB 文件分為兩個階段,RDB 文件生成階段和加載階段。

1. RDB 文件生成

RDB 加載分為以下兩種情況:

  • 如果 Redis 是服務器運行模式的話,在載入 RDB 文件時,程序會對文件中保存的鍵進行檢查,過期鍵不會被載入到數據庫中。所以過期鍵不會對載入 RDB 文件的主服務器造成影響;
  • 如果 Redis 是服務器運行模式的話,在載入 RDB 文件時,不論鍵是否過期都會被載入到數據庫中。但由于主從服務器在進行數據同步時,從服務器的數據會被清空。所以一般來說,過期鍵對載入 RDB 文件的從服務器也不會造成影響。

RDB 文件加載的源碼可以在 rdb.c 文件的 rdbLoad() 函數中找到,源碼所示:

/*?Check?if?the?key?already?expired.?This?function?is?used?when?loading *?an?RDB?file?from?disk,?either?at?startup,?or?when?an?RDB?was *?received?from?the?master.?In?the?latter?case,?the?master?is *?responsible?for?key?expiry.?If?we?would?expire?keys?here,?the *?snapshot?taken?by?the?master?may?not?be?reflected?on?the?slave.? * *?如果服務器為主節點的話, *?那么在鍵已經過期的時候,不再將它們關聯到數據庫中去 */ if?(server.masterhost?==?NULL?&amp;&amp;?expiretime?!=?-1?&amp;&amp;?expiretime?<h4 data-id="heading-6"><strong>AOF 中的過期鍵</strong></h4><p><strong>1. AOF 文件寫入</strong></p><p>當 Redis 以 AOF 模式持久化時,如果數據庫某個過期鍵還沒被刪除,那么 AOF 文件會保留此過期鍵,當此過期鍵被刪除后,Redis 會向 AOF 文件追加一條 DEL 命令來顯式地刪除該鍵值。</p><p><strong>2. AOF 重寫</strong></p><p>執行 AOF 重寫時,會對 Redis 中的鍵值對進行檢查已過期的鍵不會被保存到重寫后的 AOF 文件中,因此不會對 AOF 重寫造成任何影響。</p><p><strong><span style="font-size: 18px;">主從庫的過期鍵</span></strong></p><p>當 Redis 運行在主從模式下時,<strong>從庫不會進行過期掃描,從庫對過期的處理是被動的</strong>。也就是即使從庫中的 key 過期了,如果有客戶端訪問從庫時,依然可以得到 key 對應的值,像未過期的鍵值對一樣返回。</p><p><strong>從庫的過期鍵處理依靠主服務器控制</strong>,主庫在 key 到期時,會在 AOF 文件里增加一條 del 指令,同步到所有的從庫,從庫通過執行這條 del 指令來刪除過期的 key。</p><h2 data-id="heading-8">過期策略</h2><blockquote>在 Redis 中我們可以給一些元素設置過期時間,那當它過期之后 Redis 是如何處理這些過期鍵呢?</blockquote><p><strong><span style="font-size: 18px;">過期鍵執行流程</span></strong></p><p>Redis 之所以能知道那些鍵值過期,是因為在 Redis 中維護了一個字典,存儲了所有設置了過期時間的鍵值,我們稱之為<strong>過期字典</strong>。</p><p><img src="https://img.php.cn/upload/image/392/844/256/1644372296414098.png" title="1644372296414098.png" alt="1.png"></p><p><strong><span   style="max-width:90%">過期鍵源碼分析</span></strong></p><p>過期鍵存儲在 redisDb 結構中,源代碼在 src/server.h 文件中(基于 Redis 5):</p><pre class="brush:js;toolbar:false;">/*?Redis?database?representation.?There?are?multiple?databases?identified ?*?by?integers?from?0?(the?default?database)?up?to?the?max?configured ?*?database.?The?database?number?is?the?'id'?field?in?the?structure.?*/ typedef?struct?redisDb?{ ????dict?*dict;?????????????????/*?數據庫鍵空間,存放著所有的鍵值對?*/ ????dict?*expires;??????????????/*?鍵的過期時間?*/ ????dict?*blocking_keys;????????/*?Keys?with?clients?waiting?for?data?(BLPOP)*/ ????dict?*ready_keys;???????????/*?Blocked?keys?that?received?a?PUSH?*/ ????dict?*watched_keys;?????????/*?WATCHED?keys?for?MULTI/EXEC?CAS?*/ ????int?id;?????????????????????/*?Database?ID?*/ ????long?long?avg_ttl;??????????/*?Average?TTL,?just?for?stats?*/ ????list?*defrag_later;?????????/*?List?of?key?names?to?attempt?to?defrag?one?by?one,?gradually.?*/ }?redisDb;

過期鍵數據結構如下圖所示:

一文聊聊Redis中的過期操作和過期策略

過期策略

Redis 會刪除已過期的鍵值,以此來減少 Redis 的空間占用,但因為 Redis 本身是單線的,如果因為刪除操作而影響主業務的執行就得不償失了,為此 Redis 需要制定多個(過期)刪除策略來保證正常執行的性能。

定時刪除

在設置鍵值過期時間時,創建一個定時事件,當過期時間到達時,由事件處理器自動執行鍵的刪除操作

  • 優點:保證內存可以被盡快地釋放。
  • 缺點:在 Redis 高負載的情況下或有大量過期鍵需要同時處理時,會造成 Redis 服務器卡頓,影響主業務執行。

惰性刪除

不主動刪除過期鍵,每次從數據庫獲取鍵值時判斷是否過期,如果過期則刪除鍵值,并返回 null

  • 優點:因為每次訪問時,才會判斷過期鍵,所以此策略只會使用很少的系統資源。
  • 缺點:系統占用空間刪除不及時,導致空間利用率降低,造成了一定的空間浪費。

源碼解析

惰性刪除的源碼位于 src/db.c 文件的 expireIfNeeded 方法中,源碼如下:

int?expireIfNeeded(redisDb?*db,?robj?*key)?{ ????//?判斷鍵是否過期 ????if?(!keyIsExpired(db,key))?return?0; ????if?(server.masterhost?!=?NULL)?return?1; ????/*?刪除過期鍵?*/ ????//?增加過期鍵個數 ????server.stat_expiredkeys++; ????//?傳播鍵過期的消息 ????propagateExpire(db,key,server.lazyfree_lazy_expire); ????notifyKeyspaceEvent(NOTIFY_EXPIRED, ????????"expired",key,db-&gt;id); ????//?server.lazyfree_lazy_expire?為?1?表示異步刪除(懶空間釋放),反之同步刪除 ????return?server.lazyfree_lazy_expire???dbAsyncDelete(db,key)?: ?????????????????????????????????????????dbSyncDelete(db,key); } //?判斷鍵是否過期 int?keyIsExpired(redisDb?*db,?robj?*key)?{ ????mstime_t?when?=?getExpire(db,key); ????if?(when??when; } //?獲取鍵的過期時間 long?long?getExpire(redisDb?*db,?robj?*key)?{ ????dictEntry?*de; ????/*?No?expire??return?ASAP?*/ ????if?(dictSize(db-&gt;expires)?==?0?|| ???????(de?=?dictFind(db-&gt;expires,key-&gt;ptr))?==?NULL)?return?-1; ????/*?The?entry?was?found?in?the?expire?dict,?this?means?it?should?also ?????*?be?present?in?the?main?dict?(safety?check).?*/ ????serverAssertWithInfo(NULL,key,dictFind(db-&gt;dict,key-&gt;ptr)?!=?NULL); ????return?dictGetSignedIntegerVal(de); }

所有對數據庫的讀寫命令在執行之前,都會調用 expireIfNeeded 方法判斷鍵值是否過期,過期則會從數據庫中刪除,反之則不做任何處理。

一文聊聊Redis中的過期操作和過期策略

定期刪除

每隔一段時間檢查一次數據庫,隨機刪除一些過期鍵

Redis 默認每秒進行 10 次過期掃描,此配置可通過 Redis 的配置文件 redis.conf 進行配置,配置鍵為 hz 它的默認值是 hz 10。

注意:Redis 每次掃描并不是遍歷過期字典中的所有鍵,而是采用隨機抽取判斷并刪除過期鍵的形式執行的。

定期刪除流程

  • 從過期字典中隨機取出 20 個鍵。

  • 刪除這 20 個鍵中過期的鍵。

  • 如果過期 key 的比例超過 25%,重復步驟 1。

同時為了保證過期掃描不會出現循環過度,導致線程卡死現象,算法還增加了掃描時間的上限,默認不會超過 25ms。

一文聊聊Redis中的過期操作和過期策略

  • 優點:通過限制刪除操作的時長和頻率,來減少刪除操作對 Redis 主業務的影響,同時也能刪除一部分過期的數據減少了過期鍵對空間的無效占用。
  • 缺點:內存清理方面沒有定時刪除效果好,同時沒有惰性刪除使用的系統資源少。

源碼解析

定期刪除的核心源碼在 src/expire.c 文件下的 activeExpireCycle 方法中,源碼如下:

void?activeExpireCycle(int?type)?{ ????static?unsigned?int?current_db?=?0;?/*?上次定期刪除遍歷到的數據庫ID?*/ ????static?int?timelimit_exit?=?0;??????/*?Time?limit?hit?in?previous?call??*/ ????static?long?long?last_fast_cycle?=?0;?/*?上一次執行快速定期刪除的時間點?*/ ????int?j,?iteration?=?0; ????int?dbs_per_call?=?CRON_DBS_PER_CALL;?//?每次定期刪除,遍歷的數據庫的數量 ????long?long?start?=?ustime(),?timelimit,?elapsed; ????if?(clientsArePaused())?return; ????if?(type?==?ACTIVE_EXPIRE_CYCLE_FAST)?{ ????????if?(!timelimit_exit)?return; ????????//?ACTIVE_EXPIRE_CYCLE_FAST_DURATION?是快速定期刪除的執行時長 ????????if?(start??server.dbnum?||?timelimit_exit) ????????dbs_per_call?=?server.dbnum; ????//?慢速定期刪除的執行時長 ????timelimit?=?1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100; ????timelimit_exit?=?0; ????if?(timelimit??ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) ????????????????num?=?ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP; ????????????//?從數據庫中隨機選取?num?個鍵進行檢查 ????????????while?(num--)?{ ????????????????dictEntry?*de; ????????????????long?long?ttl; ????????????????if?((de?=?dictGetRandomKey(db-&gt;expires))?==?NULL)?break; ????????????????ttl?=?dictGetSignedInteger ????????????????//?過期檢查,并對過期鍵進行刪除 ????????????????if?(activeExpireCycleTryExpire(db,de,now))?expired++; ????????????????if?(ttl?&gt;?0)?{ ????????????????????/*?We?want?the?average?TTL?of?keys?yet?not?expired.?*/ ????????????????????ttl_sum?+=?ttl; ????????????????????ttl_samples++; ????????????????} ????????????????total_sampled++; ????????????} ????????????total_expired?+=?expired; ????????????if?(ttl_samples)?{ ????????????????long?long?avg_ttl?=?ttl_sum/ttl_samples; ????????????????if?(db-&gt;avg_ttl?==?0)?db-&gt;avg_ttl?=?avg_ttl; ????????????????db-&gt;avg_ttl?=?(db-&gt;avg_ttl/50)*49?+?(avg_ttl/50); ????????????} ????????????if?((iteration?&amp;?0xf)?==?0)?{?/*?check?once?every?16?iterations.?*/ ????????????????elapsed?=?ustime()-start; ????????????????if?(elapsed?&gt;?timelimit)?{ ????????????????????timelimit_exit?=?1; ????????????????????server.stat_expired_time_cap_reached_count++; ????????????????????break; ????????????????} ????????????} ????????????/*?每次檢查只刪除?ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4?個過期鍵?*/ ????????}?while?(expired?&gt;?ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); ????} ????//?....... }

activeExpireCycle 方法在規定的時間,分多次遍歷各個數據庫,從過期字典中隨機檢查一部分過期鍵的過期時間,刪除其中的過期鍵。

這個函數有兩種執行模式,一個是快速模式一個是慢速模式,體現是代碼中的 timelimit 變量,這個變量是用來約束此函數的運行時間的。快速模式下 timelimit 的值是固定的,等于預定義常量 ACTIVE_EXPIRE_CYCLE_FAST_DURATION,慢速模式下,這個變量的值是通過 1000000 * ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100 計算的。

Redis 使用的過期策略

Redis 使用的是惰性刪除加定期刪除的過期策略

更多編程相關知識,請訪問:Redis視頻教程!!

? 版權聲明
THE END
喜歡就支持一下吧
點贊9 分享