第 12 章:分布式锁
学习目标
- 理解分布式锁的核心问题
- 掌握 Redis 分布式锁(Redlock)和 ZK 锁
- 学会用 Redisson 简化锁的实现
- 避免锁误删、锁失效、并发的坑
一、为什么需要分布式锁?
java
// ❌ 单机锁在多节点下失效
public synchronized void deductStock() {
if (stock > 0) {
stock--;
}
}
// 3 个 JVM 各有不同的锁,同一商品被扣 3 次text
[商品库存 = 1]
├─ 节点 A:加锁(进程 A 的锁) → 扣
├─ 节点 B:加锁(进程 B 的锁) → 扣
└─ 节点 C:加锁(进程 C 的锁) → 扣
实际库存 = -2 ❌二、Redis SET NX 实现
java
public class RedisLock {
@Resource
private StringRedisTemplate redis;
public boolean tryLock(String key, String value, long expireSeconds) {
Boolean ok = redis.opsForValue()
.setIfAbsent(key, value, Duration.ofSeconds(expireSeconds));
return Boolean.TRUE.equals(ok);
}
public void unlock(String key, String value) {
// Lua 脚本:先校验再删除
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
DefaultRedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);
redis.execute(script, List.of(key), value);
}
}java
// 使用
public void deductStock(Long productId) {
String lockKey = "lock:stock:" + productId;
String lockValue = UUID.randomUUID().toString();
if (lock.tryLock(lockKey, lockValue, 30)) {
try {
int stock = jdbc.queryForObject("SELECT stock FROM product WHERE id = ?", Integer.class, productId);
if (stock > 0) {
jdbc.update("UPDATE product SET stock = stock - 1 WHERE id = ?", productId);
}
} finally {
lock.unlock(lockKey, lockValue); // 删自己的锁
}
}
}⚠️ 坑 1:
unlock不校验 value 直接del,删了别人的锁。必须用 Lua 脚本保证"Get + Del"原子。
三、Redisson 简化
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>java
@Service
public class StockService {
@Resource
private RedissonClient redisson;
public void deductStock(Long productId) {
String lockKey = "lock:stock:" + productId;
RLock lock = redisson.getLock(lockKey);
try {
// 最多等 5 秒,锁 30 秒自动过期
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
try {
// 业务逻辑
int stock = jdbc.queryForObject("SELECT stock FROM product WHERE id = ?", Integer.class, productId);
if (stock > 0) {
jdbc.update("UPDATE product SET stock = stock - 1 WHERE id = ?", productId);
}
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}⚠️ 坑 2:
lock.lock(30, TimeUnit.SECONDS)客户端断网时,锁过期被别的线程拿到,你的代码继续unlock删了别人的锁。用tryLock,业务超时自动退出。
四、可重入锁
java
// Redisson 可重入
public void methodA() {
RLock lock = redisson.getLock("lock:order");
lock.lock();
try {
methodB(); // 内部再次获取同一把锁,可重入
} finally {
lock.unlock();
}
}
public void methodB() {
RLock lock = redisson.getLock("lock:order");
lock.lock(); // ✅ 重入成功
try {
// ...
} finally {
lock.unlock(); // 配对释放
}
}底层用 hincrby 计数,加一次 +1,释放一次 -1,锁到 0 才真释放。
五、读写锁
java
// 读写分离场景:商品详情
// 读多写少,用读写锁提升性能
public void updateProduct(Long productId, ProductDto dto) {
RReadWriteLock rwLock = redisson.getReadWriteLock("lock:product:" + productId);
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
productRepository.update(productId, dto);
} finally {
writeLock.unlock();
}
}
public Product getProduct(Long productId) {
RReadWriteLock rwLock = redisson.getReadWriteLock("lock:product:" + productId);
RLock readLock = rwLock.readLock();
readLock.lock();
try {
return productRepository.findById(productId); // 多个 reader 可并发
} finally {
readLock.unlock();
}
}六、公平锁
java
// 默认非公平锁:谁先抢到谁拿
// 公平锁:按请求顺序排队
RLock fairLock = redisson.getFairLock("lock:order");
fairLock.lock();
// 100 个线程按顺序拿到锁,不会出现"饿死"⚠️ 坑 3:高并发场景用公平锁,性能急剧下降(1000 QPS 降到 100)。默认非公平锁足矣,用业务幂等保证正确性。
七、Redlock(多 Redis 节点)
java
// 单 Redis 节点不可靠,挂了就锁失效
// Redlock:在 N 个独立 Redis 节点上加锁,过半成功才算获取
Config config = new Config();
config.useClusterServers()
.addNodeAddress("redis://redis1:6379")
.addNodeAddress("redis://redis2:6379")
.addNodeAddress("redis://redis3:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("lock:order");
lock.lock();
// 至少 2/3 节点加锁成功才算有效Martin 大佬的 Redlock 算法争议较大,生产慎用。推荐用 Zookeeper 或 DB 锁 用于强一致场景。
八、Zookeeper 分布式锁
java
// Curator 框架
@Bean
public CuratorFramework curatorFramework() {
return CuratorFrameworkFactory.builder()
.connectString("zk1:2181,zk2:2181,zk3:2181")
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
}
public void withLock(String key, Runnable task) {
InterProcessMutex lock = new InterProcessMutex(curatorFramework, "/lock/" + key);
try {
if (lock.acquire(5, TimeUnit.SECONDS)) {
try {
task.run();
} finally {
lock.release();
}
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}text
Zookeeper vs Redis
├── ZK: 强一致,临时顺序节点,业务可用性略低
└── Redis: 高可用,SET NX,极端情况下可能锁失效本章小结
| 方案 | 适用 |
|---|---|
| Redis SET NX | 性能优先,容忍偶发失效 |
| Redisson | 锁的瑞士军刀,推荐 |
| Zookeeper | 强一致,适合金融 |
| 数据库 | 性能差,适合低频 |
| 关键点 | 经验 |
|---|---|
| 超时 | 业务执行时间的 3 倍 |
| 值 | UUID 唯一,防误删 |
| 续期 | Redisson Watchdog 自动续期 |
| 锁粒度 | 越细越好,不要锁整个商品库 |
动手练习
- SET NX 锁:实现
RedisLock,带 Lua 脚本原子解锁 - Redisson 接入:用 Redisson 改写商品扣库存,验证并发安全
- 可重入:写一个
methodA→methodB的调用链,验证可重入 - 读写锁:商品详情接口加上读写锁,模拟 100 并发读 + 1 写
下一章:第 13 章:分布式 ID →