2、锁机制入门:分布式锁的核心思想与基本原理

说到分布式锁,我先讲个真实经历。

几年前我负责一个电商的库存扣减服务。单机时代,用个 synchronized 就搞定了。后来业务量上来,服务拆成了 8 个实例。结果呢?超卖问题直接炸了。一台机器扣了库存,另一台不知道,照样扣。那天晚上我盯着监控,头皮发麻。

嗯,这就是典型的「多实例调度任务冲突」。要解决它,就得引入分布式锁。

2.1 为什么单机锁不够用了?

你想想看,单机锁的本质是什么?是 JVM 内部的一个标记。同一个进程里的线程能看到它,但跨进程、跨机器就看不到了。

对比维度 单机锁(synchronized / ReentrantLock) 分布式锁
作用范围 单个 JVM 进程内 跨进程、跨机器
依赖组件 JVM 内存 Redis / Zookeeper / 数据库
失效场景 线程异常退出 网络分区、节点宕机、GC 停顿
实现难度 语言内置,开箱即用 需要自己设计或引入中间件

说白了,单机锁的边界就是进程边界。一旦你的调度任务跑在多台机器上,就必须换一套思路。

2.2 分布式锁的核心思想

分布式锁的核心思想其实很简单:找一个所有实例都能访问到的「第三方裁判」。

这个裁判需要满足三个条件:

  1. 全局可见 —— 所有实例都能读写它
  2. 原子操作 —— 判断和设置不能分开
  3. 自动释放 —— 持有者挂了,锁能自己解开

我习惯把分布式锁比作「会议室门口的白板」。谁想开会,先去白板上写自己的名字。写上了,就进去开会;没写上,就等着。会议结束,擦掉名字。如果开会的人突然晕倒了,保洁阿姨过一会儿也会把名字擦掉——这就是自动释放。

核心公式:

分布式锁 = 共享存储 + 原子操作 + 超时机制

2.3 三种主流实现方案

目前业界常用的分布式锁实现,主要有三条路。我分别说说它们的原理和坑。

2.3.1 基于数据库

最朴素的做法。利用数据库的唯一索引或行锁。

-- 加锁:插入一条记录,主键或唯一索引冲突则失败
INSERT INTO task_lock (lock_key, instance_id, expire_time)
VALUES ('job:order:settle', 'instance-001', '2025-01-15 10:00:30');

-- 解锁:删除记录
DELETE FROM task_lock WHERE lock_key = 'job:order:settle' AND instance_id = 'instance-001';

我曾经在一个老项目里见过这种方案。优点是简单,不用引入新组件。缺点也很明显:性能瓶颈在数据库,而且没有自动过期机制。如果持有锁的实例挂了,那条记录就永远留在表里,其他实例永远拿不到锁。

避坑指南: 我曾经踩过这个坑。数据库锁一定要加 expire_time 字段,并且起一个后台线程定期清理过期记录。否则一次宕机,锁就死透了。

2.3.2 基于 Redis

这是目前最流行的方案。利用 Redis 的 SETNX 命令(SET if Not eXists)。

// 加锁(Redis 2.6.12+ 推荐用 SET 命令合并原子操作)
SET lock_key unique_value NX PX 30000

// 解锁(使用 Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

这里有几个关键点:

  • NX:只有 key 不存在时才设置,保证互斥
  • PX 30000:30 秒自动过期,防止死锁
  • unique_value:每个实例生成唯一值,防止误删别人的锁
  • Lua 脚本:判断和删除必须原子执行

Redis 方案性能极高,但有个老生常谈的问题:主从切换时锁可能丢失。Redis 主节点挂了,从节点还没同步锁数据,新主节点上锁就没了。另一个实例就能趁虚而入。

我的建议: 如果业务允许偶尔的锁失效(比如非金融场景),单机 Redis 就够了。如果要求强一致性,考虑 Redlock 算法或直接上 Zookeeper。

2.3.3 基于 Zookeeper

Zookeeper 的方案利用临时顺序节点。每个实例在锁目录下创建临时顺序节点,编号最小的获得锁。

// 伪代码逻辑
1. 在 /locks/task 下创建临时顺序节点 /locks/task/lock-0000000001
2. 获取 /locks/task 下的所有子节点
3. 判断自己的节点序号是否最小
4. 是 → 获得锁;否 → 监听前一个节点的删除事件
5. 前一个节点删除 → 重新判断

Zookeeper 的优势是强一致性,没有锁丢失的问题。临时节点天然支持自动释放——客户端断开连接,节点自动删除。

但性能不如 Redis,每次加解锁都要经过 ZAB 协议达成共识。适合对一致性要求高、并发量适中的场景。

2.4 三种方案对比

维度 数据库 Redis Zookeeper
性能 低(磁盘 IO) 高(内存操作) 中(ZAB 协议)
一致性 弱(主从延迟) 弱(主从切换丢锁) 强(ZAB 保证)
自动释放 需手动实现 过期时间 临时节点
实现复杂度 低 中 高
适用场景 低频、容忍锁失效 高频、允许短暂不一致 强一致、低频

2.5 分布式锁的核心流程图

下面这张图展示了分布式锁的通用流程。不管用哪种中间件,骨架都是一样的。

分布式锁核心流程 分布式锁核心流程 任务实例启动 尝试加锁(SETNX) 加锁成功 → 执行任务 加锁失败 → 等待重试 任务执行中 指数退避重试 释放锁(DEL + Lua) 任务结束 关键点 原子操作 自动过期 唯一标识

2.6 实现分布式锁的三个关键原则

不管选哪种方案,有三大原则必须刻在脑子里。

  1. 互斥性:同一时刻只能有一个实例持有锁。这是锁的根本。
  2. 安全性:锁不会因为持有者异常而永远不释放。超时或临时节点是底线。
  3. 可重入性(可选):同一个实例再次请求锁时,应该能成功。用计数器或线程标识实现。

一句话总结: 分布式锁的本质,就是把「判断-设置」这个动作从本地搬到共享存储上,并且保证这个动作是原子的。

2.7 本章小结

这一章我们聊了分布式锁为什么需要、核心思想是什么、三种主流实现方案以及它们的对比。

我个人习惯在项目初期先用 Redis 方案快速落地,等业务发展到对一致性有更高要求时,再考虑引入 Zookeeper。记住,没有银弹。选型要看你的业务场景:能接受偶尔的锁失效吗?对性能要求有多高?团队对哪个中间件更熟悉?