直接用 redis.asyncio 的 set 加锁易失败,因set不原子且无安全释放机制;须用Lua脚本校验value后删除,并设唯一client_id与过期时间。

为什么直接用 redis.asyncio 的 set 加锁容易失败
因为 set 操作本身不保证原子性——即使加了 nx=True, ex=10,在高并发下仍可能因网络延迟、客户端重试或 Redis 主从同步滞后,导致多个协程同时认为自己“抢到了锁”。更关键的是,redis.asyncio 默认不提供带原子释放逻辑的锁封装,你得自己处理 GET + DEL 的竞态问题。
正确做法是用 Lua 脚本把“判断锁归属 + 删除”打包成一个原子操作。官方推荐的锁值必须是唯一标识(比如随机 UUID),不能用固定字符串。
- 锁 key 建议带业务前缀,如
"lock:order:create" - 锁 value 必须是 client_id(如
str(uuid4())),不能是"1"或时间戳 - 务必设置过期时间(
ex),防止死锁;但不要依赖它作为唯一保障 - 不要用
await redis.delete(key)直接删——这会误删别人持有的锁
如何写一个安全的异步分布式锁类
核心是封装两个原子操作:获取锁(SET key value NX EX)和释放锁(Lua 脚本校验 value 后 DEL)。下面是一个最小可用实现:
import uuid
import asyncio
import redis.asyncio as redis
<p>class AsyncRedisLock:
def <strong>init</strong>(self, redis_client: redis.Redis, key: str, timeout: int = 10):
self.redis = redis_client
self.key = key
self.timeout = timeout
self.value = str(uuid.uuid4())
self._acquired = False</p><pre class="brush:php;toolbar:false;">async def acquire(self) -> bool:
ok = await self.redis.set(
self.key, self.value, nx=True, ex=self.timeout
)
self._acquired = bool(ok)
return self._acquired
async def release(self) -> bool:
if not self._acquired:
return False
# Lua script: only delete if value matches
script = """
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"""
result = await self.redis.eval(script, 1, self.key, self.value)
self._acquired = False
return bool(result)
立即学习“Python免费学习笔记(深入)”;
注意:redis.eval() 在 redis.asyncio 中是协程函数,必须 await;脚本里用 KEYS[1] 和 ARGV[1] 传参,不能拼字符串——否则有注入风险。
使用时怎么避免常见超时与重入陷阱
这个锁不是可重入的,同一个协程重复 acquire() 会失败。如果业务需要重入,得自己维护计数器(不推荐,复杂度陡增);更合理的做法是确保锁粒度合理,避免嵌套加锁。
- 不要在锁内做耗时 IO(如 HTTP 请求、长 SQL)——超时会导致锁自动释放,其他协程趁虚而入
- 务必用
try/finally或异步上下文管理器确保release()执行,否则锁泄漏 - 网络分区时,Redis 可能返回
ConnectionError或TimeoutError,这时无法确定锁状态,应视为加锁失败并退避重试 - 若用连接池,确认
max_connections足够,否则acquire()可能卡在等待连接上
有没有更省心的现成方案
可以考虑 aioredis-lock(已归档)或更活跃的 redlock-py 的异步分支,但要注意:它们多数没适配 redis.asyncio 4.x 的 API,且 Redlock 算法在 Redis Cluster 下仍有争议。生产环境若对一致性要求极高,建议优先用 Redis 官方的 RediSearch 或外部协调服务(如 etcd)。
真正容易被忽略的一点是:锁的 key 设计要和业务语义对齐。比如订单创建锁用 "lock:order:create" 没问题,但如果要按用户维度隔离,就得是 f"lock:order:create:{user_id}"——漏掉这个动态部分,就等于没锁。


















