通過(guò)lua實(shí)現(xiàn)redis 分布式鎖的項(xiàng)目實(shí)踐
在 Redis 中使用 Lua 腳本實(shí)現(xiàn)分布式鎖 是一種高效、原子、安全的方式。由于 Lua 腳本在 Redis 中是單線程、原子執(zhí)行的,可以避免多個(gè)客戶(hù)端在加鎖/解鎖時(shí)出現(xiàn)競(jìng)態(tài)條件(Race Condition)。
下面將為你提供一個(gè) 完整、生產(chǎn)可用的 Redis 分布式鎖 Lua 實(shí)現(xiàn)方案,包含:
- 加鎖(lock)
- 解鎖(unlock)
- 自動(dòng)續(xù)期(可選)
- 防止誤刪他人鎖
- 支持可重入(可選)
? 一、核心設(shè)計(jì)原則
- 鎖的唯一標(biāo)識(shí):每個(gè)客戶(hù)端使用唯一的
value(如 UUID)作為鎖標(biāo)識(shí),防止 A 客戶(hù)端釋放 B 客戶(hù)端的鎖。 - 原子性操作:加鎖和解鎖必須通過(guò) Lua 腳本保證原子性。
- 自動(dòng)過(guò)期:設(shè)置 TTL(如 30 秒),防止死鎖。
- 可重入支持(可選) :同一個(gè)客戶(hù)端可多次加鎖(需記錄重入次數(shù))。
本文先實(shí)現(xiàn) 基礎(chǔ)版(不可重入) ,再給出 可重入增強(qiáng)版。
? 二、基礎(chǔ)版:不可重入分布式鎖
?? 1. 加鎖腳本(lock.lua)
-- KEYS[1] = 鎖的 key(如 "my_lock")
-- ARGV[1] = 鎖的 value(唯一標(biāo)識(shí),如 UUID)
-- ARGV[2] = 過(guò)期時(shí)間(毫秒,如 "30000")
local key = KEYS[1]
local value = ARGV[1]
local ttl = tonumber(ARGV[2])
-- 嘗試加鎖:只有當(dāng) key 不存在時(shí)才設(shè)置成功
if redis.call("SET", key, value, "NX", "PX", ttl) then
return 1 -- 加鎖成功
else
return 0 -- 加鎖失?。ㄒ驯徽加茫?
end
? SET key value NX PX ttl 是 Redis 2.6.12+ 的原子命令:
- NX:僅當(dāng) key 不存在時(shí)設(shè)置
- PX:設(shè)置過(guò)期時(shí)間(毫秒)
?? 2. 解鎖腳本(unlock.lua)
-- KEYS[1] = 鎖的 key
-- ARGV[1] = 鎖的 value(用于驗(yàn)證是否是自己的鎖)
local key = KEYS[1]
local value = ARGV[1]
-- 獲取當(dāng)前鎖的 value
local current_value = redis.call("GET", key)
-- 如果存在且等于自己的 value,則刪除
if current值 == value then
redis.call("DEL", key)
return 1 -- 解鎖成功
else
return 0 -- 不是自己的鎖,拒絕刪除
end
?? 注意:不能直接 DEL,否則可能誤刪別人的鎖!
? 三、Python 示例(使用 redis-py)
import redis
import uuid
import time
# 初始化 Redis 連接
r = redis.StrictRedis(host='localhost', port=6379, decode_responses=True)
# 加載 Lua 腳本(推薦使用 script_load + evalsha 提高性能)
lock_script = r.register_script("""
if redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2]) then
return 1
else
return 0
end
""")
unlock_script = r.register_script("""
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
""")
def acquire_lock(lock_name, acquire_timeout=10, lock_timeout=30):
"""獲取分布式鎖"""
identifier = str(uuid.uuid4())
lock_timeout_ms = int(lock_timeout * 1000)
end_time = time.time() + acquire_timeout
while time.time() < end_time:
# 執(zhí)行 Lua 腳本嘗試加鎖
if lock_script(keys=[lock_name], args=[identifier, lock_timeout_ms]):
return identifier # 返回鎖標(biāo)識(shí)
time.sleep(0.01) # 短暫等待后重試
return False
def release_lock(lock_name, identifier):
"""釋放分布式鎖"""
unlock_script(keys=[lock_name], args=[identifier])
# ===== 使用示例 =====
lock_key = "order:123:lock"
# 嘗試獲取鎖
lock_id = acquire_lock(lock_key, acquire_timeout=5, lock_timeout=30)
if lock_id:
try:
print("? 獲取鎖成功,執(zhí)行業(yè)務(wù)邏輯...")
time.sleep(5) # 模擬業(yè)務(wù)處理
finally:
# 釋放鎖
release_lock(lock_key, lock_id)
print("?? 鎖已釋放")
else:
print("? 獲取鎖超時(shí)")
? 四、增強(qiáng)版:支持可重入鎖(Reentrant Lock)
適用于同一線程/協(xié)程需要多次進(jìn)入臨界區(qū)的場(chǎng)景。
設(shè)計(jì)思路:
使用 Hash 結(jié)構(gòu) 存儲(chǔ)鎖:
- field = 客戶(hù)端 ID(UUID)
- value = 重入次數(shù)(counter)
設(shè)置整體 TTL
?? 可重入加鎖腳本(reentrant_lock.lua)
-- KEYS[1] = 鎖 key
-- ARGV[1] = 客戶(hù)端 ID
-- ARGV[2] = 鎖過(guò)期時(shí)間(毫秒)
local key = KEYS[1]
local client_id = ARGV[1]
local ttl = tonumber(ARGV[2])
-- 檢查是否已持有鎖
local current_client = redis.call("HGET", key, client_id)
if current_client then
-- 已持有:重入次數(shù) +1,刷新 TTL
redis.call("HINCRBY", key, client_id, 1)
redis.call("PEXPIRE", key, ttl)
return 1
else
-- 未持有:嘗試獲取鎖
if redis.call("HLEN", key) == 0 then
-- 鎖空閑,創(chuàng)建
redis.call("HMSET", key, client_id, 1)
redis.call("PEXPIRE", key, ttl)
return 1
else
-- 鎖被他人持有
return 0
end
end
?? 可重入解鎖腳本(reentrant_unlock.lua)
-- KEYS[1] = 鎖 key
-- ARGV[1] = 客戶(hù)端 ID
local key = KEYS[1]
local client_id = ARGV[1]
-- 檢查是否持有鎖
local counter = redis.call("HGET", key, client_id)
if not counter then
return 0 -- 未持有鎖
end
counter = tonumber(counter)
if counter > 1 then
-- 重入次數(shù) >1,減1
redis.call("HINCRBY", key, client_id, -1)
return 1
else
-- 最后一次,刪除整個(gè) key
redis.call("DEL", key)
return 1
end
?? 使用方式與基礎(chǔ)版類(lèi)似,只需替換 Lua 腳本。
? 五、注意事項(xiàng)與最佳實(shí)踐
| 問(wèn)題 | 解決方案 |
|---|---|
| 鎖過(guò)期但業(yè)務(wù)未完成 | 使用“看門(mén)狗”(Watchdog)線程自動(dòng)續(xù)期(如 Redisson 的機(jī)制) |
| 主從切換導(dǎo)致鎖丟失 | 使用 Redlock 算法(多 Redis 實(shí)例),但爭(zhēng)議較大;更推薦強(qiáng)一致存儲(chǔ)(如 etcd) |
| Lua 腳本調(diào)試?yán)щy | 在開(kāi)發(fā)環(huán)境打印日志(redis.log(redis.LOG_WARNING, "msg")) |
| 性能優(yōu)化 | 使用 SCRIPT LOAD + EVALSHA 避免每次傳輸腳本 |
?? 重要提醒:
Redis 分布式鎖 不保證絕對(duì)安全(尤其在主從異步復(fù)制場(chǎng)景)。
對(duì)一致性要求極高的場(chǎng)景(如金融),建議使用 etcd / ZooKeeper。
? 六、總結(jié)
| 功能 | 基礎(chǔ)版 | 可重入版 |
|---|---|---|
| 原子加鎖 | ? | ? |
| 防止誤刪 | ? | ? |
| 自動(dòng)過(guò)期 | ? | ? |
| 同客戶(hù)端多次加鎖 | ? | ? |
| 復(fù)雜度 | 低 | 中 |
推薦:
- 普通場(chǎng)景 → 用 基礎(chǔ)版
- 需要遞歸調(diào)用/嵌套鎖 → 用 可重入版
通過(guò)以上 Lua 腳本 + 客戶(hù)端封裝,你可以在 Redis 中實(shí)現(xiàn)一個(gè)高效、安全、可靠的分布式鎖系統(tǒng)。
到此這篇關(guān)于通過(guò)lua實(shí)現(xiàn)redis 分布式鎖的項(xiàng)目實(shí)踐的文章就介紹到這了,更多相關(guān)lua redis 分布式鎖內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
mac下設(shè)置redis開(kāi)機(jī)啟動(dòng)方法步驟
這篇文章主要介紹了mac下設(shè)置redis開(kāi)機(jī)啟動(dòng),本文詳細(xì)的給出了操作步驟,需要的朋友可以參考下2015-07-07
Redis 的查詢(xún)很快的原因解析及Redis 如何保證查詢(xún)的高效
由于redis是內(nèi)存數(shù)據(jù)庫(kù),歸功于它的數(shù)據(jù)結(jié)構(gòu)所以查詢(xún)效率非常高,今天通過(guò)本文給大家介紹下Redis 的查詢(xún)很快的原因解析及Redis 如何保證查詢(xún)的高效,感興趣的朋友一起看看吧2022-03-03
批量導(dǎo)入txt數(shù)據(jù)到的redis過(guò)程
用戶(hù)通過(guò)將Redis命令逐行寫(xiě)入txt文件,利用管道模式運(yùn)行客戶(hù)端,成功執(zhí)行批量刪除以"Product*"匹配的Key操作,提高了數(shù)據(jù)清理效率2025-08-08
Redis緩存與數(shù)據(jù)庫(kù)一致性的完整指南
某金融平臺(tái)因緩存數(shù)據(jù)不一致導(dǎo)致用戶(hù)余額錯(cuò)亂,損失千萬(wàn)!文中將用銀行對(duì)賬比喻+實(shí)戰(zhàn)代碼,揭秘6大解決方案,讓你的數(shù)據(jù)毫秒級(jí)同步,所以本文給大家詳細(xì)介紹了Redis緩存與數(shù)據(jù)庫(kù)一致性的完整指南,需要的朋友可以參考下2025-09-09

