目录
- 一、为什么需要分布式锁
-
- [1. 什么是锁](#1. 什么是锁)
- [2. 单机锁为什么失效](#2. 单机锁为什么失效)
- 二、分布式锁是什么
- 三、分布式锁需要满足什么条件
-
- [1. 互斥性](#1. 互斥性)
- [2. 可重入](#2. 可重入)
- [3. 防死锁](#3. 防死锁)
- [4. 高可用](#4. 高可用)
- 四、Redis实现分布式锁
- 五、数据库唯一索引实现分布式锁
-
- [1. 建表](#1. 建表)
- [2. 获取锁](#2. 获取锁)
- [3. 原理](#3. 原理)
- [4. 缺点](#4. 缺点)
- 六、数据库乐观锁(版本号)
-
- [1. 表结构](#1. 表结构)
- [2. 查询](#2. 查询)
- [3. 更新](#3. 更新)
- [4. 原理](#4. 原理)
- [5. MyBatis Plus实现](#5. MyBatis Plus实现)
- 七、数据库悲观锁
-
- [1. 获取锁](#1. 获取锁)
- [2. 原理](#2. 原理)
- [3. 缺点](#3. 缺点)
- 八、ZooKeeper实现分布式锁
- 九、各种方案对比
- 十、单机环境如何实现锁
-
- [1. synchronized](#1. synchronized)
- [2. ReentrantLock](#2. ReentrantLock)
- [3. ReadWriteLock](#3. ReadWriteLock)
- 总结
一、为什么需要分布式锁
1. 什么是锁
锁(Lock)的本质是:
保证同一时刻只有一个线程(或一个进程)能够执行某段代码。
例如:
java
public void deductStock() {
stock--;
}
假设库存为 1:
- 用户A购买
- 用户B购买
两个线程同时执行:
java
stock--;
结果可能变成:
text
库存:-1
这就是典型的并发问题。
因此需要锁来保证:
java
synchronized(lock){
stock--;
}
同一时刻只能有一个线程进入。
2. 单机锁为什么失效
在单体应用中:
text
JVM
┌─────────────────┐
│ Thread-1 │
│ Thread-2 │
│ synchronized │
└─────────────────┘
synchronized、ReentrantLock 都能正常工作。
但是微服务部署后:
text
Nginx
/ \
Service-A Service-B
JVM1 JVM2
用户请求可能进入:
text
请求1 -> JVM1
请求2 -> JVM2
此时:
java
synchronized(lock)
只锁住当前 JVM。
因为:
text
JVM1 不知道 JVM2 的锁状态
JVM2 不知道 JVM1 的锁状态
于是两个服务同时执行。
这就是:
本地锁失效问题
因此需要:
所有节点共享同一把锁
这就是分布式锁。
二、分布式锁是什么
定义:
分布式系统中多个节点共同竞争同一个共享资源时,用来保证同一时刻只有一个节点执行操作的机制。
例如:
text
秒杀活动
定时任务
订单创建
库存扣减
退款操作
都可能需要分布式锁。
三、分布式锁需要满足什么条件
一个合格的分布式锁通常需要满足:
1. 互斥性
text
同一时间只能一个客户端获得锁
2. 可重入
例如:
java
methodA()
-> methodB()
methodA已经获得锁。
methodB再次加锁:
java
lock.lock();
不能死锁。
3. 防死锁
持有锁的服务挂掉:
text
JVM Crash
锁必须自动释放。
通常依赖:
text
TTL过期时间
4. 高可用
Redis挂了:
text
锁全部失效
需要主从、哨兵或集群保障。
四、Redis实现分布式锁
这是目前最常见的方案。
1. 原理
Redis提供:
text
SET key value NX EX 30
含义:
text
NX:不存在才创建
EX:30秒过期
执行成功:
text
OK
说明获得锁。
执行失败:
text
null
说明锁已被占用。
2. Spring Boot实现
加锁
java
@Autowired
private StringRedisTemplate redisTemplate;
public boolean tryLock(String key) {
return Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(
key,
UUID.randomUUID().toString(),
30,
TimeUnit.SECONDS
)
);
}
底层执行:
redis
SET lock:order uuid NX EX 30
释放锁
错误写法:
java
redisTemplate.delete(key);
问题:
text
线程A锁超时
线程B获得锁
线程A执行delete
把线程B的锁删了
正确做法:
java
String lua =
"if redis.call('get',KEYS[1])==ARGV[1] then " +
" return redis.call('del',KEYS[1]) " +
"else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(lua, Long.class),
Collections.singletonList(key),
uuid
);
保证:
text
谁加锁
谁解锁
3. Redisson实现
实际项目更推荐。
引入:
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
获取锁:
java
RLock lock = redissonClient.getLock("orderLock");
try {
lock.lock();
createOrder();
} finally {
lock.unlock();
}
优点:
text
自动续期
可重入
看门狗机制
高可靠
面试中一般回答:
生产环境推荐 Redisson。
五、数据库唯一索引实现分布式锁
这是最简单的方案之一。
1. 建表
sql
CREATE TABLE distributed_lock(
lock_name VARCHAR(100) PRIMARY KEY,
create_time DATETIME
);
2. 获取锁
java
try {
lockMapper.insert(lockName);
return true;
} catch (DuplicateKeyException e) {
return false;
}
对应SQL:
sql
INSERT INTO distributed_lock
VALUES('ORDER_LOCK',NOW());
3. 原理
假设:
text
JVM1 插入成功
JVM2 插入同样主键
数据库保证:
text
主键唯一
第二次插入失败。
因此:
text
只有一个服务获得锁
4. 缺点
数据库压力大:
text
频繁insert
频繁delete
性能远低于Redis。
六、数据库乐观锁(版本号)
严格来说:
它不是传统意义上的分布式锁
但经常用于解决并发问题。
1. 表结构
sql
CREATE TABLE stock(
id BIGINT,
stock INT,
version INT
);
数据:
text
stock = 100
version = 1
2. 查询
sql
SELECT stock,version
FROM stock
WHERE id = 1;
得到:
text
stock=100
version=1
3. 更新
sql
UPDATE stock
SET stock = stock - 1,
version = version + 1
WHERE id = 1
AND version = 1;
4. 原理
线程A:
text
version=1
线程B:
text
version=1
线程A先更新:
text
version=2
线程B再更新:
sql
WHERE version=1
匹配不到。
更新失败:
text
update count = 0
然后重试。
5. MyBatis Plus实现
实体:
java
@Version
private Integer version;
配置:
java
@Bean
public MybatisPlusInterceptor interceptor() {
MybatisPlusInterceptor interceptor =
new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(
new OptimisticLockerInnerInterceptor());
return interceptor;
}
更新时自动带:
sql
version = oldVersion
条件。
七、数据库悲观锁
利用数据库行锁。
1. 获取锁
sql
SELECT *
FROM stock
WHERE id = 1
FOR UPDATE;
2. 原理
事务提交前:
text
其他事务无法修改这条记录
例如:
java
@Transactional
public void deductStock(){
Stock stock =
stockMapper.selectForUpdate(1L);
stock.setCount(
stock.getCount()-1
);
stockMapper.update(stock);
}
3. 缺点
性能较差:
text
锁粒度大
阻塞严重
数据库压力高
八、ZooKeeper实现分布式锁
原理:
text
临时顺序节点
例如:
text
/lock/order0001
/lock/order0002
/lock/order0003
最小节点获得锁:
text
order0001
删除后:
text
order0002
获得锁。
常见框架:
text
Apache Curator
优点:
text
一致性强
可靠
缺点:
text
实现复杂
性能低于Redis
九、各种方案对比
| 方案 | 性能 | 实现难度 | 推荐程度 |
|---|---|---|---|
| synchronized | ★★★★★ | 简单 | 单机 |
| ReentrantLock | ★★★★★ | 简单 | 单机 |
| 数据库唯一索引 | ★★ | 简单 | 小项目 |
| 数据库悲观锁 | ★★ | 简单 | 一般 |
| 数据库乐观锁 | ★★★★ | 简单 | 库存扣减 |
| ZooKeeper | ★★★ | 复杂 | 大型系统 |
| Redis+Lua | ★★★★★ | 中等 | 推荐 |
| Redisson | ★★★★★ | 简单 | 生产推荐 |
十、单机环境如何实现锁
如果系统没有部署多个实例:
text
只有一个JVM
那么根本不需要分布式锁。
1. synchronized
java
public synchronized void createOrder() {
}
或者:
java
private final Object lock = new Object();
synchronized(lock){
}
特点:
text
JDK内置
自动释放
简单
2. ReentrantLock
java
private final ReentrantLock lock =
new ReentrantLock();
public void createOrder(){
lock.lock();
try{
//业务逻辑
}finally{
lock.unlock();
}
}
优点:
text
可重入
可中断
支持公平锁
支持超时
3. ReadWriteLock
读多写少场景:
java
private ReadWriteLock lock =
new ReentrantReadWriteLock();
读锁:
java
lock.readLock().lock();
写锁:
java
lock.writeLock().lock();
特点:
text
多个读线程同时执行
写线程独占
总结
分布式锁的核心目标只有一句话:
在多个 JVM、多个服务实例同时访问同一资源时,保证同一时刻只有一个节点执行关键业务逻辑。
实际生产中:
- 单机应用:
synchronized、ReentrantLock - 库存扣减:乐观锁(Version)
- 中小项目:Redis + Lua
- 企业级项目:Redisson
- 强一致性场景:ZooKeeper
学习路径,掌握:
synchronized → ReentrantLock → Redis SET NX EX → Lua释放锁 → Redisson看门狗 → 乐观锁Version → ZooKeeper临时顺序节点