Intro
本文概述如何借住 redis 实现一个分布式锁。
为何是 Lua
redis 保证了 lua 解释器执行脚本的事务性,即执行结果要么不可见,要么已完成。
参考这篇文档。
简单锁
简单锁指的是简单互斥锁,一旦锁定,则其他锁定请求都必须等待。
直觉的想法是通过 redis 的键来保持锁,故准备一个用于锁定互斥的名字(比如说 mutex-1)然后指定为键。
直接 SET KEY VALUE 是显然不正确的,如果临界区内程序崩溃或意外断网将导致死锁。
只判断是否存在也不够,临界区内程序崩溃会导致锁无法被释放。最直接的兜底措施就是设置一个超时时间,保证业务能在超时时间内完成,如果崩溃也可以在一段时间后自动释放。
redis 的 SET 命令提供了 NX 和 EX seconds | PX milliseconds 选项,可以实现只在不存在键的时候设置,并同时指定过期时间,有原子性保证。所以我们可以使用 SET 命令实现分布式锁。
因为有超时自动释放存在,所以还存在一个竞争:在解锁时 KEY 已经因超时释放,锁被其他人持有。这时候无条件删除就会导致意外释放他人占有的锁。我们还需要增加一个持有者的判断,决定是否释放。
import datetime
import logging
import threading
import time
import uuid
from contextlib import contextmanager
import redis
logging.(
level=.,
format="[%(asctime)s] %(levelname).1s %(name)s:%(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.("main")
client = redis.()
@contextmanager
def lock(client:., key: str, expire:.):
wait = 0.1
maximum = 5
owner = str(.())
while True:
resp = client.(,, ex=, nx=True)
if resp:
break
logger.(f"unable to acquire lock, retrying in {} seconds...")
time.()
wait *= 2 # exponential backoff
if wait > maximum:
wait = maximum
logger.("entering critical section")
yield
logger.("exiting critical section")
script = """\
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
logger.("releasing lock")
resp = client.(, 1,,)
logger.("lock released")
# 验证
def run(client:.):
with(, "run",.(seconds=10)):
logger.(f"thread {.().} lock acquired")
time.(3)
threads = [
threading.(target=, args=(,)),
threading.(target=, args=(,)),
]
for thread in threads:
thread.()
for thread in threads:
thread.()
互斥锁不能提供事务性保证,如何在超时时保证一致性是个问题。
读写锁部分存在严重错误,已删除。