分布式锁怎么实现并行处理

文章导读
分布式锁本身不产生并行:加锁这一步天生是串行的。想让并行真正发生,做法是把锁的粒度从「全局一把」拆到「按分片、按任务 ID 一把」,锁里只做认领和短临界区,耗时的业务计算放到锁外执行。判断标准很直接:跑起来后 Redis 里同时存在的锁键数量,如果恒为 1,那么再多 worker 也只是排队串行;如果键名带分片编号且数量接近并发 worker 数,才是并行在跑。
📋 目录
  1. 壹 分布式锁:先把能核对的事实摆清楚
  2. 贰 分布式锁最容易搞错的一点
  3. 叁 分布式锁:一个照着就能做完的做法
  4. 肆 分布式锁:怎么验证结果对不对
A A

分布式锁本身不产生并行:加锁这一步天生是串行的。想让并行真正发生,做法是把锁的粒度从「全局一把」拆到「按分片、按任务 ID 一把」,锁里只做认领和短临界区,耗时的业务计算放到锁外执行。判断标准很直接:跑起来后 Redis 里同时存在的锁键数量,如果恒为 1,那么再多 worker 也只是排队串行;如果键名带分片编号且数量接近并发 worker 数,才是并行在跑。

适用于多进程或多个实例争抢同一批任务的场景:先确认锁键是否按分片拆开,再把加锁范围压缩到「认领」动作上,用 SET NX PX 拿锁、用 Lua 比对持有者后释放,最后用 redis-cli --scan 统计锁键数量、用 MONITOR 采样不同键名数量来验证并行是否真的成立。边界在于:分布式锁只能保证同一分片不被重复处理,分片划分是否均匀、锁过期时间是否够业务跑完,需要结合自己的任务耗时和环境确认。

分布式锁:先把能核对的事实摆清楚

这里要核对的对象是:若干 worker 进程、一个存放锁键的 Redis 实例、一份可被切分的任务集合。前提条件是 Redis 可连通、客户端能发起 SET 命令、worker 侧有可写日志。以下这些都能当场看到结果,不含推断:

  • Redis 版本与连通性:redis-cli -h <host> -p <port> INFO server | grep redis_version,正常会打印一行版本号。
  • 当前锁键清单:redis-cli --scan --pattern 'lock:job:*',列出所有匹配键,注意键名里有没有分片编号。
  • 单个锁的剩余存活时间:redis-cli PTTL lock:job:shard-07,返回正数表示有租约,返回 -1 表示这个键没有过期时间,返回 -2 表示键不存在。
  • 实时命令流:timeout 10 redis-cli MONITOR,会逐条打印 10 秒内的 SET、DEL、EVAL,用来观察键名分布。
  • worker 日志位置:应用日志里通常需要自行打一行「持有锁键 + worker 标识 + 开始时间」,如果日志里没有这一行,其他判断都无从核对。

这一段先不下结论,只把这些值记下来:锁键总数、键名是否带分片、PTTL 是正数还是 -1。

分布式锁最容易搞错的一点

最常见的误解是把「加锁」当成「并行」:看到代码里调用了 SETNX,就认为多台机器已经在并行处理了。实际分界在于锁键的命名方式——所有 worker 抢同一个 lock:job 时,同一时刻只有一个 worker 进入临界区,其余全部等待,这是串行;按分片写成 lock:job:shard-01、lock:job:shard-02 时,不同分片之间互不阻塞,才有并行。

能区分这两种情况的检查动作是在任务跑起来之后采样:

timeout 10 redis-cli -h <host> -p <port> MONITOR | grep -oE 'lock:job:[A-Za-z0-9_-]+' | sort -u

如果输出只有一行(或者根本没有分片后缀),说明仍是单键串行,worker 加多少都不能提速;如果输出多行且数量与同时工作的 worker 数相仿,说明分片锁已经在生效。另一种交叉核对是 redis-cli --scan --pattern 'lock:job:*' | wc -l,执行多次看数量是否随并发 worker 数上下浮动,恒定为 1 就是串行。

分布式锁:一个照着就能做完的做法

下面的路径以 Redis 单实例加 SET NX PX 为例,参数请按自己的网络名、端口和任务耗时替换。做完每一步都标注了应该看到什么,看不到就停下来查。

分布式锁怎么实现并行处理
  1. 确认基础可用:执行前面那条 INFO server,能看到 redis_version 一行。看不到先解决网络和认证,不要往下走。
  2. 确定分片数。把任务按业务主键哈希或按区间切成 N 片,N 建议从小于 worker 总数的值起步,让 worker 有余量轮转,避免某一片卡住导致整体停摆。
  3. worker 拿锁,一条命令完成设置与过期:
redis-cli -h <host> -p <port> SET lock:job:shard-07 <worker-id> NX PX 30000

返回 OK 表示拿到锁,返回 (nil) 表示该分片已被别人持有,直接跳到下一片,不要重试等待。30 秒这个值是租约,必须大于单片的正常处理耗时。

  1. 在锁内只做认领和写状态,把批量计算、远程调用放到锁外或者释放锁之后做。锁内代码越长,并行度越低。
  2. 处理完成后释放,必须比对持有者,避免误删别人的锁:
redis-cli -h <host> -p <port> EVAL "if redis.call('GET',KEYS[1])==ARGV[1] then return redis.call('DEL',KEYS[1]) else return 0 end" 1 lock:job:shard-07 <worker-id>

返回 1 表示删掉了自己的锁,返回 0 表示锁已过期或已属于别人,两种情况都不应再次处理该分片。若单片耗时确实接近租约,应提前用同样的 Lua 比对方式做 PEXPIRE 续租,而不是简单加大过期时间。

容易踩的坑是漏掉 PX 参数:只写 SET lock:job:shard-07 <worker-id> NX,进程崩溃后这个键不会过期,后续所有 worker 都拿不到该分片,表面上像「并行没生效」,其实是任务被永久卡死。检查方法就是 redis-cli PTTL lock:job:shard-07,返回 -1 即为此坑。

分布式锁:怎么验证结果对不对

验证分三层,都是可以当场执行的动作:

  • 锁层:redis-cli --scan --pattern 'lock:job:*' | wc -l 在任务高峰期应大于 1,且大致等于同时在跑的 worker 数;逐键执行 PTTL,正常结果是 0 到租约值之间的正数,不应出现 -1。
  • 命令层:timeout 10 redis-cli MONITOR 里应能看到多个不同后缀的 SET 命令交错出现,而不是同一条键被反复抢占。
  • 日志层:把各 worker 的日志按分片 ID 聚合,正常结果是同一分片在任一时刻只有一个「开始-结束」区间,不同 worker 之间的分片 ID 不重叠;如果同一分片出现两个并发区间,说明租约短于实际耗时或释放逻辑写错了。

还有一个必要核对:把 worker 数临时改成 1 再跑一次,看总耗时是否与多 worker 时明显不同。若两种配置耗时几乎一致,说明锁键没有真正拆开,或者临界区里包了几乎全部逻辑,此时调 worker 数没有意义,应先改锁粒度。是否需要引入 fencing token、是否要把锁迁到 Redis 集群或换用其他协调组件,取决于任务对重复执行的容忍度,需要结合环境自行确认。