为什么需要分布式锁?
单机应用:synchronized / ReentrantLock ← JVM 内锁 分布式应用:多 JVM 实例,synchronized 不够用!← 需要分布式锁MySQL 分布式锁(最朴素)
-- 用唯一索引实现分布式锁 CREATE TABLE distributed_lock ( lock_key VARCHAR(64) PRIMARY KEY, lock_value VARCHAR(64) NOT NULL, expire_time BIGINT NOT NULL ); -- 加锁:插入 INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES ('order_lock', 'uuid-123', UNIX_TIMESTAMP() + 30); -- 释放锁:删除 DELETE FROM distributed_lock WHERE lock_key = 'order_lock' AND lock_value = 'uuid-123';Java 实现:
public boolean tryLock(String lockKey, long expireSeconds) { String value = UUID.randomUUID().toString(); long expireTime = Instant.now().getEpochSecond() + expireSeconds; try { int rows = jdbcTemplate.update( "INSERT INTO distributed_lock (lock_key, lock_value, expire_time) VALUES (?, ?, ?)", lockKey, value, expireTime); return rows == 1; } catch (DuplicateKeyException e) { return false; // 锁已被其他线程持有 } } public boolean unlock(String lockKey, String value) { // ⚠️ 关键:必须验证 value(防止误删别人的锁) int rows = jdbcTemplate.update( "DELETE FROM distributed_lock WHERE lock_key = ? AND lock_value = ?", lockKey, value); return rows == 1; }优缺点:
- ✅ 简单易用,有数据库就能用
- ❌性能差(MySQL 写并发 1k+ TPS)
- ❌ 锁等待靠轮询
- ⚠️小流量场景可以用
Redis 分布式锁
1 SETNX + EX(最基础)
// 加锁 public boolean tryLock(String lockKey, long expireSeconds) { String result = redisTemplate.opsForValue().set(lockKey, "locked", expireSeconds, TimeUnit.SECONDS); return "OK".equals(result); } // 释放锁 public void unlock(String lockKey) { redisTemplate.delete(lockKey); }⚠️ 3 大问题:
1.死锁:设置锁后宕机,锁永远不释放→ 用 EX 过期时间解决
2.误删锁:A 加锁 → A 业务慢 → 锁过期 → B 加锁 → A 释放锁(删了 B 的锁)→用 UUID 解决
3.不可重入:同一个线程不能再次获得锁 →用 ThreadLocal + 计数器解决
2 SETNX + UUID + Lua
public class RedisDistributedLock { // 加锁 Lua 脚本 private static final String LOCK_SCRIPT = "if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then " + " redis.call('expire', KEYS[1], ARGV[2]) " + " return 1 " + "else " + " return 0 " + "end"; // 释放锁 Lua 脚本(⚠️ 必须验证 UUID,防止误删) 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"; public boolean tryLock(String lockKey, long expireSeconds) { String uuid = UUID.randomUUID().toString(); Long result = redisTemplate.execute( new DefaultRedisScript<>(LOCK_SCRIPT, Long.class), Arrays.asList(lockKey), uuid, String.valueOf(expireSeconds)); return result != null && result == 1; } public boolean unlock(String lockKey, String uuid) { Long result = redisTemplate.execute( new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class), Arrays.asList(lockKey), uuid); return result != null && result == 1; } }实战代码:
// DLQConsumer.java @RabbitListener(queues = "dlq.queue") public void processDLQ(ReportMessage message, Channel channel) throws IOException { String lockKey = "dlq:lock:" + message.getMessageId(); String uuid = UUID.randomUUID().toString(); try { // 1. Redis 分布式锁(30 秒过期) if (!redisLock.tryLock(lockKey, uuid, 30)) { log.info("任务已被其他 Consumer 锁定,跳过: {}", message.getMessageId()); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); return; } // 2. 业务处理 reportService.process(message); // 3. ACK channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { log.error("处理失败", e); channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false); } finally { // 4. 释放锁(验证 UUID) redisLock.unlock(lockKey, uuid); } }优缺点:
- ✅性能高(Redis 写 10w+ TPS)
- ✅ 实现简单
- ❌不保证强一致(Redis 主从切换可能丢锁)
- ❌ 需要自己实现可重入
Redisson 分布式锁
// 1. 引入 Redisson <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.23.4</version> </dependency> // 2. 配置 Redisson @Configuration public class RedissonConfig { @Bean public RedissonClient redissonClient() { Config config = new Config(); config.useSingleServer() .setAddress("redis://127.0.0.1:6379") .setPassword("mova_redis_pwd"); return Redisson.create(config); } } // 3. 使用分布式锁 @Service public class ReportTaskService { @Autowired private RedissonClient redissonClient; public void processReport(ReportTask task) { // ⚠️ 关键:可重入锁(基于 Redis Hash 实现) RLock lock = redissonClient.getLock("report:lock:" + task.getId()); try { // 1. 尝试加锁(10 秒过期,watchdog 自动续期) if (lock.tryLock(10, 30, TimeUnit.SECONDS)) { try { // 业务处理 reportService.process(task); } finally { // 2. 释放锁 lock.unlock(); } } else { log.info("获取锁失败,跳过任务: {}", task.getId()); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }Redisson 7 大优势:
1.可重入:基于 Redis Hash,同一线程可多次加锁
2.看门狗自动续期:默认 30 秒续期一次,业务卡住锁不会过期
3.公平锁:按请求顺序排队
4.联锁(MultiLock):同时加多个锁(转账场景 A 减 + B 加)
5.红锁(RedLock):5 个独立 Redis 节点(强一致)
6.读写锁:RReadWriteLock
7.信号量:RSemaphore(限流场景)
"项目用Redisson 分布式锁 + 看门狗自动续期机制,30 秒自动续期,业务卡住锁不会过期。4 个 JVM 实例同时抢锁只让 1 个实例处理任务,任务零重复。"
Redisson 看门狗机制
┌────────────────────────────────────────────┐ │ Redisson 看门狗机制 │ ├────────────────────────────────────────────┤ │ 1. 加锁时设置 lockTime = 30 秒 │ │ ↓ │ │ 2. 后台线程每 10 秒检查锁是否还在 │ │ ↓ │ │ 3. 如果还在,自动续期 30 秒 │ │ ↓ │ │ 4. 业务执行完主动 unlock │ │ ↓ │ │ 5. 如果宕机,30 秒后锁自动过期 │ └────────────────────────────────────────────┘"Redisson 看门狗机制:业务执行时锁自动续期,30 秒一次,避免业务执行时间超过锁过期时间导致锁失效。"
Redisson 联锁(转账场景)
// 转账场景:A 账户 -100,B 账户 +100,必须同时成功 public void transfer(String fromAccount, String toAccount, BigDecimal amount) { RLock lockA = redissonClient.getLock("lock:account:" + fromAccount); RLock lockB = redissonClient.getLock("lock:account:" + toAccount); // ⚠️ 联锁:要么都加成功,要么都失败 RedissonMultiLock multiLock = new RedissonMultiLock(lockA, lockB); try { if (multiLock.tryLock(10, 30, TimeUnit.SECONDS)) { try { // A 减 100 accountMapper.debit(fromAccount, amount); // B 加 100 accountMapper.credit(toAccount, amount); } finally { multiLock.unlock(); } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }Redisson 读写锁
// mpvs 客户数据:1 个写,多个读 public Order queryOrder(Long orderId) { RReadWriteLock rwLock = redissonClient.getReadWriteLock("order:lock:" + orderId); // 读锁(多个线程可同时读) RLock readLock = rwLock.readLock(); readLock.lock(); try { return orderMapper.selectById(orderId); } finally { readLock.unlock(); } } public void updateOrder(Order order) { RReadWriteLock rwLock = redissonClient.getReadWriteLock("order:lock:" + orderId); // 写锁(独占) RLock writeLock = rwLock.writeLock(); writeLock.lock(); try { orderMapper.updateById(order); } finally { writeLock.unlock(); } }性能提升:读多写少场景用读写锁,性能提升 5-10 倍。
ZooKeeper 分布式锁
// ZK 分布式锁基于临时顺序节点 public class ZKDistributedLock { private ZooKeeper zk; private String lockPath = "/locks"; public void acquire(String lockKey) throws Exception { // 1. 创建临时顺序节点 String nodePath = zk.create(lockPath + "/" + lockKey + "_", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); // 2. 获取所有子节点 List<String> children = zk.getChildren(lockPath, false); Collections.sort(children); // 排序 // 3. 判断自己是否最小节点 if (nodePath.equals(lockPath + "/" + children.get(0))) { // 获取锁 } else { // 监听前一个节点 String prevNode = children.get(Collections.binarySearch(children, nodePath.substring(lockPath.length() + 1)) - 1); zk.exists(lockPath + "/" + prevNode, new Watcher() { @Override public void process(WatchedEvent event) { synchronized (this) { notifyAll(); // 唤醒等待 } } }); wait(); // 等待前一个节点释放 } } }优缺点:
- ✅强一致(ZK 集群半数以上节点写成功才返回)
- ✅ 公平锁(按请求顺序)
- ❌性能差(ZK 集群写性能 1k+ TPS)
- ❌ 部署复杂(需要 ZK 集群)
- ⚠️老项目用过
- ❌新项目不推荐(用 Redisson 更简单)
RedLock 红锁(强一致场景)
// RedLock:5 个独立的 Redis 节点(部署在不同机器) public class RedLock { private List<RedissonClient> redissonClients; // 5 个独立的 Redis 节点 public boolean tryLock(String lockKey, long expireSeconds) { // 同时向 5 个节点请求加锁 List<RLock> locks = redissonClients.stream() .map(client -> client.getLock(lockKey)) .collect(Collectors.toList()); RedissonRedLock redLock = new RedissonRedLock(locks.toArray(new RLock[0])); try { // ⚠️ 关键:5 个节点中 > N/2 + 1 个加锁成功才算成功 return redLock.tryLock(100, expireSeconds * 1000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } } }5 节点架构:
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ Redis1 │ │ Redis2 │ │ Redis3 │ │ Redis4 │ │ Redis5 │ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘ ↑ ↑ ↑ ↑ ↑ └─────────┴─────────┴─────────┴─────────┘ RedissonRedLock (5 个节点中 >3 个加锁成功才算成功)优缺点:
- ✅强一致(5 节点多数派,2 个节点同时挂掉也能用)
- ❌成本高(要部署 5 个独立 Redis)
- ❌性能差(要等 5 个节点响应)
- ⚠️金融项目强一致场景用(央行清算、跨行转账)
- ❌一般业务用不上(Redisson 单 Redis 够用)
三、5 种分布式锁对比
| 维度 | MySQL | Redis SETNX | Redisson | ZooKeeper | RedLock |
|---|---|---|---|---|---|
| 性能 | 1k TPS | 10w TPS | 10w+ TPS | 1k TPS | 5w TPS |
| 可靠性 | 中 | 中(主从切换可能丢锁) | 高(看门狗) | 高 | 最高 |
| 强一致 | ✅ | ❌ | ❌ | ✅ | ✅ |
| 可重入 | 需自己实现 | ❌ | ✅ | ❌ | ✅ |
| 公平锁 | ❌ | ❌ | ✅ | ✅ | ✅ |
| 复杂度 | 低 | 低 | 中 | 高 | 极高 |
| 部署成本 | 0(用现有 DB) | 0(用现有 Redis) | 低 | 高(要 ZK 集群) | 极高(5 Redis) |
| 推荐度 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ |