难度:⭐⭐⭐⭐⭐ | 适合人群:想掌握分布式锁的开发者
💥 开场:一次"恐怖"的超卖事故
时间: 双11凌晨
地点: 公司作战室
事件: 秒杀活动
产品经理: "iPhone限量100台,0点开抢!"
我: "代码都准备好了,没问题!" 😎
0点整,秒杀开始...
00:00:01 - 订单数:50
00:00:02 - 订单数:100
00:00:03 - 订单数:150 ← 等等?怎么超了?
00:00:05 - 订单数:237 😱
产品经理(咆哮): "怎么回事??库存只有100台,怎么卖出去237台???"
我: "不可能啊,我加了库存判断..." 😰
紧急查看代码:
java
@Service
public class OrderService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean createOrder(Long productId, Long userId) {
String stockKey = "stock:product:" + productId;
// 1. 查询库存
String stockStr = redisTemplate.opsForValue().get(stockKey);
int stock = Integer.parseInt(stockStr);
// 2. 判断库存
if (stock <= 0) {
return false; // 没库存了
}
// 3. 扣减库存
stock--;
redisTemplate.opsForValue().set(stockKey, String.valueOf(stock));
// 4. 创建订单
orderDao.save(new Order(productId, userId));
return true;
}
}
哈吉米(紧急赶来): "你这代码有并发问题!"
我: "哪里有问题?" 🤔
南北绿豆画了个图:
但库存只减了1!
阿西噶阿西: "这就是经典的并发问题,需要加锁!"
我: "单机可以用synchronized,但现在是分布式部署,怎么办?" 😓
哈吉米: "这就需要分布式锁了!"
🎯 第一问:什么是分布式锁?
单机锁 vs 分布式锁
单机环境:
java
// 单机可以用synchronized
public synchronized void deductStock() {
int stock = getStock();
if (stock > 0) {
setStock(stock - 1); // 只有一个线程能执行
}
}
工作原理:
markdown
JVM内存中的锁对象
↓
线程1获取锁 → 执行代码 → 释放锁
线程2等待 → 获取锁 → 执行代码 → 释放锁
分布式环境:
arduino
服务器1(JVM1)
├─ 线程1:synchronized锁住了JVM1的对象
└─ 线程2:等待JVM1的锁
服务器2(JVM2)
├─ 线程3:synchronized锁住了JVM2的对象 ← 不同的锁!
└─ 线程4:等待JVM2的锁
线程1和线程3同时执行! ❌
synchronized失效!
南北绿豆: "分布式环境需要一个所有服务器都能访问的'外部锁'!"
分布式锁的特点
markdown
分布式锁需要满足:
1. 互斥性
- 同一时刻只有一个客户端能持有锁
2. 不会死锁
- 即使持有锁的客户端崩溃,也能释放锁
3. 加锁和解锁必须是同一个客户端
- 不能解别人的锁
4. 容错性
- 只要大部分Redis节点正常,就能加锁和解锁
分布式锁的实现方式
方式 | 优点 | 缺点 | 推荐度 |
---|---|---|---|
Redis | 性能高、简单 | 可能丢失锁 | ⭐⭐⭐⭐⭐ |
Zookeeper | 可靠性高 | 性能低、复杂 | ⭐⭐⭐⭐ |
数据库 | 简单、易理解 | 性能差 | ⭐⭐ |
Etcd | 可靠性高 | 运维复杂 | ⭐⭐⭐ |
哈吉米: "99%的场景用Redis分布式锁就够了!"
🔧 第二问:Redis分布式锁的演进
版本1:SETNX(最初版本)
java
public boolean lock(String lockKey) {
// SETNX:key不存在才设置
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked");
return Boolean.TRUE.equals(result);
}
public void unlock(String lockKey) {
redisTemplate.delete(lockKey);
}
// 使用
if (lock("lock:product:123")) {
try {
// 业务逻辑
} finally {
unlock("lock:product:123");
}
}
问题1:死锁!
其他客户端永远获取不到锁
死锁!
版本2:SETNX + EXPIRE(有缺陷)
java
public boolean lock(String lockKey) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked");
if (Boolean.TRUE.equals(result)) {
// 设置过期时间
redisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
return true;
}
return false;
}
问题2:非原子操作!
又死锁了!
版本3:SET EX NX(原子操作)
java
public boolean lock(String lockKey, String lockValue, long expireTime) {
// SET key value EX seconds NX
// 一条命令完成:设置值 + 过期时间 + 不存在才设置
Boolean result = redisTemplate.opsForValue().setIfAbsent(
lockKey,
lockValue,
expireTime,
TimeUnit.SECONDS
);
return Boolean.TRUE.equals(result);
}
public void unlock(String lockKey, String lockValue) {
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (lockValue.equals(currentValue)) {
redisTemplate.delete(lockKey);
}
}
// 使用
String lockValue = UUID.randomUUID().toString();
if (lock("lock:product:123", lockValue, 10)) {
try {
// 业务逻辑
} finally {
unlock("lock:product:123", lockValue);
}
}
改进:
- ✅ 原子操作
- ✅ 自动过期
- ✅ 防止误删(检查lockValue)
问题3:误删别人的锁!
版本4:Lua脚本保证原子性
java
public void unlock(String lockKey, String lockValue) {
// Lua脚本:判断和删除必须原子执行
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
lockValue
);
}
Lua脚本的优势:
- ✅ 在Redis中原子执行
- ✅ 判断和删除一气呵成
- ✅ 不会误删
问题4:锁续期!
diff
业务执行时间不确定:
- 锁过期时间:10秒
- 业务执行时间:15秒
10秒后锁过期 → 其他客户端获取锁 → 并发问题
需要自动续期(Watch Dog)!
版本5:Redisson(推荐,生产级)
阿西噶阿西: "Redisson已经帮我们实现了所有细节!"
添加依赖:
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.20.0</version>
</dependency>
配置:
yaml
spring:
redis:
host: localhost
port: 6379
使用:
java
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public boolean createOrder(Long productId, Long userId) {
// 获取锁对象
RLock lock = redissonClient.getLock("lock:product:" + productId);
try {
// 尝试加锁
// 参数:等待时间、锁自动释放时间、时间单位
boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (locked) {
System.out.println("获取锁成功");
// 业务逻辑
int stock = getStock(productId);
if (stock > 0) {
deductStock(productId);
saveOrder(productId, userId);
return true;
}
return false;
} else {
System.out.println("获取锁失败");
return false;
}
} catch (InterruptedException e) {
e.printStackTrace();
return false;
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println("释放锁成功");
}
}
}
}
Redisson的优势:
✅ 自动续期(Watch Dog机制)
✅ 可重入锁
✅ 公平锁/非公平锁
✅ 读写锁
✅ 联锁(MultiLock)
✅ 红锁(RedLock)
✅ 自动释放(防死锁)
🐶 第三问:Watch Dog自动续期机制
什么是Watch Dog?
哈吉米: "Redisson的看门狗机制,自动给锁续命!"
工作原理:
每10秒检查一次 loop 每10秒 WatchDog->>WatchDog: 检查锁是否还持有 WatchDog->>Redis: 续期(重置为30秒) end Note over 客户端: 业务执行完成 客户端->>Redis: 释放锁 deactivate WatchDog Note over WatchDog: 停止Watch Dog
源码分析(简化版):
java
// Redisson的Watch Dog机制
private void renewExpiration() {
// 定时任务:每 internalLockLeaseTime/3 执行一次
// 默认30秒,所以每10秒执行
Timeout task = commandExecutor.getConnectionManager().newTimeout(timeout -> {
// 续期锁
renewExpirationAsync(threadId);
}, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
}
private void renewExpirationAsync(long threadId) {
// 执行Lua脚本,重置过期时间为30秒
String script =
"if redis.call('hexists', KEYS[1], ARGV[2]) == 1 then " +
" redis.call('pexpire', KEYS[1], ARGV[1]); " +
" return 1; " +
"else " +
" return 0; " +
"end";
// ...
}
效果:
makefile
加锁时间:00:00:00,过期时间30秒
↓
00:00:10 - Watch Dog续期 → 过期时间变成00:00:40
↓
00:00:20 - Watch Dog续期 → 过期时间变成00:00:50
↓
00:00:25 - 业务执行完成,主动释放锁
↓
Watch Dog停止
再也不用担心业务执行时间长了! ✨
🔄 第四问:可重入锁
什么是可重入?
南北绿豆: "可重入就是同一个线程可以多次获取同一把锁。"
场景:
java
public void method1() {
RLock lock = redissonClient.getLock("lock:test");
lock.lock();
try {
System.out.println("method1 获取锁");
method2(); // 调用method2
} finally {
lock.unlock();
}
}
public void method2() {
RLock lock = redissonClient.getLock("lock:test"); // 同一把锁
lock.lock(); // 再次获取锁
try {
System.out.println("method2 获取锁");
// 业务逻辑
} finally {
lock.unlock();
}
}
时序图:
Redisson实现原理:
java
// Redisson使用Hash结构存储锁
HSET lock:test threadId 1 // 加锁,计数1
HSET lock:test threadId 2 // 再次加锁,计数2
HINCRBY lock:test threadId -1 // 释放锁,计数1
HINCRBY lock:test threadId -1 // 释放锁,计数0,删除
💻 第五问:完整实战 - 秒杀场景
秒杀业务代码
java
@Service
public class SeckillService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private OrderDao orderDao;
/**
* 秒杀下单
*/
public boolean seckill(Long productId, Long userId) {
String lockKey = "lock:seckill:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁(等待5秒,锁10秒后自动释放)
boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!locked) {
System.out.println("获取锁失败,请稍后重试");
return false;
}
System.out.println("获取锁成功:" + Thread.currentThread().getName());
// 1. 检查库存
String stockKey = "stock:product:" + productId;
String stockStr = redisTemplate.opsForValue().get(stockKey);
if (stockStr == null) {
return false;
}
int stock = Integer.parseInt(stockStr);
if (stock <= 0) {
System.out.println("库存不足");
return false;
}
// 2. 检查是否已经购买过(一人一单)
String userKey = "seckill:user:" + userId + ":product:" + productId;
Boolean bought = redisTemplate.hasKey(userKey);
if (Boolean.TRUE.equals(bought)) {
System.out.println("已经购买过了");
return false;
}
// 3. 扣减库存
Long newStock = redisTemplate.opsForValue().decrement(stockKey);
System.out.println("扣减库存成功,剩余:" + newStock);
// 4. 创建订单
Order order = new Order();
order.setProductId(productId);
order.setUserId(userId);
order.setCreateTime(new Date());
orderDao.save(order);
// 5. 标记已购买
redisTemplate.opsForValue().set(userKey, "1", 1, TimeUnit.DAYS);
System.out.println("秒杀成功:用户" + userId + ",订单" + order.getId());
return true;
} catch (InterruptedException e) {
e.printStackTrace();
return false;
} finally {
// 释放锁(必须是自己持有的锁)
if (lock.isHeldByCurrentThread()) {
lock.unlock();
System.out.println("释放锁成功");
}
}
}
}
并发测试
java
@Test
public void testSeckill() throws InterruptedException {
Long productId = 123L;
// 初始化库存
redisTemplate.opsForValue().set("stock:product:" + productId, "100");
// 模拟1000个用户并发秒杀
CountDownLatch latch = new CountDownLatch(1);
AtomicInteger successCount = new AtomicInteger(0);
ExecutorService executor = Executors.newFixedThreadPool(100);
for (int i = 1; i <= 1000; i++) {
final long userId = i;
executor.submit(() -> {
try {
latch.await(); // 等待,模拟同时请求
boolean success = seckillService.seckill(productId, userId);
if (success) {
successCount.incrementAndGet();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
Thread.sleep(1000);
System.out.println("准备开始秒杀...");
latch.countDown(); // 开始!
Thread.sleep(10000); // 等待执行完成
System.out.println("========== 秒杀结果 ==========");
System.out.println("成功下单:" + successCount.get() + " 个");
System.out.println("剩余库存:" + redisTemplate.opsForValue().get("stock:product:" + productId));
System.out.println("数据库订单数:" + orderDao.count());
executor.shutdown();
}
输出:
arduino
准备开始秒杀...
获取锁成功:pool-1-thread-1
扣减库存成功,剩余:99
秒杀成功:用户1,订单1001
释放锁成功
获取锁成功:pool-1-thread-2
扣减库存成功,剩余:98
秒杀成功:用户2,订单1002
释放锁成功
...
========== 秒杀结果 ==========
成功下单:100 个
剩余库存:0
数据库订单数:100
完全准确!没有超卖! ✅
🎨 第六问:Redisson高级特性
公平锁
java
// 公平锁:先来先得
RLock fairLock = redissonClient.getFairLock("lock:fair");
fairLock.lock();
try {
// 业务逻辑
} finally {
fairLock.unlock();
}
读写锁
java
RReadWriteLock rwLock = redissonClient.getReadWriteLock("lock:rw");
// 读锁(共享锁,多个线程可以同时持有)
RLock readLock = rwLock.readLock();
readLock.lock();
try {
// 读操作
} finally {
readLock.unlock();
}
// 写锁(排他锁,只有一个线程可以持有)
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
// 写操作
} finally {
writeLock.unlock();
}
时序图:
联锁(MultiLock)
java
// 同时锁定多个资源
RLock lock1 = redissonClient.getLock("lock:1");
RLock lock2 = redissonClient.getLock("lock:2");
RLock lock3 = redissonClient.getLock("lock:3");
// 联锁:必须同时获取所有锁
RLock multiLock = redissonClient.getMultiLock(lock1, lock2, lock3);
multiLock.lock();
try {
// 同时操作多个资源
} finally {
multiLock.unlock();
}
使用场景: 转账(同时锁定两个账户)
📊 第七问:三种分布式锁对比
Redis vs Zookeeper vs 数据库
维度 | Redis | Zookeeper | 数据库 |
---|---|---|---|
性能 | 高 ⚡⚡ | 中 ⚡ | 低 🐢 |
可靠性 | 中 | 高 ✅ | 中 |
实现复杂度 | 简单 | 复杂 | 简单 |
是否阻塞 | 非阻塞 | 阻塞 | 阻塞 |
Watch Dog | 有(Redisson) | 不需要 | 不需要 |
推荐场景 | 高并发、对可靠性要求不极致 | 强一致性要求 | 并发低 |
推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ |
Redis分布式锁的问题
阿西噶阿西: "Redis锁也不是完美的。"
问题:主从切换时可能丢锁
(锁还没同步到从节点) Note over Redis从节点: 从节点升级为主节点 participant 客户端2 客户端2->>Redis从节点: 加锁(同一个key) Redis从节点-->>客户端2: 成功(因为没有这个锁) Note over 客户端1,客户端2: 两个客户端都持有锁!💥
解决方案:RedLock算法
java
// 使用RedLock(需要多个Redis实例)
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://redis1:6379");
RedissonClient client1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://redis2:6379");
RedissonClient client2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://redis3:6379");
RedissonClient client3 = Redisson.create(config3);
// RedLock:至少N/2+1个实例加锁成功才算成功
RLock lock1 = client1.getLock("lock:order");
RLock lock2 = client2.getLock("lock:order");
RLock lock3 = client3.getLock("lock:order");
RedissonRedLock redLock = new RedissonRedLock(lock1, lock2, lock3);
redLock.lock();
try {
// 业务逻辑
} finally {
redLock.unlock();
}
但: RedLock实现复杂,一般场景不需要
💡 最佳实践
1. 锁的粒度
java
// ❌ 不推荐:锁粒度太大
RLock lock = redissonClient.getLock("lock:order");
lock.lock();
try {
// 所有订单操作都用同一把锁,并发度低
} finally {
lock.unlock();
}
// ✅ 推荐:锁粒度细化
RLock lock = redissonClient.getLock("lock:order:" + orderId);
lock.lock();
try {
// 每个订单一把锁,并发度高
} finally {
lock.unlock();
}
2. 锁的超时时间
java
// ❌ 不推荐:时间太短
lock.tryLock(1, 2, TimeUnit.SECONDS); // 业务可能执行不完
// ❌ 不推荐:时间太长
lock.tryLock(100, 300, TimeUnit.SECONDS); // 锁持有时间过长
// ✅ 推荐:合理的时间 + Watch Dog
lock.tryLock(5, 30, TimeUnit.SECONDS);
// 等待5秒,锁30秒(Watch Dog会续期)
3. 异常处理
java
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
// 业务逻辑
} catch (BusinessException e) {
// 业务异常处理
throw e;
} catch (Exception e) {
// 系统异常处理
log.error("系统异常", e);
throw new SystemException("系统错误");
} finally {
// 确保释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
4. 监控
java
// 记录锁的持有时间
long start = System.currentTimeMillis();
lock.lock();
try {
// 业务逻辑
} finally {
long cost = System.currentTimeMillis() - start;
// 如果持有锁超过5秒,记录警告
if (cost > 5000) {
log.warn("锁持有时间过长:{}ms,lockKey:{}", cost, lockKey);
}
lock.unlock();
}
💡 知识点总结
分布式锁核心要点
✅ 为什么需要分布式锁?
- 分布式环境synchronized失效
- 需要外部锁协调多个服务器
✅ Redis锁的演进
- SETNX - 会死锁
- SETNX + EXPIRE - 非原子
- SET EX NX - 原子,但可能误删
- Lua脚本 - 原子删除
- Redisson - 生产级(推荐)
✅ Redisson特性
- Watch Dog自动续期
- 可重入锁
- 公平锁/非公平锁
- 读写锁
- 联锁、红锁
✅ 实战场景
- 秒杀防超卖
- 库存扣减
- 防止重复提交
- 定时任务防重复执行
✅ 注意事项
- 锁粒度要细
- 超时时间要合理
- 必须在finally中释放
- 只释放自己的锁
记忆口诀
分布式锁很重要,
Redis实现最常用。
SETNX加EXPIRE,
原子操作要保证。
Lua脚本删锁好,
判断删除一起搞。
Redisson是神器,
自动续期看门狗。
秒杀防超卖,
库存扣减要加锁。
🤔 常见面试题
Q1: Redis分布式锁如何实现?
A:
vbnet
基础实现:
SET lock_key unique_value EX 10 NX
- EX 10:10秒后自动过期
- NX:key不存在才设置
- unique_value:唯一标识(UUID)
释放锁(Lua脚本):
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
生产推荐:Redisson
- 自动续期
- 可重入
- 更多特性
Q2: Redis锁有什么问题?
A:
diff
问题1:主从切换时可能丢锁
- 主节点加锁
- 锁还没同步到从节点
- 主节点宕机
- 从节点升级,没有锁
- 其他客户端能加锁成功
解决:RedLock(多个独立Redis实例)
问题2:业务执行时间长,锁过期
解决:Watch Dog自动续期
问题3:误删别人的锁
解决:Lua脚本原子删除
Q3: 什么场景用分布式锁?
A:
markdown
典型场景:
1. 秒杀/抢购
- 防止超卖
2. 库存扣减
- 保证库存准确
3. 防止重复提交
- 订单重复创建
4. 定时任务
- 多实例只执行一次
5. 缓存重建
- 防止缓存击穿
💬 写在最后
从SETNX到Redisson,我们深入学习了Redis分布式锁:
- 🔐 理解了分布式锁的必要性
- 🔄 掌握了锁的演进过程
- 🐶 学会了Watch Dog机制
- 💻 完成了秒杀实战案例
这篇文章,希望能让你在生产环境中正确使用分布式锁!
如果这篇文章对你有帮助,请:
- 👍 点赞支持
- ⭐ 收藏备用
- 🔄 转发分享
- 💬 评论交流
下一篇我们聊Redis主从复制与哨兵! 👋