自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Redis 如何為 List/Set/Hash 的元素設(shè)置單獨的過期時間

數(shù)據(jù)庫 Redis
通過合理的數(shù)據(jù)結(jié)構(gòu)選擇和巧妙的應用,我們成功地解決了為 List、Set 和 Hash 結(jié)構(gòu)中的字段設(shè)置單獨過期時間的問題。

大家好,我是小?,一個漂泊江湖多年的 985 非科班程序員,曾混跡于國企、互聯(lián)網(wǎng)大廠和創(chuàng)業(yè)公司的后臺開發(fā)攻城獅。

1. 引言

1.1 消費隊列

這天,小?在購買火車票時,發(fā)現(xiàn)如果存在一個未支付的訂單時,就不能再進行購票了。如果把待支付的訂單放在一個隊列里面,那么隊列的長度就只能是 1.

正好最近用 Redis 比較多,于是,我突發(fā)奇想,如何用 Redis 原生的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)一個簡易版的延時消費隊列呢?

業(yè)務狀態(tài)圖如下:

圖片圖片

并且,需要保證隊列的長度是可控的,比如,我們只允許用戶有 3 個未支付的訂單。

1.2 Redis實現(xiàn)

Redis,作為一款高性能的緩存和數(shù)據(jù)存儲數(shù)據(jù)庫,一直以來都是后臺開發(fā)者的得力助手。

如果用 Redis 作為消費隊列,那么我們可以用到的數(shù)據(jù)結(jié)構(gòu)有:List、Hash 和 Set。在上述的業(yè)務場景中,由于我們只需要關(guān)注 orderId(訂單 ID),因此這三個數(shù)據(jù)結(jié)構(gòu)都是可用的。

比如,用 hash 來存儲時,我們可以將 key 設(shè)置為 UnpaidOrder-{userId},每個 field 都是一個訂單。

圖片圖片

但是,我們現(xiàn)在面臨一個挑戰(zhàn):每個訂單的存活時長是不同的,分為手動消費和定期刪除的邏輯。

  • 訂單 1 手動支付后,需要將 orderId1 從列表中刪除
  • 訂單 2 在半小時內(nèi)還未支付,就自動過期,用戶還可以繼續(xù)提交訂單到未支付狀態(tài)

所以在 List、Set 或者 Hash 結(jié)構(gòu)中,每個 field 都需要設(shè)置單獨的過期時間。

這是一個常見而又棘手的問題,本文將從互聯(lián)網(wǎng)業(yè)務中常見的解決方案入手,來深入探討一下 Redis 的底層實現(xiàn)。

2. 常見方案

在實際業(yè)務中,我們經(jīng)常會遇到這樣的場景:需要統(tǒng)計某些字段的個數(shù),并且這些字段的過期時間各有先后。

就上述場景而言,我們需要統(tǒng)計用戶的未支付訂單數(shù),但是每個訂單數(shù)的過期時間是不同的。

在這種情況下,我們需要在業(yè)務中手動刪除過期的字段,或者讓它們自動過期。

2.1 為單獨的 field 設(shè)置過期?

我們知道,Redis 里面暫時沒有接口給 List、Set 或者 Hash 的 field 單獨設(shè)置過期時間,只能給整個列表、集合或者 Hash 設(shè)置過期時間。

這樣,當 List/Set/Hash 過期時,里面的所有 field 元素就全部過期了。

但這樣并不滿足需求。

小?嘗試在網(wǎng)上找一些已知方案,其中有一個 Stack Overflow 的問題帖子和我面臨的很相似:

圖來源:StackOverflow,Redis 中如何給 HSET 的孩子key(指 field)設(shè)置過期時間?

接著,帖子下面的回答里無意看到了 Redis 作者的回答:

圖片圖片

中文翻譯如下:

嗨,這是不可能的,要么為該特定字段使用不同的頂級 key,要么與提交的字段一起存儲另一個具有過期時間的字段,然后同時獲取這兩個字段,并讓應用程序了解它是否仍然有效(基于當前時間)。

大意就是,不可能,除非你同時把 field 和過期時間都存下來,然后在程序里面判斷它是否過期。

這真是布袋里失火,很燒包!

2.2. 設(shè)置整體過期時間

既然 Redis 創(chuàng)始人都這么說了,Redis 是不可能為單獨的 field 設(shè)置過期時間,那我們首先考慮的就是給整個 List/Set/Hash 設(shè)置過期時間。

這樣的做法簡單粗暴,但卻很難滿足每個字段單獨設(shè)置過期時間的需求。

于是,我思前想后,既然每個訂單的過期時間不一樣,那我們是否可以根據(jù)時間來創(chuàng)建不同的集合,將同一時間過期的訂單放在同一個集合里面:

圖片圖片

然后,分別為不同的集合設(shè)置 TTL,當訂單過期未支付時,訂單會隨著集合的過期而在同一分鐘內(nèi)被刪除。

但是這樣的問題是,每次新增訂單時,都得把過去 30 分鐘的集合全部遍歷一遍,查詢是否有該用戶的訂單,再判斷用戶的未支付訂單數(shù)有沒有超量。

并且,以分鐘創(chuàng)建集合,可能存在一個問題:用戶的訂單本來在 01 秒就過期了,但是在 59 秒才被刪除。

如果以秒來創(chuàng)建集合,30 分鐘又需要創(chuàng)建 1800 個集合,就更難管理了,所以對集合設(shè)置整體過期時間不太可行。

那有沒有更優(yōu)雅的實現(xiàn)方式呢?

2.3 zset 結(jié)合 score實現(xiàn)

當然是有的!

Redis 除了常用的 List/Set/Hash 結(jié)構(gòu),它還有一個專門用來排序的數(shù)據(jù)結(jié)構(gòu) zset(即 Sorted Set,排序集合)。

而基于 Redis 的 Zset 結(jié)構(gòu),可以通過 Score 來表示過期時間,我們可以輕松地實現(xiàn)每個 Field 的單獨過期。

圖片圖片

具體實現(xiàn)為:

  1. 每當新增一個待支付訂單,就將當前時間的 Unix timestamp 加上過期時間 30min 作為 score 設(shè)置到這個元素上,這樣,sorted set 會根據(jù)這個過期時間戳對元素排序存儲;
  2. 當訂單被支付后,根據(jù) userId 和 orderId 去刪除 sorted set 里的待支付訂單;
  3. 同時,在程序里新增一個定時任務,每隔一秒去刪除當前時間已過期的訂單。

2.4 底層實現(xiàn)

用 Redis 的 zset 一方面可以很方便地實現(xiàn)了對每個字段的單獨過期,不再受整個 Key 的過期時間限制,提高了靈活性。

另一方面,Redis 的 zset 操作是十分高效的,不會給系統(tǒng)帶來顯著的性能壓力。

這得益于 zset 底層的數(shù)據(jù)結(jié)構(gòu),Zset 底層實現(xiàn)采用了 ZipList(壓縮列表)和 SkipList(跳表)兩種實現(xiàn)方式,當滿足:

  • Zset 中保存的元素個數(shù)小于 128(可通過修改 zset-max-ziplist-entries 配置來修改)
  • Zset 中保存的所有元素長度小于 64byte(通過修改 zset-max-ziplist-values 配置來修改)

兩個條件時,Zset 采用 ZipList 實現(xiàn);否則,用 SkipList 實現(xiàn)。

ZipList 實現(xiàn)

圖片圖片

ZipList 是一個數(shù)組的形式,存儲數(shù)據(jù)時分為列表頭部分和數(shù)據(jù)部分,列表頭部分有 3 個元素:

  • zlbytes:表示當前 list 的存儲元素的總長度
  • zllen:表示當前 list 存儲的元素的個數(shù)
  • zltail:表示當前 list 的頭結(jié)點的地址,通過 zltail 就是可以實現(xiàn) list 的遍歷

數(shù)據(jù)部分以鍵值對的方式依次排列,鍵存儲的是實際 member,值存儲的是 member 對應的分值(score)。

SkipList 實現(xiàn)

圖片圖片

SkipList 分為兩部分:

  1. dict 部分是由字典實現(xiàn)(其實就是 HashMap,里面放了成員到 score 的映射);
  2. zset 部分使用跳躍表實現(xiàn)(存放了所有的成員,解決了 HashMap 中 key 無序的問題)。

從圖中可以看出,dict 和 zset 都存儲數(shù)據(jù)。

但實際上 dict 和 zset 最終使用的指針都指向了同一份成員數(shù)據(jù),即數(shù)據(jù)是被兩部分共享的,為了方便表達將同一份數(shù)據(jù)展示在兩個地方。

2.5 代碼實現(xiàn)

當我們插入一個過期時間到 zset 時,Redis 會自動幫我們排好序,我們只需要在程序中新增一個定時任務,比如:每秒執(zhí)行一次刪除任務,刪除時間戳從 0 到當前時間戳的 score 值即可。

偽代碼如下:

# 1. 創(chuàng)建新的待支付訂單時,查詢zset個數(shù)
count = zcard UnpaidOrder-{userId}

# 2. 判斷未支付訂單個數(shù)
if count >= 3:
    return

# 3. 新增訂單
zadd UnpaidOrder-{userId} redis.Z{Score: {timestamp1}, Member: {order1}}

# 4.1 訂單支付后,從 set 中刪除未支付訂單
zrem UnpaidOrder-{userId} order1

# 4.2 過期時間到了,從 set 中刪除未支付訂單
zremrange UnpaidOrder-{userId} 0 {current_timestamp}

3. 結(jié)語

通過合理的數(shù)據(jù)結(jié)構(gòu)選擇和巧妙的應用,我們成功地解決了為 List、Set 和 Hash 結(jié)構(gòu)中的字段設(shè)置單獨過期時間的問題。

這個方案在實際項目中得到了驗證,并取得了顯著的效果。對比其它的延時隊列,或者 etcd 的 field 過期方案,Redis 的實現(xiàn)相對而言更為便捷,理解起來也更為簡單。

希望這個方案能夠在你的項目中派上用場,提高開發(fā)效率,更好地應對實際需求。如果你有更多關(guān)于 Redis 使用的問題,也歡迎在評論區(qū)交流討論。

愿你在 Redis 的世界里愈發(fā)游刃有余,取得更多技術(shù)的新突破。

責任編輯:武曉燕 來源: xin猿意碼
相關(guān)推薦

2020-06-11 19:00:24

Redis數(shù)據(jù)庫

2021-12-13 09:02:13

localStorag面試前端

2010-01-22 10:53:04

C++堆棧

2024-12-25 10:24:31

2023-05-15 20:11:34

2012-11-14 10:27:26

2020-10-22 09:09:41

Python數(shù)據(jù)科學代碼

2023-03-14 11:00:05

過期策略Redis

2021-02-22 09:23:55

LRU時間HashMap

2013-09-11 09:49:18

Java數(shù)組集合

2025-01-21 08:10:00

2017-10-11 09:15:07

Windows 應用

2021-09-28 09:36:13

redisHash結(jié)構(gòu)

2018-10-18 08:00:00

Redis Enter數(shù)據(jù)庫Docker

2020-01-16 10:20:45

piwheels樹莓派Linux

2019-07-15 09:09:29

RedisJava操作系統(tǒng)

2020-08-06 07:49:57

List元素集合

2021-01-21 07:35:40

JenkinsUICSS

2012-12-31 13:13:13

App出售

2019-08-16 09:07:47

RedisNoSQL數(shù)據(jù)庫
點贊
收藏

51CTO技術(shù)棧公眾號