📖 开场:火车站的厕所
想象火车站只有一个厕所 🚽:
没有锁(并发问题):
小明推门进去 → 正在使用 🚶
小红也推门进去 → 尴尬了!😱
有锁(互斥):
小明进去 → 反锁门 🔒
小红推门 → 锁着的,等待 ⏰
小明出来 → 开锁 🔓
小红进去 → 反锁门 🔒 ✅
这就是锁的作用:保证同一时间只有一个人使用资源!
🤔 什么是分布式锁?
单机锁 vs 分布式锁
单机锁(Java的synchronized):
┌─────────────┐
│ JVM1 │
│ │
│ Thread1 ───┼─→ synchronized(lock) {
│ Thread2 ───┼─→ // 只有一个线程能进入
│ Thread3 ───┼─→ }
│ │
└─────────────┘
特点:只能在一个JVM内有效 ✅
分布式锁(跨JVM):
┌─────────────┐
│ JVM1 │──┐
│ Thread1 │ │
└─────────────┘ │
│ ┌──────────────┐
┌─────────────┐ │ │ Redis │
│ JVM2 │──┼────→│ 锁中心 │
│ Thread2 │ │ └──────────────┘
└─────────────┘ │
│ 只有一个线程能获取锁 🔒
┌─────────────┐ │
│ JVM3 │──┘
│ Thread3 │
└─────────────┘
特点:跨JVM、跨服务器 ✅
为什么需要分布式锁?
场景:秒杀扣减库存
商品库存:10件
没有分布式锁:
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Server1 │ │ Server2 │ │ Server3 │
│ 读库存 │ │ 读库存 │ │ 读库存 │
│ 10 │ │ 10 │ │ 10 │
│ -1 │ │ -1 │ │ -1 │
│ 写回9 │ │ 写回9 │ │ 写回9 │
└─────────┘ └─────────┘ └─────────┘
结果:实际卖了3件,库存显示9件(应该是7件)❌
有分布式锁:
Server1获取锁 → 读10 → -1 → 写回9 → 释放锁
↓
Server2获取锁 → 读9 → -1 → 写回8 → 释放锁
↓
Server3获取锁 → 读8 → -1 → 写回7 → 释放锁
结果:实际卖了3件,库存7件 ✅
🎯 分布式锁的四种实现方式
方式1:基于数据库(最简单)📦
原理
利用数据库的唯一索引
↓
插入成功 → 获取锁成功 🔒
插入失败(主键冲突)→ 获取锁失败 ❌
↓
删除记录 → 释放锁 🔓
建表
CREATE TABLE distributed_lock (
lock_name VARCHAR(64) NOT NULL COMMENT '锁名称',
holder VARCHAR(64) NOT NULL COMMENT '持有者',
expire_time BIGINT NOT NULL COMMENT '过期时间戳',
PRIMARY KEY (lock_name) -- ⭐ 唯一索引保证互斥
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
实现代码
@Component
@Slf4j
public class DatabaseDistributedLock {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* ⭐ 获取锁
*
* @param lockName 锁名称
* @param holder 持有者(通常是UUID)
* @param expireSeconds 过期时间(秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockName, String holder, int expireSeconds) {
try {
long expireTime = System.currentTimeMillis() + expireSeconds * 1000;
// ⭐ 插入锁记录
String sql = "INSERT INTO distributed_lock (lock_name, holder, expire_time) VALUES (?, ?, ?)";
jdbcTemplate.update(sql, lockName, holder, expireTime);
log.info("获取锁成功: lockName={}, holder={}", lockName, holder);
return true;
} catch (DuplicateKeyException e) {
// ⭐ 主键冲突,说明锁已被占用
log.info("获取锁失败(锁已被占用): lockName={}", lockName);
// ⭐ 检查是否过期,如果过期则尝试删除
tryDeleteExpiredLock(lockName);
return false;
}
}
/**
* ⭐ 释放锁
*/
public void unlock(String lockName, String holder) {
String sql = "DELETE FROM distributed_lock WHERE lock_name = ? AND holder = ?";
int rows = jdbcTemplate.update(sql, lockName, holder);
if (rows > 0) {
log.info("释放锁成功: lockName={}, holder={}", lockName, holder);
} else {
log.warn("释放锁失败(锁不存在或不属于当前持有者): lockName={}, holder={}", lockName, holder);
}
}
/**
* ⭐ 删除过期的锁
*/
private void tryDeleteExpiredLock(String lockName) {
long now = System.currentTimeMillis();
String sql = "DELETE FROM distributed_lock WHERE lock_name = ? AND expire_time < ?";
int rows = jdbcTemplate.update(sql, lockName, now);
if (rows > 0) {
log.info("删除过期的锁: lockName={}", lockName);
}
}
}
使用示例
@Service
@Slf4j
public class OrderService {
@Autowired
private DatabaseDistributedLock lock;
/**
* 扣减库存
*/
public boolean deductStock(Long productId, int quantity) {
String lockName = "stock:" + productId;
String holder = UUID.randomUUID().toString();
try {
// ⭐ 获取锁(超时时间10秒)
if (!lock.tryLock(lockName, holder, 10)) {
log.warn("获取锁失败,放弃扣减库存");
return false;
}
log.info("获取锁成功,开始扣减库存");
// ⭐ 业务逻辑(扣减库存)
int stock = getStock(productId);
if (stock < quantity) {
log.warn("库存不足: stock={}, need={}", stock, quantity);
return false;
}
setStock(productId, stock - quantity);
log.info("扣减库存成功: productId={}, quantity={}, remaining={}",
productId, quantity, stock - quantity);
return true;
} finally {
// ⭐ 释放锁
lock.unlock(lockName, holder);
}
}
private int getStock(Long productId) {
// 从数据库读取库存
return 100;
}
private void setStock(Long productId, int stock) {
// 写入数据库
}
}
优缺点
优点 ✅:
- 实现简单(利用数据库特性)
- 不需要额外的中间件
- 可以借助数据库事务
缺点 ❌:
- 性能差(数据库操作慢)
- 单点故障(数据库挂了,锁服务不可用)
- 过期时间不好控制(需要定时任务清理)
- 不可重入(同一个线程无法再次获取锁)
适用场景:
- 并发量不高
- 已有数据库,不想引入新中间件
- 对性能要求不高
方式2:基于Redis(最常用)⭐⭐⭐
原理
利用Redis的SETNX命令(SET if Not eXists)
↓
SETNX成功 → 获取锁成功 🔒
SETNX失败(key已存在)→ 获取锁失败 ❌
↓
DEL key → 释放锁 🔓
简单实现(有问题)❌
public boolean tryLock(String lockKey, String holder) {
// ⭐ SETNX:key不存在时设置,返回1;存在时不设置,返回0
Boolean success = redisTemplate.opsForValue().setIfAbsent(lockKey, holder);
return Boolean.TRUE.equals(success);
}
public void unlock(String lockKey) {
redisTemplate.delete(lockKey);
}
问题1:没有过期时间
获取锁成功 → 业务代码异常 → 没有释放锁
↓
锁永远无法释放 → 死锁!💀
问题2:释放了别人的锁
线程A获取锁 → 业务处理慢(超时)→ 锁过期自动释放
↓
线程B获取锁
↓
线程A处理完成 → 释放锁(实际释放的是线程B的锁)❌
正确实现(Lua脚本)✅
@Component
@Slf4j
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* ⭐ 获取锁
*
* @param lockKey 锁的key
* @param holder 持有者标识(UUID)
* @param expireSeconds 过期时间(秒)
* @return 是否获取成功
*/
public boolean tryLock(String lockKey, String holder, int expireSeconds) {
// ⭐ SETNX + 过期时间(原子操作)
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(lockKey, holder, expireSeconds, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(success)) {
log.info("获取锁成功: lockKey={}, holder={}", lockKey, holder);
return true;
} else {
log.info("获取锁失败(锁已被占用): lockKey={}", lockKey);
return false;
}
}
/**
* ⭐ 释放锁(Lua脚本,保证原子性)
*
* 逻辑:
* 1. 检查锁是否是自己的(holder匹配)
* 2. 是自己的才删除
*/
public void unlock(String lockKey, String holder) {
// ⭐ Lua脚本
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
// 执行脚本
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
holder
);
if (result != null && result == 1) {
log.info("释放锁成功: lockKey={}, holder={}", lockKey, holder);
} else {
log.warn("释放锁失败(锁不存在或不属于当前持有者): lockKey={}, holder={}", lockKey, holder);
}
}
/**
* ⭐ 尝试获取锁(带重试)
*
* @param lockKey 锁的key
* @param holder 持有者标识
* @param expireSeconds 过期时间(秒)
* @param retryTimes 重试次数
* @param retryInterval 重试间隔(毫秒)
* @return 是否获取成功
*/
public boolean tryLockWithRetry(String lockKey, String holder, int expireSeconds,
int retryTimes, long retryInterval) {
for (int i = 0; i < retryTimes; i++) {
if (tryLock(lockKey, holder, expireSeconds)) {
return true;
}
// 重试前等待
if (i < retryTimes - 1) {
try {
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
}
return false;
}
}
使用示例
@Service
@Slf4j
public class OrderService {
@Autowired
private RedisDistributedLock lock;
/**
* 扣减库存
*/
public boolean deductStock(Long productId, int quantity) {
String lockKey = "stock:lock:" + productId;
String holder = UUID.randomUUID().toString();
try {
// ⭐ 获取锁(10秒过期,重试3次,每次间隔100ms)
if (!lock.tryLockWithRetry(lockKey, holder, 10, 3, 100)) {
log.warn("获取锁失败,放弃扣减库存");
return false;
}
log.info("获取锁成功,开始扣减库存");
// ⭐ 业务逻辑
int stock = getStockFromRedis(productId);
if (stock < quantity) {
log.warn("库存不足");
return false;
}
setStockToRedis(productId, stock - quantity);
log.info("扣减库存成功");
return true;
} finally {
// ⭐ 释放锁
lock.unlock(lockKey, holder);
}
}
private int getStockFromRedis(Long productId) {
String stock = redisTemplate.opsForValue().get("stock:" + productId);
return stock != null ? Integer.parseInt(stock) : 0;
}
private void setStockToRedis(Long productId, int stock) {
redisTemplate.opsForValue().set("stock:" + productId, String.valueOf(stock));
}
}
Redisson实现(推荐)⭐⭐⭐
Redisson = Redis官方推荐的Java客户端,内置了分布式锁
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setPassword("yourpassword");
return Redisson.create(config);
}
}
使用Redisson:
@Service
@Slf4j
public class OrderService {
@Autowired
private RedissonClient redissonClient;
/**
* 扣减库存
*/
public boolean deductStock(Long productId, int quantity) {
// ⭐ 获取锁对象
RLock lock = redissonClient.getLock("stock:lock:" + productId);
try {
// ⭐ 尝试获取锁
// tryLock(等待时间, 过期时间, 时间单位)
boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);
if (!acquired) {
log.warn("获取锁失败");
return false;
}
log.info("获取锁成功");
// ⭐ 业务逻辑
int stock = getStockFromRedis(productId);
if (stock < quantity) {
return false;
}
setStockToRedis(productId, stock - quantity);
return true;
} catch (InterruptedException e) {
log.error("获取锁被中断", e);
return false;
} finally {
// ⭐ 释放锁(自动检查是否是自己的锁)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
Redisson的Watch Dog机制 🐕
问题:业务处理时间超过锁的过期时间怎么办?
解决:Redisson的Watch Dog自动续期
获取锁(过期时间10秒)
↓
业务处理中...(5秒)
↓
Watch Dog检测到锁还在持有,自动续期到10秒
↓
业务处理中...(5秒)
↓
Watch Dog再次续期到10秒
↓
业务处理完成
↓
释放锁,停止Watch Dog ✅
实现原理:
// 获取锁时,不指定过期时间,Redisson会使用默认的30秒,并启动Watch Dog
RLock lock = redissonClient.getLock("myLock");
lock.lock(); // ⭐ 不指定过期时间,启动Watch Dog
// Watch Dog会每10秒(30秒的1/3)检查一次
// 如果锁还在持有,自动续期到30秒
优缺点
优点 ✅:
- 性能高(Redis内存操作)
- 可以设置过期时间(防止死锁)
- 支持自动续期(Redisson的Watch Dog)
- 支持可重入锁(Redisson)
缺点 ❌:
- Redis单机有单点故障风险
- Redis主从切换时可能丢锁(见Redlock算法)
适用场景:
- 高并发场景(秒杀、抢购)⭐⭐⭐
- 对性能要求高
- 大部分分布式锁场景(最常用)
方式3:基于Zookeeper(最可靠)🦓
原理
利用Zookeeper的临时顺序节点
↓
创建临时顺序节点:/locks/lock_0000000001
/locks/lock_0000000002
/locks/lock_0000000003
↓
序号最小的节点获取锁 🔒
其他节点监听前一个节点(watch机制)
↓
最小节点删除(业务完成或客户端断开)
↓
下一个节点收到通知,获取锁 ✅
实现代码(Curator)
依赖:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.2.1</version>
</dependency>
配置:
@Configuration
public class ZookeeperConfig {
@Bean
public CuratorFramework curatorFramework() {
// 重试策略:最多重试3次,每次间隔1秒
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("localhost:2181")
.sessionTimeoutMs(5000)
.connectionTimeoutMs(5000)
.retryPolicy(retryPolicy)
.build();
client.start();
return client;
}
}
使用:
@Service
@Slf4j
public class OrderService {
@Autowired
private CuratorFramework curatorFramework;
/**
* 扣减库存
*/
public boolean deductStock(Long productId, int quantity) {
String lockPath = "/locks/stock/" + productId;
// ⭐ 创建可重入锁
InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
try {
// ⭐ 尝试获取锁(最多等待3秒)
if (!lock.acquire(3, TimeUnit.SECONDS)) {
log.warn("获取锁失败");
return false;
}
log.info("获取锁成功");
// ⭐ 业务逻辑
int stock = getStock(productId);
if (stock < quantity) {
return false;
}
setStock(productId, stock - quantity);
return true;
} catch (Exception e) {
log.error("处理失败", e);
return false;
} finally {
// ⭐ 释放锁
try {
lock.release();
} catch (Exception e) {
log.error("释放锁失败", e);
}
}
}
}
优缺点
优点 ✅:
- 可靠性高(Zookeeper的CP特性)
- 自动删除临时节点(客户端断开连接)
- 公平锁(按顺序获取锁)
- 不会丢锁(强一致性)
缺点 ❌:
- 性能较Redis差(磁盘IO)
- 需要部署和维护Zookeeper集群
- 实现相对复杂
适用场景:
- 对可靠性要求极高
- 不能容忍丢锁
- 已有Zookeeper集群
方式4:Redlock算法(Redis集群)🔴
原理
问题:Redis单机有单点故障
Redlock算法:在多个独立的Redis实例上获取锁
N个独立的Redis实例(N=5)
获取锁的步骤:
1. 获取当前时间戳t1
2. 依次尝试在N个Redis实例上获取锁(设置很短的超时时间,如5ms)
3. 获取当前时间戳t2
4. 计算获取锁的耗时:t2 - t1
5. 判断是否成功:
- 在大多数实例(N/2+1=3个)上获取到锁
- 获取锁的耗时 < 锁的有效时间
6. 如果成功,锁的有效时间 = 原有效时间 - 获取锁的耗时
7. 如果失败,释放所有已获取的锁
实现(Redisson支持)
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
// ⭐ 配置多个Redis实例
config.useReplicatedServers()
.addNodeAddress(
"redis://127.0.0.1:6379",
"redis://127.0.0.1:6380",
"redis://127.0.0.1:6381",
"redis://127.0.0.1:6382",
"redis://127.0.0.1:6383"
);
return Redisson.create(config);
}
}
使用:
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public boolean deductStock(Long productId, int quantity) {
// ⭐ 获取RedLock(Redisson自动实现Redlock算法)
RLock lock1 = redissonClient.getLock("lock1:" + productId);
RLock lock2 = redissonClient.getLock("lock2:" + productId);
RLock lock3 = redissonClient.getLock("lock3:" + productId);
// ⭐ RedissonRedLock
RLock redLock = redissonClient.getRedLock(lock1, lock2, lock3);
try {
if (!redLock.tryLock(3, 10, TimeUnit.SECONDS)) {
return false;
}
// 业务逻辑
return true;
} catch (InterruptedException e) {
return false;
} finally {
redLock.unlock();
}
}
}
优缺点
优点 ✅:
- 高可用(多个Redis实例)
- 不会因为单个Redis故障而丢锁
缺点 ❌:
- 实现复杂
- 需要多个独立的Redis实例(成本高)
- 性能比单Redis差(需要在多个实例上操作)
适用场景:
- 对可用性要求极高
- 不能容忍Redis单点故障
- 预算充足(多个Redis实例)
📊 四种方式对比
| 实现方式 | 性能 | 可靠性 | 复杂度 | 成本 | 适用场景 |
|---|---|---|---|---|---|
| 数据库 | ⭐ 低 | ⭐⭐ 中 | ⭐ 低 | ⭐ 低 | 并发低,简单场景 |
| Redis | ⭐⭐⭐ 高 | ⭐⭐ 中 | ⭐⭐ 中 | ⭐⭐ 中 | 大部分场景(推荐) ⭐ |
| Zookeeper | ⭐⭐ 中 | ⭐⭐⭐ 高 | ⭐⭐⭐ 高 | ⭐⭐⭐ 高 | 可靠性要求极高 |
| Redlock | ⭐⭐ 中 | ⭐⭐⭐ 高 | ⭐⭐⭐ 高 | ⭐⭐⭐ 高 | 高可用 + 高可靠 |
🎓 面试题速答
Q1: 分布式锁有哪些实现方式?
A: 四种方式!
- 数据库:唯一索引实现互斥
- Redis:SETNX + Lua脚本(最常用)⭐
- Zookeeper:临时顺序节点(最可靠)
- Redlock:多个Redis实例(高可用)
推荐使用Redis(Redisson),性能和可靠性的最佳平衡
Q2: Redis分布式锁如何实现?
A: 核心:SETNX + 过期时间 + Lua脚本
// 1. 获取锁(SETNX + 过期时间,原子操作)
redisTemplate.opsForValue()
.setIfAbsent(lockKey, holder, 10, TimeUnit.SECONDS);
// 2. 释放锁(Lua脚本,保证原子性)
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
关键点:
- 设置过期时间(防止死锁)
- Lua脚本释放锁(防止释放别人的锁)
- holder标识(UUID,确保只释放自己的锁)
Q3: Redis分布式锁有什么问题?
A: 三个主要问题:
-
单点故障:
- 问题:Redis挂了,锁服务不可用
- 解决:Redlock算法(多个Redis实例)
-
主从切换丢锁:
- 问题:主库获取锁后,还没同步到从库就挂了,从库升主后,锁丢失
- 解决:Redlock算法
-
业务超时:
- 问题:业务处理时间超过锁的过期时间
- 解决:Redisson的Watch Dog自动续期
Q4: Redisson的Watch Dog是什么?
A: Watch Dog = 自动续期机制 🐕
// 获取锁时不指定过期时间
RLock lock = redissonClient.getLock("myLock");
lock.lock(); // ⭐ 默认30秒,启动Watch Dog
// Watch Dog每10秒(30秒的1/3)检查一次
// 如果锁还在持有,自动续期到30秒
作用:
- 防止业务处理时间过长,锁自动过期
- 自动续期,不需要手动设置很长的过期时间
原理:
- 后台线程定时检查
- 如果锁还在持有,重置过期时间
Q5: Zookeeper分布式锁有什么优势?
A: 最大优势:可靠性高!
-
临时节点:
- 客户端断开连接,节点自动删除
- 不会出现死锁
-
强一致性:
- Zookeeper的CP特性(一致性优先)
- 不会丢锁
-
公平锁:
- 按照创建顺序获取锁
- 避免饥饿
劣势:
- 性能较Redis差
- 需要部署和维护Zookeeper集群
Q6: 如何选择分布式锁的实现方式?
A: 根据场景选择!
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 秒杀、抢购 | Redis(Redisson) | 性能高 ⭐⭐⭐ |
| 扣减库存 | Redis | 性能 + 可靠性 ⭐⭐ |
| 订单处理 | Redis | 性能足够 ⭐⭐ |
| 金融交易 | Zookeeper | 可靠性第一 ⭐⭐⭐ |
| 配置更新 | Zookeeper | 强一致性 ⭐⭐⭐ |
| 简单定时任务 | 数据库 | 简单,成本低 ⭐ |
总结:
- 大部分场景:Redis(Redisson)⭐⭐⭐
- 高可靠场景:Zookeeper
- 简单场景:数据库
🎬 总结
分布式锁四种实现方式
┌─────────────────────────────────────┐
│ 数据库锁 │
│ 唯一索引 + INSERT │
│ ⭐ 简单,性能差 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Redis锁(推荐)⭐⭐⭐ │
│ SETNX + Lua脚本 │
│ ⭐⭐⭐ 高性能,常用 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Zookeeper锁 │
│ 临时顺序节点 │
│ ⭐⭐⭐ 最可靠,性能稍差 │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Redlock算法 │
│ 多个Redis实例 │
│ ⭐⭐⭐ 高可用,成本高 │
└─────────────────────────────────────┘
大部分场景用Redis(Redisson)!✅
🎉 恭喜你!
你已经完全掌握了分布式锁的四种实现方式!🎊
核心要点:
- 数据库:简单,性能差
- Redis:常用,推荐 ⭐⭐⭐
- Zookeeper:可靠,复杂
- Redlock:高可用,成本高
下次面试,这样回答:
"分布式锁有四种实现方式:数据库、Redis、Zookeeper和Redlock算法。
最常用的是Redis实现,使用SETNX命令获取锁,设置过期时间防止死锁,用Lua脚本释放锁保证原子性。推荐使用Redisson框架,它内置了Watch Dog自动续期机制,解决了业务处理时间过长的问题。
Zookeeper实现可靠性最高,利用临时顺序节点,客户端断开连接节点自动删除,不会死锁,而且是公平锁。但性能较Redis差,需要维护Zookeeper集群。
Redlock算法解决了Redis单点故障问题,在多个独立的Redis实例上获取锁,大多数实例(N/2+1)获取成功才算成功。
我们项目的秒杀系统使用Redisson实现分布式锁,扣减库存时保证并发安全,QPS达到10万。"
面试官:👍 "很好!你对分布式锁理解很全面!"
本文完 🎬
上一篇: 194-消息队列如何保证消息有序性.md
下一篇: 196-Redis实现分布式锁的细节和问题.md
作者注:写完这篇,我都想去火车站当厕所管理员了!🚽
如果这篇文章对你有帮助,请给我一个Star⭐!