0
0
0

Redis 分布式锁学习笔记

2026-10-26
文章摘要
|

一、为什么需要分布式锁?

Java 的 synchronized 和 ReentrantLock 属于 JVM 本地锁,只能保证单个 JVM 内的线程安全。

在 Spring Boot 集群部署时,每个 JVM 都有独立的锁对象,无法协调不同服务实例的并发操作。

因此,需要使用分布式锁实现跨 JVM 的互斥。

常见实现方式:MySQL、Redis、ZooKeeper。

二、分布式锁的核心特性

特性

说明

多进程可见

所有 JVM 都能访问同一套锁状态

互斥性

同一时刻,同一把锁只能被一个客户端持有

安全性

避免永久死锁、误删其他线程的锁

高可用

部分节点故障时,仍能继续提供服务

高性能

支持高并发,获取和释放锁的开销较低

多进程可见和互斥性是基础,其次考虑安全性、高可用和高性能。

另外,可重入性表示同一线程能够重复获取自己持有的锁,属于常见特性,但并非所有分布式锁都必须支持。

三、分布式锁的三种实现方式

对比

MySQL

Redis

ZooKeeper

实现原理

事务行锁等机制

SET NX PX

临时顺序节点

性能

一般

较高

相对较低

锁释放

事务结束释放

主动释放或 TTL 过期

删除节点或会话过期

适用场景

数据库事务相关业务

高并发业务

分布式协调

1. MySQL

利用数据库事务和行锁实现互斥。

SELECT * FROM goods WHERE id = 1 FOR UPDATE;

其他事务尝试获取同一条记录的排他锁时,需要等待。

事务提交、回滚或连接异常终止并完成回滚后,相关锁会释放。

注意:普通 UPDATE 虽然会进行加锁,但不等于自动实现了完整的业务分布式锁。

2. Redis

通过 SET NX PX 实现互斥锁。

SET lock:order:1001 uuid123 NX PX 30000
  • NX:锁不存在时才能创建。

  • PX:设置过期时间,避免永久占用。

  • uuid123:锁的唯一标识,用于判断锁的归属。

Redis 分布式锁需要解决两个问题:

① 锁提前过期

业务尚未执行完毕,锁已经过期,其他线程就可能获得同一把锁。

解决方案:使用 Redisson 的 Watchdog(看门狗)机制自动续期。

注意:Redisson 只有在不指定固定 leaseTime 的相应加锁方式下,才使用看门狗自动续期。

② 误删其他线程的锁

线程 A 的锁过期后,线程 B 获取了同一把锁。如果线程 A 直接执行 DEL,就可能误删线程 B 的锁。

解决方案:使用唯一标识配合 Lua 脚本,原子地判断锁归属并释放。

if redis.call('GET', KEYS[1]) == ARGV[1] then
    return redis.call('DEL', KEYS[1])
end
return 0

此外,Redis 主从异步复制与故障切换也可能导致锁状态丢失,不能认为设置 TTL 就能保证所有情况下的互斥安全。

3. ZooKeeper

通过临时顺序节点实现分布式锁。

例如:

线程 A → lock-0001
线程 B → lock-0002
线程 C → lock-0003
  1. 序号最小的节点获得锁。

  2. 其他线程监听自己的前一个节点。

  3. 前一个节点删除后,后继线程继续竞争锁。

  4. 客户端会话过期时,对应临时节点自动删除。

ZooKeeper 适合分布式协调场景,但节点操作和协调成本通常高于 Redis 的简单锁操作。

四、一人一单如何实现?

秒杀场景下,同一用户可能同时发送多个请求。

如果多个线程都查询到用户尚未下单,就可能重复创建订单。

解决方案:按照用户 ID + 商品 ID 加锁,保证同一用户购买同一商品时的操作互斥。

1. 锁必须覆盖事务提交

错误做法:在事务方法内部获取和释放锁。

因为方法内部释放锁后,Spring 事务代理可能还没有提交事务,其他线程就可能查询不到刚创建的订单。

推荐流程:

获取用户分布式锁
       ↓
调用 Spring 事务方法
       ↓
检查是否重复下单
       ↓
扣减库存、创建订单
       ↓
数据库事务提交
       ↓
释放分布式锁

2. 事务方法必须通过 Spring 代理调用

Spring 的 @Transactional 通常通过 AOP 代理实现。

// 不推荐:同类内部调用可能绕过事务代理
this.createOrder();

推荐将锁逻辑和事务逻辑分别放在两个 Spring Service 中,由加锁服务调用注入的事务服务。

这样既能使 @Transactional 生效,也更容易保证锁覆盖整个事务提交过程。

3. 数据库唯一索引兜底

即使使用分布式锁,也建议建立联合唯一索引:

ALTER TABLE voucher_order
ADD UNIQUE KEY uk_user_voucher (user_id, voucher_id);

防止同一个用户对同一商品重复下单。

同时,库存扣减应使用原子条件更新,并和订单插入放在同一数据库事务中。

五、实际开发建议

Java 8 + Spring Boot 项目中,推荐直接使用 Redisson,而不是自行实现分布式锁。

主要原因:

  • 支持可重入锁。

  • 支持看门狗自动续期。

  • 提供锁超时与等待机制。

  • 简化加锁、解锁及并发控制代码。

但 Redisson 也不能保证所有故障情况下都不会发生锁失效,关键业务仍需要数据库约束、事务和幂等机制兜底。

六、总结

  1. 本地锁:只能保护单个 JVM,多实例部署无法直接共享。

  2. MySQL 分布式锁:利用事务与行锁实现互斥。

  3. Redis 分布式锁:利用 SET NX PX 实现互斥,通过唯一标识避免误删,Redisson 可自动续期。

  4. ZooKeeper 分布式锁:利用临时顺序节点和监听机制。

  5. 一人一单:用户维度加锁,锁覆盖事务提交,通过数据库唯一索引兜底。

核心原则:分布式锁负责跨实例互斥,数据库事务负责数据一致性,唯一约束负责防止重复数据。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论