分布式锁本身不产生并行,分布式锁做的是把会互相踩踏的那一小段操作串起来,让其余部分放手并行。搜「分布式锁怎么实现并行操作」的人,多半卡在锁粒度这一步:锁 key 写成一个全局名字,所有请求都在同一个 key 上排队,看起来就像加了锁就不能并行;把 key 按资源 ID 拆开,同一个订单串行、不同订单照常并行,这才是分布式锁在并行场景里的正常用法。
判断要点:并行度由锁 key 的粒度决定,不由“加不加锁”决定。适用场景是同一份资源被多个进程或线程并发写;操作动作是把锁 key 收敛到资源维度,例如 lock:order:{orderId},并保证 TTL 大于临界区耗时、value 每次唯一;验证方式是同时开两个会话,对同一 key 抢两次、对不同 key 各抢一次,观察返回 OK 与 nil 的分布;风险边界是锁过期与业务未结束重叠时,会出现两个客户端同时进入临界区,需要用续期或 fencing token 兜底。
分布式锁:先把能核对的事实摆清楚
先把对象写明确:一个 Redis 实例或集群、一个锁 key、一个每次请求都不重复的 value、一个以毫秒计的过期时间。前提条件是业务代码能拿到资源标识(订单号、任务 ID、用户 ID),客户端能连通 Redis,服务端支持 SET 的 NX 与 PX 选项。部署前先跑 redis-server --version 和 redis-cli --version,确认版本信息,老版本对选项的支持不同,这一步不能靠记忆。
能核对的现象有这几处,先看再判断:
- 锁 key 长什么样:
SCAN 0 MATCH lock:* COUNT 100,看是lock:order这种全局名,还是lock:order:1001这种带资源 ID 的名字。 - 锁还有多久过期:
PTTL lock:order:1001,返回毫秒数;返回-2表示 key 已不存在,返回-1表示 key 没有过期时间,后者在锁场景里通常是写错了。 - 释放是否走了比对:在代码里搜
DEL直接删锁的地方,正常的释放应当带 value 比对。 - 日志位置:业务日志里至少要能看到“尝试获取锁的时间、获取结果、释放时间”三行,缺任意一行,后面就没法判断临界区实际占了多久。
- Redis 侧面板或命令:
SLOWLOG GET 10看有没有异常慢的EVAL,INFO commandstats看cmdstat_set与cmdstat_eval的调用量级。
这一段只收集现象,不下“锁没问题”或“锁有问题”的结论。
分布式锁最容易搞错的一点
最常见的误解是:加了分布式锁就等于把并行变成了串行,所以想并行就不能加锁。实际的分界在锁 key 的粒度上——锁管住的是同一个 key 上的并发,不同 key 之间本来就可以同时持有锁。锁 key 只写资源类型不写资源 ID,等于把整个业务域的并发压成一条队列;锁 key 写到资源 ID,互斥范围才收窄到真正会冲突的那一份数据。

能把两种情况区分开的检查动作是:开两个 redis-cli 会话,在同一秒内分别执行
SET lock:order:1001 "req-a" NX PX 10000
SET lock:order:1002 "req-b" NX PX 10000两条都返回 OK,说明不同资源可以并行;再对 1001 抢一次,返回 (nil),说明同资源互斥。如果换成 SET lock:order ... 做同样的实验,第二条就会拿到 (nil),两个毫不相干的订单互相阻塞,并行度被自己写没了。
分布式锁:一个照着就能做完的做法
- 确定互斥的最小范围:只包住“读—判断—写”这段会冲突的逻辑,网络调用、消息发送尽量放到锁外面。
- 拼锁 key:
lock:{业务名}:{资源ID},资源 ID 取业务主键,不要取时间戳或随机数。 - 抢锁,value 用本次请求的唯一标识:
SET lock:order:1001 "req-7f3a-uuid" NX PX 10000成功返回 OK,失败返回 (nil)。拿不到就按既定降级走:快速失败返回、稍后重试或转异步,不要无上限自旋。

- 拿到锁后再读一次数据并校验版本号,因为抢锁成功不代表数据没被别人改过。
- 临界区耗时接近 TTL 时续期,续期必须比对 value:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('pexpire', KEYS[1], ARGV[2]) else return 0 end" 1 lock:order:1001 "req-7f3a-uuid" 10000返回 1 表示续期成功,返回 0 表示锁已不属于自己,业务要停下来,不能继续写。
- 释放锁同样比对 value,用 Lua 保证判断和删除是原子的:
EVAL "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end" 1 lock:order:1001 "req-7f3a-uuid"返回 1 是正常释放,返回 0 说明锁已过期或被别人持有,直接丢弃,不要重试删除。
- 把获取时间、释放时间、锁 key 打进日志,作为下一步验证的依据。
容易踩的坑在 TTL 和临界区时长的关系上:TTL 设得比业务最长耗时短,锁先过期,业务还在跑,第二个请求拿到同一把锁进来,两个客户端同时写同一份数据,而日志里显示的却是“两次都成功获取锁”。判断动作是把日志里的释放时间减去获取时间,跟 TTL 比一比;如果临界区耗时经常逼近 TTL,要么调大 TTL,要么加续期,要么把锁内逻辑拆短。单纯调大 TTL 不是万能解,进程崩溃后锁要等更久才释放,这个取舍需要结合环境确认。
分布式锁:怎么验证结果对不对
验证分两层,先验锁的行为,再验业务的并行度。

锁行为可以在命令行直接看。开两个会话,第一个执行 SET lock:order:1001 "req-a" NX PX 10000,第二个立刻执行同一条但 value 换成 req-b,正常结果是一个 OK、一个 (nil);随后 PTTL lock:order:1001 应当返回一个小于 10000 的正整数,返回 -1 就说明过期时间没设上。再用错误的 value 执行释放脚本,正常结果必须返回 0 且 key 仍在,这一步能证明“别人的锁删不掉”。
并行度可以在 redis-cli MONITOR 里看一段时间内的命令流水,正常结果应当是多条不同后缀的 SET lock:order:xxxx 交替出现,而不是所有请求都挤在同一个 key 上;如果 MONITOR 输出里同一个锁 key 的 SET 与 EVAL 严格成对且中间夹着大量业务命令,说明这把锁包住了太长的一段逻辑。
业务侧用结果反推:同一批任务并发执行后,检查每条资源记录是否只被处理一次,重复处理的记录数正常应当为 0;再对照日志里同一条资源的获取与释放是否首尾相接、没有交叠。出现交叠就回到 TTL 与续期上排查。MONITOR 会带来额外开销,建议只在测试环境或短时间窗口内开启,用完立即关闭。