Redis中原子性操作的的實(shí)現(xiàn)
一、Redis 原子性操作的本質(zhì):為什么 Redis 能保證原子性?
首先需要明確一個(gè)關(guān)鍵概念:Redis 的原子性是指單個(gè)命令的執(zhí)行是 "不可中斷" 的—— 當(dāng)一個(gè)命令開始執(zhí)行后,直到其執(zhí)行完畢,Redis 不會(huì)中斷它去執(zhí)行其他命令。這種特性并非 Redis 獨(dú)創(chuàng),而是基于其單線程模型的天然優(yōu)勢。
1.1 底層原理:單線程模型 + 命令隊(duì)列
Redis 采用單線程事件循環(huán)模型處理客戶端請求,這種設(shè)計(jì)架構(gòu)主要由以下幾個(gè)關(guān)鍵組件構(gòu)成:
- I/O 多路復(fù)用:Redis 使用 epoll/kqueue/select 等系統(tǒng)調(diào)用來高效處理大量網(wǎng)絡(luò)連接
- 命令隊(duì)列:所有客戶端請求都會(huì)被序列化到一個(gè)全局內(nèi)存隊(duì)列中
- 單線程事件循環(huán):主線程按 "先進(jìn)先出(FIFO)" 的順序從隊(duì)列中取出命令執(zhí)行
這種設(shè)計(jì)從根本上保證了:
- 命令執(zhí)行的獨(dú)占性:每個(gè)命令在執(zhí)行期間獨(dú)占 CPU 資源
- 狀態(tài)一致性:命令執(zhí)行的結(jié)果不會(huì)出現(xiàn) "部分完成" 的中間狀態(tài)
- 操作完整性:完整的操作序列不會(huì)被其他命令打斷
典型應(yīng)用場景示例: 當(dāng)執(zhí)行 INCR key 命令時(shí),Redis 會(huì)嚴(yán)格按照以下順序完整執(zhí)行:
- 從內(nèi)存中讀取 key 的當(dāng)前值(假設(shè)為 5)
- 在 CPU 寄存器中執(zhí)行加 1 操作(5 → 6)
- 將新值(6)寫回內(nèi)存
- 返回結(jié)果給客戶端
在此期間,即使有 100 個(gè)客戶端同時(shí)發(fā)送 INCR 命令,Redis 也會(huì)將它們排隊(duì)處理,確保每個(gè) INCR 操作都能正確累加。
1.2 原子性的邊界:單個(gè)命令 vs 多個(gè)命令
需要特別注意的是:Redis 僅保證 "單個(gè)命令" 的原子性,多個(gè)命令的組合并不天然具備原子性。理解這一點(diǎn)對設(shè)計(jì)可靠的 Redis 應(yīng)用至關(guān)重要。
典型問題示例
# 以下兩個(gè)命令組合不具備原子性 GET key1 # 步驟1:讀取key1 SET key2 value2 # 步驟2:寫入key2
潛在風(fēng)險(xiǎn)場景:
- 客戶端A執(zhí)行
GET key1獲取值為 100 - 此時(shí)客戶端B修改了 key1 的值為 200
- 客戶端A繼續(xù)執(zhí)行
SET key2 value2 - 結(jié)果:客戶端A基于已過期的 key1 值做出了錯(cuò)誤決策
解決方案對比
| 方案 | 實(shí)現(xiàn)方式 | 適用場景 | 性能影響 |
|---|---|---|---|
| 事務(wù)(MULTI/EXEC) | 將多個(gè)命令打包執(zhí)行 | 簡單的命令組合 | 中等,需要排隊(duì) |
| Lua腳本 | 原子執(zhí)行復(fù)雜邏輯 | 需要條件判斷的業(yè)務(wù) | 較高,需要解析腳本 |
| WATCH | 樂觀鎖機(jī)制 | 需要檢測變化的場景 | 較高,可能重試 |
Lua 腳本示例:
-- 原子性地檢查并設(shè)置值
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("SET", KEYS[2], ARGV[2])
else
return 0
end
最佳實(shí)踐建議:
- 對于簡單的計(jì)數(shù)器場景,優(yōu)先使用原生原子命令(INCR/DECR 等)
- 需要組合不超過5個(gè)命令時(shí),使用 MULTI/EXEC 事務(wù)
- 復(fù)雜業(yè)務(wù)邏輯(包含條件判斷)必須使用 Lua 腳本
- 對性能敏感的場景,提前測試不同方案的 QPS 表現(xiàn)
二、Redis 核心原子操作分類與實(shí)踐
2.1 基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)的原子操作
數(shù)據(jù)結(jié)構(gòu)詳解與擴(kuò)展應(yīng)用場景
String類型
SETNX命令擴(kuò)展應(yīng)用:- 實(shí)現(xiàn)分布式鎖的基礎(chǔ)原語
- 用戶首次登錄初始化配置
- 防止緩存擊穿(當(dāng)緩存失效時(shí),只允許一個(gè)請求去查詢數(shù)據(jù)庫)
GETSET典型使用場景:- 系統(tǒng)維護(hù)狀態(tài)切換(獲取當(dāng)前狀態(tài)并更新為新狀態(tài))
- 實(shí)現(xiàn)簡單的消息隊(duì)列(配合
LPUSH使用)
Hash類型
HSET高級(jí)用法:- 用戶會(huì)話管理(存儲(chǔ)多個(gè)會(huì)話屬性)
- 商品詳情緩存(避免序列化/反序列化整個(gè)對象)
HINCRBY實(shí)際案例:- 電商平臺(tái)商品庫存扣減(保證庫存準(zhǔn)確性)
- 論壇帖子點(diǎn)贊計(jì)數(shù)
List類型
- 高級(jí)隊(duì)列模式:
- 阻塞式隊(duì)列(
BLPOP/BRPOP) - 循環(huán)隊(duì)列(
LINDEX+LPUSH)
- 阻塞式隊(duì)列(
- 典型應(yīng)用:
- 最新消息展示(固定長度列表)
- 任務(wù)調(diào)度系統(tǒng)
- 高級(jí)隊(duì)列模式:
Set類型
- 擴(kuò)展功能:
- 共同好友計(jì)算(
SINTER) - 數(shù)據(jù)去重處理
- 共同好友計(jì)算(
- 實(shí)際案例:
- 用戶標(biāo)簽系統(tǒng)
- 抽獎(jiǎng)活動(dòng)參與者管理
- 擴(kuò)展功能:
ZSet類型
- 高級(jí)應(yīng)用:
- 延遲隊(duì)列(使用時(shí)間戳作為score)
- 熱點(diǎn)數(shù)據(jù)統(tǒng)計(jì)
- 典型場景:
- 游戲排行榜
- 優(yōu)先級(jí)任務(wù)調(diào)度
- 高級(jí)應(yīng)用:
用戶登錄狀態(tài)存儲(chǔ)的進(jìn)階實(shí)現(xiàn)
// 高級(jí)登錄狀態(tài)管理
public boolean setLoginStatus(String userId, String deviceId) {
String key = "user:" + userId + ":session";
String value = deviceId + ":" + System.currentTimeMillis();
// 使用SET命令的完整參數(shù)
String result = jedis.set(key, value,
"NX", // 僅當(dāng)key不存在時(shí)設(shè)置
"EX", // 設(shè)置過期時(shí)間單位秒
3600, // 1小時(shí)過期
"GET" // 返回舊值(如果存在)
);
if (result != null) {
// 處理舊設(shè)備踢出邏輯
handleOldDeviceLogout(result);
}
return "OK".equals(result);
}
2.2 計(jì)數(shù)器與自增操作
INCR系列命令的底層原理
Redis實(shí)現(xiàn)原子自增的方式:
- 單線程模型保證命令串行執(zhí)行
- 內(nèi)存操作避免磁盤I/O延遲
- 特殊編碼優(yōu)化(當(dāng)值較小時(shí)使用更緊湊的存儲(chǔ)格式)
計(jì)數(shù)器的高級(jí)應(yīng)用模式
滑動(dòng)窗口限流
-- Lua腳本實(shí)現(xiàn)滑動(dòng)窗口限流 local current_time = redis.call('TIME')[1] local window_size = 60 local max_requests = 100 local key = KEYS[1] -- 清除過期記錄 redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window_size) -- 獲取當(dāng)前請求數(shù) local count = redis.call('ZCARD', key) if count >= tonumber(ARGV[1]) then return 0 end -- 添加當(dāng)前請求記錄 redis.call('ZADD', key, current_time, current_time..math.random()) redis.call('EXPIRE', key, window_size) return 1分布式ID生成器
// Twitter的Snowflake算法變種實(shí)現(xiàn) public Long generateId(String bizType) { String key = "id_generator:" + bizType; long timestamp = System.currentTimeMillis(); // 獲取序列號(hào)并自增 long sequence = jedis.incr(key); jedis.expire(key, 3600); return ((timestamp - 1288834974657L) << 22) | (datacenterId << 17) | (workerId << 12) | (sequence % 4096); }精確計(jì)數(shù)與基數(shù)統(tǒng)計(jì)
- 小數(shù)據(jù)量:直接使用INCR
- 大數(shù)據(jù)量:結(jié)合HyperLogLog進(jìn)行基數(shù)估算
2.3 分布式鎖的完整實(shí)現(xiàn)方案
分布式鎖的演進(jìn)過程
基礎(chǔ)版本
SET lock:resource unique_value NX EX 30
改進(jìn)版本(解決鎖續(xù)期問題)
// 加鎖 String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); // 啟動(dòng)守護(hù)線程定期續(xù)期 new Thread(() -> { while (locked) { jedis.expire(lockKey, expireTime/1000); Thread.sleep(expireTime/3); } }).start();Redlock算法實(shí)現(xiàn)
// 多節(jié)點(diǎn)加鎖 List<Jedis> jedisList = getRedisNodes(); int successCount = 0; long startTime = System.currentTimeMillis(); for (Jedis jedis : jedisList) { if (jedis.set(lockKey, value, "NX", "PX", expireTime) != null) { successCount++; } } // 檢查是否在大多數(shù)節(jié)點(diǎn)上加鎖成功 boolean locked = successCount >= (jedisList.size()/2 + 1);
生產(chǎn)環(huán)境最佳實(shí)踐
鎖粒度控制
- 細(xì)粒度鎖:按業(yè)務(wù)ID拆分(如order:123)
- 粗粒度鎖:全局資源保護(hù)
異常處理
try { if (acquireLock()) { // 業(yè)務(wù)邏輯 } } finally { // 確保釋放鎖 releaseLock(); }性能優(yōu)化
- 避免長時(shí)間持有鎖
- 使用tryLock模式(帶超時(shí))
- 鎖分段技術(shù)提升并發(fā)
鎖監(jiān)控
# 監(jiān)控鎖狀態(tài) redis-cli --latency -h 127.0.0.1 -p 6379 redis-cli slowlog get
集群環(huán)境特殊考量
主從切換問題
- 使用Redlock算法
- 監(jiān)控主從同步延遲
多數(shù)據(jù)中心部署
- 跨機(jī)房延遲評估
- 本地緩存與分布式鎖結(jié)合
鎖服務(wù)降級(jí)方案
- 本地鎖降級(jí)
- 樂觀鎖替代
- 熔斷機(jī)制
三、多命令原子性實(shí)現(xiàn):事務(wù)與 Lua 腳本
當(dāng)需要多個(gè)命令組合實(shí)現(xiàn)原子性時(shí),Redis 提供了兩種方案:MULTI/EXEC事務(wù)和Lua 腳本。下面對比兩者的差異與適用場景。
3.1 MULTI/EXEC 事務(wù):弱一致性的批量執(zhí)行
Redis 事務(wù)并非傳統(tǒng)數(shù)據(jù)庫的 ACID 事務(wù),其核心特性是 "批量執(zhí)行 + 要么全部執(zhí)行,要么全部不執(zhí)行"(但不支持回滾)。
事務(wù)執(zhí)行流程詳解
- MULTI:標(biāo)記事務(wù)開始,后續(xù)命令進(jìn)入隊(duì)列
- 命令入隊(duì):所有操作命令不會(huì)被立即執(zhí)行,而是返回"QUEUED"狀態(tài)
- EXEC:執(zhí)行所有隊(duì)列中的命令
- DISCARD:可選操作,用于取消事務(wù)
127.0.0.1:6379> MULTI # 開啟事務(wù) OK 127.0.0.1:6379> INCR counter:user # 命令1:用戶數(shù)+1 QUEUED # 命令入隊(duì),未執(zhí)行 127.0.0.1:6379> SET user:1002:status "active" # 命令2:設(shè)置用戶狀態(tài) QUEUED 127.0.0.1:6379> EXEC # 執(zhí)行事務(wù),所有命令原子性執(zhí)行 1) (integer) 101 # INCR命令結(jié)果 2) OK # SET命令結(jié)果
事務(wù)的局限性詳解
不支持回滾:
- 語法錯(cuò)誤:事務(wù)中某個(gè)命令語法錯(cuò)誤(如錯(cuò)誤的命令名),整個(gè)事務(wù)都不會(huì)執(zhí)行
- 運(yùn)行時(shí)錯(cuò)誤:如對字符串執(zhí)行INCR操作,錯(cuò)誤命令會(huì)失敗,但其他命令仍會(huì)執(zhí)行
弱隔離性:
- 事務(wù)執(zhí)行期間會(huì)阻塞其他客戶端命令
- 但事務(wù)內(nèi)的命令是"非原子性入隊(duì)"的(即入隊(duì)時(shí)不執(zhí)行,執(zhí)行時(shí)才獲取數(shù)據(jù))
- 可能出現(xiàn)"WATCH"失效問題
無法處理并發(fā)沖突:
- 沒有類似數(shù)據(jù)庫的樂觀鎖機(jī)制
- 兩個(gè)事務(wù)同時(shí)修改同一key時(shí),后執(zhí)行的會(huì)覆蓋先執(zhí)行的結(jié)果
適用場景
- 需要批量執(zhí)行多個(gè)命令,且不要求嚴(yán)格的事務(wù)隔離性
- 簡單的計(jì)數(shù)器更新、狀態(tài)標(biāo)記等場景
- 配合WATCH實(shí)現(xiàn)簡單的樂觀鎖控制
3.2 Lua 腳本:強(qiáng)一致性的原子執(zhí)行
Redis 支持通過 Lua 腳本執(zhí)行自定義邏輯,且整個(gè) Lua 腳本的執(zhí)行過程是原子性的—— 腳本執(zhí)行期間,Redis 不會(huì)中斷或執(zhí)行其他命令。這使得 Lua 腳本成為實(shí)現(xiàn)復(fù)雜原子邏輯的最佳選擇。
Lua 腳本的核心優(yōu)勢
完整的原子性:
- 腳本作為一個(gè)整體執(zhí)行,不會(huì)被其他命令打斷
- 所有操作要么全部成功,要么全部失敗
豐富的邏輯控制:
- 支持條件判斷(if...else)
- 支持循環(huán)(for/while)
- 支持變量和復(fù)雜計(jì)算
網(wǎng)絡(luò)效率高:
- 多個(gè)命令打包成腳本,只需一次網(wǎng)絡(luò)往返
- 特別適合高延遲環(huán)境
實(shí)踐案例:庫存扣減(避免超賣)
某商品庫存初始值為 100,需實(shí)現(xiàn) "用戶下單時(shí)原子性扣減庫存,庫存不足時(shí)返回失敗":
-- Lua腳本:KEYS[1]為庫存key,ARGV[1]為扣減數(shù)量
local stock = redis.call('get', KEYS[1])
if not stock or tonumber(stock) < tonumber(ARGV[1]) then
return 0 # 庫存不足,扣減失敗
end
return redis.call('decrby', KEYS[1], ARGV[1]) # 原子性扣減庫存
Java調(diào)用示例:
String luaScript = "local stock = redis.call('get', KEYS[1])\n" +
"if not stock or tonumber(stock) < tonumber(ARGV[1]) then\n" +
" return 0\n" +
"end\n" +
"return redis.call('decrby', KEYS[1], ARGV[1])";
List<String> keys = Collections.singletonList("stock:goods:1001");
List<String> args = Collections.singletonList("1");
// 執(zhí)行腳本
Long result = (Long) jedis.eval(luaScript, keys, args);
if (result == 0) {
System.out.println("庫存不足");
} else {
System.out.println("庫存扣減成功,剩余庫存:" + result);
}
腳本緩存優(yōu)化
Redis會(huì)緩存執(zhí)行過的腳本(通過SHA1校驗(yàn)和),后續(xù)可通過evalsha調(diào)用:
# 首次執(zhí)行
127.0.0.1:6379> script load "return redis.call('get', KEYS[1])"
"a5a06e6a8a4b4a5a5a5a5a5a5a5a5a5a5a5a5a5"
# 后續(xù)執(zhí)行
127.0.0.1:6379> evalsha a5a06e6a8a4b4a5a5a5a5a5a5a5a5a5a5a5a5 1 mykey
"value"
適用場景
- 需要嚴(yán)格原子性的復(fù)雜操作(如庫存扣減、秒殺)
- 需要條件判斷的多步驟操作
- 高頻操作需要減少網(wǎng)絡(luò)開銷的場景
- 分布式鎖的實(shí)現(xiàn)(包含鎖的獲取、續(xù)期和釋放)
注意事項(xiàng)
- 腳本執(zhí)行時(shí)間不宜過長(默認(rèn)5秒超時(shí))
- 避免在腳本中執(zhí)行耗時(shí)操作
- 腳本應(yīng)保持簡單,避免復(fù)雜計(jì)算
四、Redis 原子操作的常見問題與避坑指南
即使掌握了原子操作的用法,在實(shí)際開發(fā)中仍可能因細(xì)節(jié)處理不當(dāng)導(dǎo)致問題。下面總結(jié) 4 個(gè)高頻坑點(diǎn)及解決方案,并提供具體優(yōu)化建議。
4.1 坑點(diǎn) 1:混淆 "單命令原子性" 與 "多命令原子性"
問題現(xiàn)象: 在電商秒殺場景中,開發(fā)者錯(cuò)誤地認(rèn)為多個(gè)獨(dú)立命令的組合具有原子性。例如以下庫存扣減邏輯:
# 錯(cuò)誤示例:判斷庫存>0后扣減(非原子操作)
if redis.call('get', 'stock:1001') > 0 then
redis.call('decr', 'stock:1001') # 可能出現(xiàn)并發(fā)時(shí)庫存為負(fù)
end
問題原因:
get和decr是兩個(gè)獨(dú)立命令,中間可能插入其他請求- 在高并發(fā)場景下,多個(gè)請求可能同時(shí)判斷庫存為正,導(dǎo)致"超賣"現(xiàn)象
解決方案:
- 使用 Lua 腳本將"判斷+扣減"封裝為原子操作(完整示例見3.2節(jié))
- 或者直接使用
DECR命令的返回值判斷(返回減后的值,若為負(fù)則不允許扣減)
4.2 坑點(diǎn) 2:分布式鎖未設(shè)置過期時(shí)間
典型場景: 在分布式任務(wù)調(diào)度系統(tǒng)中,使用Redis實(shí)現(xiàn)分布式鎖時(shí)出現(xiàn)以下問題:
# 錯(cuò)誤加鎖方式(未設(shè)置過期時(shí)間) SET lock:order_123 true NX
風(fēng)險(xiǎn)分析:
- 若客戶端崩潰或網(wǎng)絡(luò)異常,鎖將永遠(yuǎn)無法釋放
- 其他客戶端將無法獲取鎖,導(dǎo)致系統(tǒng)死鎖
- 需要人工介入刪除key才能恢復(fù)
最佳實(shí)踐:
- 必須使用帶過期時(shí)間的加鎖命令:
SET lock:order_123 true NX EX 10
- 過期時(shí)間設(shè)置原則:
- 大于業(yè)務(wù)執(zhí)行的最大耗時(shí)(如業(yè)務(wù)最多執(zhí)行5秒,設(shè)10秒)
- 建議設(shè)置自動(dòng)續(xù)期機(jī)制(如Redisson的watchdog)
- 配合唯一標(biāo)識(shí)實(shí)現(xiàn)安全解鎖:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
4.3 坑點(diǎn) 3:Lua 腳本執(zhí)行效率低下
性能問題案例: 某社交平臺(tái)在用戶Feed流處理腳本中,包含以下低效操作:
-- 低效腳本示例:遍歷所有粉絲進(jìn)行計(jì)數(shù)
local followers = redis.call('SMEMBERS', 'user:'..userId..':followers')
local count = 0
for i, follower in ipairs(followers) do
count = count + redis.call('SCARD', 'user:'..follower..':posts')
end
return count
影響分析:
- Redis單線程模型下,腳本執(zhí)行會(huì)阻塞其他命令
- 當(dāng)粉絲量達(dá)百萬級(jí)時(shí),腳本執(zhí)行可能超過1秒
- 導(dǎo)致Redis整體吞吐量下降,QPS驟降
優(yōu)化建議:
- 腳本優(yōu)化原則:
- 避免大數(shù)據(jù)集遍歷(改用SCAN分批處理)
- 復(fù)雜計(jì)算移到客戶端(如排序、聚合)
- 單個(gè)腳本執(zhí)行時(shí)間控制在10ms內(nèi)
- 改進(jìn)方案:
- 使用Redis的
SCARD命令直接獲取集合基數(shù) - 或改用客戶端分批查詢后聚合
- 使用Redis的
4.4 坑點(diǎn) 4:使用 INCR 實(shí)現(xiàn)分布式 ID 時(shí)的溢出問題
問題背景: 某物聯(lián)網(wǎng)平臺(tái)使用Redis生成設(shè)備ID:
INCR device:id_counter
潛在風(fēng)險(xiǎn):
- Redis計(jì)數(shù)器最大值為2^63-1(約9e18)
- 假設(shè)每天生成1億ID,約需2.5億年才會(huì)溢出
- 但某些高頻場景(如日志ID)可能快速耗盡
解決方案:
- 組合ID生成方案:
# 時(shí)間戳(41bit) + 機(jī)器ID(10bit) + 序列號(hào)(12bit) INCR id:20230101 # 每日重置計(jì)數(shù)器
- 定期重置機(jī)制:
EXPIRE id_counter 86400 # 每日自動(dòng)過期
- 分片方案:
INCR id_counter:{shard1} # 按業(yè)務(wù)分片使用不同key
監(jiān)控建議:
- 對關(guān)鍵計(jì)數(shù)器設(shè)置監(jiān)控告警
- 當(dāng)計(jì)數(shù)值超過閾值時(shí)自動(dòng)告警
- 定期檢查計(jì)數(shù)器增長趨勢
到此這篇關(guān)于Redis 的原子性操作的文章就介紹到這了,更多相關(guān)Redis 原子性操作內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis實(shí)現(xiàn)集群搭建+集群讀寫的示例
本文介紹了Redis集群的搭建和讀寫操作,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
Redisson之lock()和tryLock()的區(qū)別及說明
這篇文章主要介紹了Redisson之lock()和tryLock()的區(qū)別及說明,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-12-12
Redis持久化方式之RDB和AOF的原理及優(yōu)缺點(diǎn)
在Redis中,數(shù)據(jù)可以分為兩類,即內(nèi)存數(shù)據(jù)和磁盤數(shù)據(jù),Redis?提供了兩種不同的持久化方式,其中?RDB?是快照備份機(jī)制,AOF?則是追加寫操作機(jī)制,本文將詳細(xì)給大家介紹Redis?持久化方式RDB和AOF的原理及優(yōu)缺點(diǎn),感興趣的同學(xué)可以跟著小編一起來學(xué)習(xí)2023-06-06

