分布式锁 --- 概念、原理与实践
一、为什么需要分布式锁
1.1 单机锁的局限
单机应用(1个JVM进程):
线程A ─┐
线程B ─┼→ synchronized / ReentrantLock → 资源
线程C ─┘
✅ 有效:所有线程在同一个JVM内,共享同一把锁
分布式应用(多个JVM进程/多台服务器):
服务器1(JVM-1):线程A → synchronized → 本地锁1
服务器2(JVM-2):线程B → synchronized → 本地锁2
服务器3(JVM-3):线程C → synchronized → 本地锁3
❌ 无效:三台服务器各自加的是各自的本地锁,互相不可见
→ 三个线程可以同时访问共享资源(数据库同一行)
1.2 类比理解
本地锁 = 家门锁(只能锁住自己家门,邻居有自己的锁)
分布式锁 = 小区门禁(所有住户共享同一个门禁系统,一次只能一个人通过)
1.3 核心问题
多个服务实例同时操作同一条数据时,如何保证同一时刻只有一个实例能执行关键操作?
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、核心概念
2.1 什么是分布式锁
分布式锁是一种跨进程/跨服务器的互斥机制 ,通过一个所有实例都能访问的共享存储(Redis/ZooKeeper/数据库)来协调并发访问。
2.2 关键术语
| 概念 | 说明 | 类比 |
|---|---|---|
| 锁 | 标记"当前资源正在被使用" | 公共厕所的"使用中"牌子 |
| 获取锁(加锁) | 尝试标记资源被自己占用 | 进门后翻转牌子为"使用中" |
| 释放锁(解锁) | 标记资源不再被自己占用 | 出门后翻转牌子为"空闲" |
| 锁持有者标识 | 记录谁持有锁(requestId) | 牌子上写了"3号隔间-张三" |
| 锁过期时间(TTL) | 锁自动释放的时间 | 超过30分钟自动解锁(防止人晕在里面) |
| 自旋等待 | 获取锁失败后反复重试 | 门口排队等待 |
| 超时放弃 | 等待超过一定时间后放弃 | 等了10分钟还没轮到,走人 |
2.3 分布式锁必须满足的条件
| 条件 | 说明 |
|---|---|
| 互斥性 | 同一时刻只有一个客户端能持有锁 |
| 防死锁 | 持有者崩溃后锁能自动释放(TTL 过期) |
| 防误释放 | 只有锁的持有者才能释放锁(A 加的锁 B 不能释放) |
| 高可用 | 锁服务本身不能是单点故障 |
三、与本地锁的对比
3.1 Java 本地锁(JVM 内有效)
| 锁类型 | 特点 | 适用范围 |
|---|---|---|
synchronized |
JVM 内置,自动释放 | 同一 JVM 内的线程 |
ReentrantLock |
可重入、可中断、可超时 | 同一 JVM 内的线程 |
ReadWriteLock |
读写分离,读不互斥 | 同一 JVM 内的线程 |
3.2 分布式锁(跨 JVM 有效)
| 实现方式 | 存储介质 | 特点 |
|---|---|---|
| Redis | 内存 | 性能最高,但 Redis 宕机有风险 |
| ZooKeeper | 磁盘+内存 | 强一致性,但性能略低 |
| 数据库 | 磁盘 | 最简单,但性能最低 |
| Etcd | 磁盘+内存 | 强一致性,云原生场景 |
四、底层原理
4.1 基于 Redis 的分布式锁
加锁原理
客户端A → Redis:SET lock_key requestId_A NX PX 30000
NX = Only set if Not eXists(key不存在时才设置成功)
PX 30000 = 30秒后自动过期
如果返回 OK → 加锁成功
如果返回 nil → 加锁失败(锁被别人持有)
为什么用 NX?
时刻T1:客户端A执行 SET lock NX → 成功(key不存在)
时刻T2:客户端B执行 SET lock NX → 失败(key已存在)
→ 只有A获得锁,B被拒绝
为什么需要 PX(过期时间)?
客户端A加锁成功后崩溃了(没来得及释放锁):
没有过期时间 → 锁永远不释放 → 死锁(其他人永远无法获取)
有过期时间 → 30秒后Redis自动删除key → 其他人可以获取锁
为什么需要 requestId?
场景:A加的锁,B来释放
时刻T1:A 加锁成功(lock = requestId_A)
时刻T2:A 处理业务超时,锁过期自动释放
时刻T3:B 加锁成功(lock = requestId_B)
时刻T4:A 处理完毕,执行释放锁操作
如果不校验 requestId:
A 直接 DEL lock → 把B的锁删了!B以为自己还持有锁,实际已经没了
如果校验 requestId:
A 释放时检查 lock 的值是否是 requestId_A → 不是 → 不释放
→ B 的锁不受影响
加锁 Lua 脚本(原子操作)
lua
-- KEYS[1] = 锁的key
-- KEYS[2] = requestId的key
-- ARGV[1] = requestId值
-- ARGV[2] = 过期时间(毫秒)
if (redis.call('exists', KEYS[1]) == 0) then
redis.call('hset', KEYS[1], KEYS[2], ARGV[1])
redis.call('pexpire', KEYS[1], ARGV[2])
return 1 -- 加锁成功
else
return 0 -- 锁已存在,加锁失败
end
为什么用 Lua 脚本?
exists+hset+pexpire三个命令需要原子执行- 如果分开执行,两条命令之间可能被其他客户端插入操作
- Redis 保证 Lua 脚本原子性执行
解锁 Lua 脚本(原子操作)
lua
-- KEYS[1] = 锁的key
-- KEYS[2] = requestId的key
-- ARGV[1] = requestId值
if redis.call('hget', KEYS[1], KEYS[2]) == ARGV[1] then
redis.call('del', KEYS[1])
return 1 -- 解锁成功
else
return 0 -- 不是自己的锁,拒绝释放
end
4.2 基于数据库的分布式锁
原理
利用数据库的唯一约束 或排他锁实现互斥。
方式1:唯一索引(INSERT 方式)
sql
-- 建表
CREATE TABLE distributed_lock (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
lock_key VARCHAR(100) NOT NULL UNIQUE, -- 唯一约束
request_id VARCHAR(64) NOT NULL,
expire_time DATETIME NOT NULL,
create_time DATETIME NOT NULL
);
-- 加锁:INSERT(唯一约束保证互斥)
INSERT INTO distributed_lock (lock_key, request_id, expire_time, create_time)
VALUES ('order_process_123', 'uuid-xxx', NOW() + INTERVAL 30 SECOND, NOW());
-- 成功 → 获得锁
-- 失败(Duplicate entry) → 锁被占用
-- 解锁:DELETE(校验 request_id)
DELETE FROM distributed_lock
WHERE lock_key = 'order_process_123' AND request_id = 'uuid-xxx';
方式2:SELECT FOR UPDATE(悲观锁)
sql
-- 加锁
BEGIN;
SELECT * FROM distributed_lock WHERE lock_key = 'order_process_123' FOR UPDATE;
-- 其他事务对同一行的 SELECT FOR UPDATE 会阻塞等待
-- 执行业务逻辑...
-- 解锁
COMMIT; -- 事务提交后锁自动释放
4.3 基于 ZooKeeper 的分布式锁
原理
利用 ZooKeeper 的临时有序节点实现。
/locks/order_process_123/
├── node_0000000001 (客户端A创建) ← 序号最小,获得锁
├── node_0000000002 (客户端B创建) ← 监听前一个节点
└── node_0000000003 (客户端C创建) ← 监听前一个节点
客户端A处理完毕 → 删除 node_0000000001
→ 触发 node_0000000002 的 Watch 通知
→ 客户端B检查自己是否最小 → 是 → 获得锁
优点: 临时节点在客户端断连时自动删除(防死锁),Watch 机制避免轮询。
五、JPA/数据库中的锁实现
5.1 乐观锁(Optimistic Locking)
思想: 假设冲突很少发生,不加锁,提交时检查是否被修改过。
java
@Entity
@Table(name = "stock")
public class Stock {
@Id
private Integer id;
private Integer itemSkuId;
private Integer qty;
@Version // JPA 乐观锁注解
private Integer version;
}
执行流程:
线程A:SELECT * FROM stock WHERE id=1 → version=5, qty=100
线程B:SELECT * FROM stock WHERE id=1 → version=5, qty=100
线程A:UPDATE stock SET qty=90, version=6 WHERE id=1 AND version=5 → 成功(影响1行)
线程B:UPDATE stock SET qty=80, version=6 WHERE id=1 AND version=5 → 失败(影响0行,version已变为6)
→ JPA 抛出 OptimisticLockException
→ 线程B可以重试
适用场景: 并发冲突概率低,读多写少。
5.2 悲观锁(Pessimistic Locking)
思想: 假设冲突频繁,操作前先加锁。
java
// JPA 悲观锁查询
@Repository
public interface StockRepository extends JpaRepository<Stock, Integer> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT s FROM Stock s WHERE s.itemSkuId = :itemSkuId")
Stock findByItemSkuIdForUpdate(@Param("itemSkuId") Integer itemSkuId);
}
执行的 SQL:
sql
SELECT * FROM stock WHERE item_sku_id = ? FOR UPDATE;
-- 其他事务对同一行的写操作会阻塞,直到当前事务提交
适用场景: 并发冲突概率高,写多读少。
5.3 乐观锁 vs 悲观锁 vs 分布式锁
| 维度 | 乐观锁 | 悲观锁 | 分布式锁 |
|---|---|---|---|
| 加锁时机 | 更新时检查 | 查询时加锁 | 业务操作前加锁 |
| 锁的范围 | 数据库行级 | 数据库行级 | 跨服务/跨资源 |
| 冲突处理 | 抛异常/重试 | 阻塞等待 | 阻塞等待/超时放弃 |
| 性能 | 高(无锁) | 中(行锁等待) | 取决于实现(Redis最快) |
| 死锁风险 | 无 | 有(多表交叉锁) | 有(靠TTL解决) |
| 适用场景 | 低冲突 | 高冲突单表 | 跨服务/跨资源互斥 |
| 典型用法 | @Version | FOR UPDATE | Redis SETNX |
六、各技术框架中的分布式锁实现
6.1 Redisson(最流行的 Redis 分布式锁框架)
java
// 依赖
// <dependency>
// <groupId>org.redisson</groupId>
// <artifactId>redisson-spring-boot-starter</artifactId>
// <version>3.27.0</version>
// </dependency>
@Service
public class OrderService {
@Resource
private RedissonClient redissonClient;
public void processOrder(String orderId) {
RLock lock = redissonClient.getLock("order_lock_" + orderId);
try {
// 等待10秒,锁自动释放时间30秒
boolean acquired = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (acquired) {
doProcess(orderId);
} else {
throw new RuntimeException("获取锁超时");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
Redisson 的高级特性:
- 看门狗机制(Watchdog):自动续期,防止业务未完成锁就过期
- 可重入锁:同一线程可以重复获取同一把锁
- 红锁(RedLock):多 Redis 节点加锁,防单点故障
6.2 Spring Integration(数据库锁)
java
// 依赖
// <dependency>
// <groupId>org.springframework.integration</groupId>
// <artifactId>spring-integration-jdbc</artifactId>
// </dependency>
@Configuration
public class LockConfig {
@Bean
public DefaultLockRepository lockRepository(DataSource dataSource) {
DefaultLockRepository repository = new DefaultLockRepository(dataSource);
repository.setPrefix("APP_LOCK_");
repository.setTimeToLive(30000); // 30秒过期
return repository;
}
@Bean
public JdbcLockRegistry lockRegistry(LockRepository lockRepository) {
return new JdbcLockRegistry(lockRepository);
}
}
@Service
public class OrderService {
@Resource
private LockRegistry lockRegistry;
public void processOrder(String orderId) {
Lock lock = lockRegistry.obtain("order_" + orderId);
try {
if (lock.tryLock(10, TimeUnit.SECONDS)) {
try {
doProcess(orderId);
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
6.3 Curator(ZooKeeper 锁)
java
// 依赖
// <dependency>
// <groupId>org.apache.curator</groupId>
// <artifactId>curator-recipes</artifactId>
// <version>5.5.0</version>
// </dependency>
@Service
public class OrderService {
@Resource
private CuratorFramework curatorClient;
public void processOrder(String orderId) {
InterProcessMutex lock = new InterProcessMutex(
curatorClient, "/locks/order_" + orderId);
try {
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
doProcess(orderId);
} finally {
lock.release();
}
}
} catch (Exception e) {
throw new RuntimeException("获取锁失败", e);
}
}
}
七、通用示例代码
7.1 基于 Redis 的分布式锁
java
/**
* 分布式锁实现(基于Redis + Lua脚本).
*
* 设计要点:
* 1. NX 保证互斥
* 2. PX 防死锁(自动过期)
* 3. requestId 防误释放
* 4. Lua 脚本保证原子性
* 5. 自旋等待 + 超时退出
*/
public class RedisDistributedLock implements AutoCloseable {
private static final Logger log = LoggerFactory.getLogger(RedisDistributedLock.class);
private final StringRedisTemplate redisTemplate;
private final String lockKey;
private final String requestId;
private final long leaseTimeMillis;
private volatile boolean locked = false;
private static final String LOCK_PREFIX = "distributed_lock:";
// 加锁Lua脚本
private static final String LOCK_SCRIPT =
"if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " +
" redis.call('pexpire', KEYS[1], ARGV[2]); " +
" return 1; " +
"else " +
" return 0; " +
"end";
// 解锁Lua脚本
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" redis.call('del', KEYS[1]); " +
" return 1; " +
"else " +
" return 0; " +
"end";
public RedisDistributedLock(StringRedisTemplate redisTemplate,
String businessKey, long leaseTimeMillis) {
this.redisTemplate = redisTemplate;
this.lockKey = LOCK_PREFIX + businessKey;
this.requestId = UUID.randomUUID().toString().replace("-", "");
this.leaseTimeMillis = leaseTimeMillis;
}
/**
* 尝试获取锁(支持超时等待).
*
* @param waitTimeMillis 最大等待时间(毫秒),0表示不等待立即返回
* @return true-获取成功,false-超时失败
*/
public boolean tryLock(long waitTimeMillis) {
long deadline = System.currentTimeMillis() + waitTimeMillis;
// 第一次尝试
if (doLock()) {
this.locked = true;
log.debug("获取锁成功: key={}, requestId={}", lockKey, requestId);
return true;
}
// 自旋等待
while (System.currentTimeMillis() < deadline) {
try {
Thread.sleep(100); // 每100ms重试一次
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
if (doLock()) {
this.locked = true;
log.debug("获取锁成功(重试): key={}, requestId={}", lockKey, requestId);
return true;
}
}
log.warn("获取锁超时: key={}", lockKey);
return false;
}
/**
* 释放锁.
*/
public boolean unlock() {
if (!locked) {
return true;
}
DefaultRedisScript<Long> script = new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(lockKey), requestId);
boolean success = result != null && result == 1;
if (success) {
this.locked = false;
log.debug("释放锁成功: key={}, requestId={}", lockKey, requestId);
} else {
log.warn("释放锁失败(非持有者): key={}, requestId={}", lockKey, requestId);
}
return success;
}
@Override
public void close() {
unlock();
}
private boolean doLock() {
DefaultRedisScript<Long> script = new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
Long result = redisTemplate.execute(script,
Collections.singletonList(lockKey),
requestId, String.valueOf(leaseTimeMillis));
return result != null && result == 1;
}
}
7.2 锁工厂
java
/**
* 分布式锁工厂.
* 注入后直接使用,无需关心底层 Redis 操作.
*/
@Component
public class DistributedLockFactory {
@Resource
private StringRedisTemplate stringRedisTemplate;
/**
* 获取分布式锁实例.
*
* @param businessKey 业务锁标识(如 "order_123")
* @param leaseTime 锁过期时间
* @param unit 时间单位
*/
public RedisDistributedLock getLock(String businessKey, long leaseTime, TimeUnit unit) {
return new RedisDistributedLock(stringRedisTemplate, businessKey, unit.toMillis(leaseTime));
}
/**
* 获取分布式锁(默认过期10分钟).
*/
public RedisDistributedLock getLock(String businessKey) {
return getLock(businessKey, 10, TimeUnit.MINUTES);
}
}
7.3 使用示例
java
@Service
public class StockDeductService {
@Resource
private DistributedLockFactory lockFactory;
@Resource
private StockRepository stockRepository;
/**
* 扣减库存(分布式锁保证并发安全).
*/
public void deductStock(Integer itemId, Integer qty) {
String lockKey = "stock_deduct_" + itemId;
// try-with-resources 自动释放锁
try (RedisDistributedLock lock = lockFactory.getLock(lockKey, 30, TimeUnit.SECONDS)) {
// 尝试获取锁,最多等待5秒
if (!lock.tryLock(5000)) {
throw new RuntimeException("系统繁忙,请稍后重试");
}
// 获取锁成功,安全执行业务
Stock stock = stockRepository.findByItemId(itemId);
if (stock.getQty() < qty) {
throw new RuntimeException("库存不足");
}
stock.setQty(stock.getQty() - qty);
stockRepository.save(stock);
}
// 离开 try 块自动调用 close() → unlock()
}
}
八、常见问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 死锁 | 持有者崩溃未释放 | TTL 过期自动释放 |
| 误释放 | A 释放了 B 的锁 | requestId 校验 |
| 锁过期但业务未完成 | 业务执行时间超过 TTL | Watchdog 自动续期(Redisson) |
| Redis 主从切换丢锁 | 主节点宕机,从节点升主但未同步锁 | RedLock(多节点加锁) |
| 锁饥饿 | 某些线程一直获取不到锁 | 公平锁(按申请顺序排队) |
| 重入问题 | 同一线程再次获取同一把锁 | 可重入锁(计数器+线程ID) |
九、关键设计总结
| 设计要点 | Redis 实现 | 数据库实现 | ZooKeeper 实现 |
|---|---|---|---|
| 互斥性 | SETNX | UNIQUE KEY / FOR UPDATE | 临时有序节点 |
| 防死锁 | PEXPIRE TTL | 定时清理过期记录 | 临时节点自动删除 |
| 防误释放 | Lua 校验 requestId | WHERE request_id = ? | 只能删除自己创建的节点 |
| 原子性 | Lua 脚本 | 数据库事务 | ZK 原子性保证 |
| 等待通知 | 轮询(sleep + retry) | 无(需轮询) | Watch 机制(事件驱动) |
| 性能 | ⭐⭐⭐⭐⭐(内存操作) | ⭐⭐(磁盘IO) | ⭐⭐⭐(网络+磁盘) |
| 可靠性 | ⭐⭐⭐(主从可能丢锁) | ⭐⭐⭐⭐(事务保证) | ⭐⭐⭐⭐⭐(强一致性) |