第 195 章:Redis 分布式锁
学习目标
- 理解分布式锁原理
- 掌握 Redisson 实战
- 学会可重入锁、公平锁
- 了解 RedLock 算法
一、为什么需要分布式锁
单机用 synchronized / ReentrantLock 就够,但多机部署时锁状态不共享,需要分布式锁。
二、分布式锁基本要求
| 特性 | 说明 |
|---|---|
| 互斥 | 任何时刻只能一个客户端持有 |
| 不死锁 | 持锁方崩溃,锁能释放 |
| 容错 | Redis 部分节点挂了,锁仍可用 |
| 可重入 | 同一线程可多次获取 |
| 公平 | 按顺序获取(可选) |
三、最简单的 SETNX 实现
3.1 加锁
java
public boolean tryLock(String key, String value, long expireMs) {
Boolean ok = redisTemplate.opsForValue().setIfAbsent(key, value, expireMs, TimeUnit.MILLISECONDS);
return Boolean.TRUE.equals(ok);
}关键
- 必须用
SET key value NX PX expireMs单条命令(原子) - 不能先
SETNX再EXPIRE(两条命令,中间崩溃就死锁)
3.2 解锁(必须验证 value)
java
private static final String UNLOCK_LUA =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
public void unlock(String key, String value) {
redisTemplate.execute(new DefaultRedisScript<>(UNLOCK_LUA, Long.class),
Collections.singletonList(key), value);
}不要这样做
java
// ❌ 没校验 value,可能误删别人的锁
public void unlock(String key) {
redisTemplate.delete(key);
}3.3 完整实现
java
public class SimpleRedisLock {
private static final String UNLOCK_SCRIPT =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
"return redis.call('DEL', KEYS[1]) else return 0 end";
private final RedisTemplate<String, String> redisTemplate;
private final String lockKey;
private final String lockValue;
private final long expireMs;
private volatile boolean locked = false;
public SimpleRedisLock(RedisTemplate<String, String> redisTemplate,
String lockKey, long expireMs) {
this.redisTemplate = redisTemplate;
this.lockKey = lockKey;
this.lockValue = UUID.randomUUID().toString();
this.expireMs = expireMs;
}
public boolean tryLock() {
Boolean ok = redisTemplate.opsForValue().setIfAbsent(
lockKey, lockValue, expireMs, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(ok)) {
locked = true;
return true;
}
return false;
}
public void unlock() {
if (!locked) return;
redisTemplate.execute(new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class),
Collections.singletonList(lockKey), lockValue);
}
}四、Redisson(企业首选)
4.1 引入
xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.4</version>
</dependency>4.2 配置
yaml
spring:
redis:
host: localhost
port: 6379java
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redisson() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setPassword("yourpass")
.setDatabase(0);
return Redisson.create(config);
}
}4.3 基础用法
java
@Autowired
private RedissonClient redissonClient;
public void doBusiness() {
RLock lock = redissonClient.getLock("order:lock:123");
try {
// 尝试加锁,最多等 5s,锁 10s 自动过期
boolean ok = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!ok) {
throw new BusinessException("系统繁忙");
}
// 业务逻辑
orderService.process();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}4.4 锁特性
4.5 看门狗(WatchDog)
自动续期:锁默认 30s,后台线程每 10s 检查,业务没完就续到 30s。
java
// 默认行为
lock.tryLock(); // 不传时间 → 用看门狗(30s 锁,后台续期)
// lock.lock(); // 阻塞等,默认 30s
// 不用看门狗
lock.tryLock(5, 10, TimeUnit.SECONDS); // 显式 10s,到期自动释放4.6 公平锁
按请求顺序排队:
java
RLock fairLock = redissonClient.getFairLock("resource:1");
fairLock.lock();
try {
// 业务
} finally {
fairLock.unlock();
}4.7 读写锁
java
RReadWriteLock rwLock = redissonClient.getReadWriteLock("cache:user");
// 读锁(可并发)
RLock readLock = rwLock.readLock();
readLock.lock();
try {
return redisTemplate.opsForValue().get("user:1");
} finally {
readLock.unlock();
}
// 写锁(排他)
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
redisTemplate.opsForValue().set("user:1", user);
} finally {
writeLock.unlock();
}4.8 信号量(Semaphore)
java
RSemaphore semaphore = redissonClient.getSemaphore("resource");
// 限流:同时最多 10 个
boolean ok = semaphore.tryAcquire();
if (ok) {
try {
// 业务
} finally {
semaphore.release();
}
}4.9 CountDownLatch
java
RCountDownLatch latch = redissonClient.getCountDownLatch("task");
latch.trySetCount(3);
// 3 个子任务完成
latch.countDown();
// 主线程等
latch.await(10, TimeUnit.SECONDS);五、RedLock 算法(高可用)
主从架构下,主挂了从还没同步,锁可能丢失。RedLock 用多个独立 Redis 实例:
java
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.1.1:6379");
RedissonClient client1 = Redisson.create(config1);
// ... 多个
RedissonRedLock redLock = new RedissonRedLock(
lock1, lock2, lock3, lock4, lock5);
redLock.lock();
try {
// 大多数(N/2 + 1)节点加锁成功才算成功
} finally {
redLock.unlock();
}性能
RedLock 性能差,大多数场景主从 + 自动续期足够。
六、实战:秒杀场景
java
@Service
public class SeckillService {
@Autowired
private RedissonClient redisson;
@Autowired
private GoodsMapper goodsMapper;
@Transactional
public void seckill(Long userId, Long goodsId) {
// 1. 分布式锁防超卖
RLock lock = redisson.getLock("seckill:" + goodsId);
try {
if (!lock.tryLock(3, 10, TimeUnit.SECONDS)) {
throw new BusinessException("系统繁忙");
}
// 2. 查库存
Goods goods = goodsMapper.selectById(goodsId);
if (goods.getStock() <= 0) {
throw new BusinessException("已售罄");
}
// 3. 扣库存
goodsMapper.decrementStock(goodsId);
// 4. 创建订单
orderMapper.insert(new Order(userId, goodsId));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
}七、Redisson 最佳实践
java
// ✅ 用 tryLock 不用 lock(避免阻塞)
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { ... }
// ✅ 检查 isHeldByCurrentThread
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
// ✅ 业务异常时锁会自动释放(过期时间到)
// ✅ 不要在锁里做耗时操作八、本章小结
| 方案 | 优点 | 缺点 |
|---|---|---|
| SETNX | 简单 | 无续期、不公平 |
| Redisson | 功能全、续期 | 重量级 |
| RedLock | 高可用 | 性能差、复杂 |
| 锁类型 | 用途 |
|---|---|
| RLock | 默认可重入 |
| RFairLock | 公平 |
| RReadWriteLock | 读写分离 |
| RSemaphore | 限流 |
动手练习
- 用 SETNX + Lua 实现一个分布式锁
- 集成 Redisson,实现订单处理
- 用 RReadWriteLock 优化缓存读写
- 用 RSemaphore 实现接口限流