一:Redisson 分布式锁
1. 为什么需要分布式锁?
单体应用用 synchronized 或 ReentrantLock 就够了,但分布式环境下多个 JVM 实例之间互斥不了。分布式锁解决的是跨进程、跨节点的互斥访问。
2. 三种实现方式对比
| 维度 | Redis | ZooKeeper | MySQL |
|---|---|---|---|
| 实现原理 | SET NX EX + Lua 释放 |
临时顺序节点 + Watcher | 唯一索引 / 乐观锁 |
| 性能 | 极高(1-3ms) | 中等(3-10ms) | 低 |
| 一致性 | 最终一致(主从异步复制有锁丢失风险) | 强一致(ZAB 协议) | 强一致 |
| 可靠性 | 需 Redisson 看门狗续期 | 会话过期自动释放 | 需定时清理死锁 |
| 复杂度 | 低(用 Redisson 一行代码) | 中(需理解 ZNode) | 高 |
| 适用场景 | 高并发、性能敏感 | 强一致、高可靠 | 简单场景、已有 MySQL |
选型建议:
- 绝大多数场景用 Redis + Redisson,性能最好,生态最成熟
- 金融级强一致场景用 ZooKeeper
- 简单项目或已有 MySQL 不想引入新组件,用 MySQL 唯一索引
3. 手写 Redis 分布式锁
最基础的版本,用 SET key value NX EX 原子命令:
arduino
@Component
public class SimpleRedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_PREFIX = "lock:";
private static final long EXPIRE_SECONDS = 30;
/**
* 加锁
*/
public boolean tryLock(String lockName, String requestId) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent(LOCK_PREFIX + lockName, requestId, EXPIRE_SECONDS, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
/**
* 释放锁(用 Lua 保证原子性)
*/
public void unlock(String lockName, String requestId) {
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(LOCK_PREFIX + lockName),
requestId);
}
}
为什么释放锁要用 Lua? 因为 GET 和 DEL 是两个操作,中间可能被其他线程插入,导致误删别人的锁。Lua 脚本在 Redis 中是原子执行的。
存在的问题:
- 业务执行时间超过 30 秒,锁过期了,其他线程能加锁,导致并发问题
- 需要自己处理续期逻辑
这就是 Redisson 看门狗要解决的核心痛点。
二:Redisson 分布式锁与看门狗机制
1. Redisson 快速上手
xml
<<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.27.2</version>
</dependency>
yaml
spring:
redis:
redisson:
config: |
singleServerConfig:
address: "redis://127.0.0.1:6379"
password: null
database: 0
# 看门狗超时时间,默认 30000ms
lockWatchdogTimeout: 30000
2. 基础使用
csharp
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
public void deductStock(String productId) {
RLock lock = redissonClient.getLock("stock:" + productId);
try {
// 方式1:无参 lock(),启用看门狗自动续期
lock.lock();
// 方式2:带过期时间,禁用看门狗
// lock.lock(10, TimeUnit.SECONDS);
// 业务逻辑:扣减库存
int stock = getStock(productId);
if (stock > 0) {
updateStock(productId, stock - 1);
}
} finally {
// 只有当前线程持有锁才释放,避免误删
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
3. 看门狗机制



核心机制:
| 参数 | 默认值 | 说明 |
|---|---|---|
lockWatchdogTimeout |
30 秒 | 锁的过期时间 |
| 续期周期 | 10 秒(30/3) | 每隔 1/3 超时时间检查一次 |
| 触发条件 | lock() 无参调用 |
带 leaseTime 参数不启用看门狗 |
工作流程:
- 线程 A 调用
lock()加锁成功,Redisson 设置锁过期时间为 30 秒 - 启动看门狗后台任务(Netty
HashedWheelTimer),每隔 10 秒检查 - 如果线程 A 还持有锁,发送 Lua 脚本将 TTL 重置为 30 秒
- 线程 A 调用
unlock(),停止看门狗,释放锁 - 如果线程 A 宕机,看门狗停止,锁 30 秒后自动过期
为什么用 Netty Timer 不用 JDK Timer?
- 异步非阻塞,复用 EventLoop
- 异常隔离,单个任务失败不影响其他续期
- 哈希轮算法 O(1) 性能,适合高并发
4. 可重入锁
Redisson 锁是可重入的,同一个线程多次 lock() 不会死锁:
csharp
public void methodA() {
RLock lock = redissonClient.getLock("myLock");
lock.lock();
try {
methodB(); // 同一线程,可以再次获取锁
} finally {
lock.unlock();
}
}
public void methodB() {
RLock lock = redissonClient.getLock("myLock");
lock.lock(); // 重入,计数 +1
try {
// 业务
} finally {
lock.unlock(); // 计数 -1,不会真正释放
}
}
底层用 Redis Hash 结构存储:KEY 是锁名,FIELD 是线程标识,VALUE 是重入次数。
三:Redisson 进阶:红锁、读写锁、信号量
1. 红锁(RedLock)------多主节点部署
Redis 主从异步复制有锁丢失风险,Redisson 提供 RedLock 算法:在 N 个独立 Redis 主节点上加锁,过半成功才算获取锁。
ini
@Configuration
public class RedLockConfig {
@Bean
public RedissonRedLock redLock() {
Config config1 = new Config();
config1.useSingleServer().setAddress("redis://192.168.1.1:6379");
RedissonClient redisson1 = Redisson.create(config1);
Config config2 = new Config();
config2.useSingleServer().setAddress("redis://192.168.1.2:6379");
RedissonClient redisson2 = Redisson.create(config2);
Config config3 = new Config();
config3.useSingleServer().setAddress("redis://192.168.1.3:6379");
RedissonClient redisson3 = Redisson.create(config3);
RLock lock1 = redisson1.getLock("redLock");
RLock lock2 = redisson2.getLock("redLock");
RLock lock3 = redisson3.getLock("redLock");
return new RedissonRedLock(lock1, lock2, lock3);
}
}
typescript
@Service
public class RedLockService {
@Autowired
private RedissonRedLock redLock;
public void safeOperation() {
try {
// 尝试在大多数节点加锁
boolean locked = redLock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
// 执行业务
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
redLock.unlock();
}
}
}
实际上 RedLock 争议很大,Redis 作者和分布式系统专家 Martin Kleppmann 有过著名辩论。生产环境更推荐用 Redis Cluster + Redisson 单节点锁 + 看门狗,或者直接用 ZooKeeper。
2. 读写锁(ReadWriteLock)
读读共享,读写互斥,写写互斥:
typescript
@Service
public class CacheService {
@Autowired
private RedissonClient redissonClient;
public String readData(String key) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwLock:" + key);
RLock readLock = rwLock.readLock();
readLock.lock();
try {
return getFromCache(key);
} finally {
readLock.unlock();
}
}
public void writeData(String key, String value) {
RReadWriteLock rwLock = redissonClient.getReadWriteLock("rwLock:" + key);
RLock writeLock = rwLock.writeLock();
writeLock.lock();
try {
updateCache(key, value);
} finally {
writeLock.unlock();
}
}
}
3. 信号量(Semaphore)------限流
java
@Service
public class RateLimiterService {
@Autowired
private RedissonClient redissonClient;
public void limitedOperation() {
RSemaphore semaphore = redissonClient.getSemaphore("semaphore:api");
// 设置许可证数量(比如最多 10 个并发)
semaphore.trySetPermits(10);
boolean acquired = semaphore.tryAcquire(3, 5, TimeUnit.SECONDS); // 等5秒
if (acquired) {
try {
// 执行业务
} finally {
semaphore.release(); // 释放许可证
}
} else {
throw new RuntimeException("系统繁忙,请稍后重试");
}
}
}
四:ZooKeeper 分布式锁实现
1. 核心原理

利用 ZooKeeper 的临时顺序节点(EPHEMERAL_SEQUENTIAL) :
- 所有客户端在
/locks下创建临时顺序节点,如/locks/lock0000000001 - 判断自己创建的节点是不是序号最小的
- 如果是,获取锁成功;如果不是,监听前一个节点
- 前一个节点删除(释放锁),触发 Watcher 通知,客户端再次判断
- 客户端宕机会话过期,临时节点自动删除,避免死锁
2. Curator 实现(生产环境直接用)
Curator 是 Netflix 开源的 ZooKeeper 客户端,封装了分布式锁:
xml
<<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>5.6.0</version>
</dependency>
typescript
@Configuration
public class CuratorConfig {
@Bean
public CuratorFramework curatorFramework() {
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181")
.sessionTimeoutMs(5000)
.connectionTimeoutMs(3000)
.retryPolicy(new ExponentialBackoffRetry(1000, 3))
.build();
client.start();
return client;
}
}
@Service
public class ZkLockService {
@Autowired
private CuratorFramework client;
public void deductStock(String productId) {
InterProcessMutex lock = new InterProcessMutex(client, "/locks/stock/" + productId);
try {
// 获取锁,最多等待 10 秒
boolean acquired = lock.acquire(10, TimeUnit.SECONDS);
if (acquired) {
try {
// 扣减库存业务
System.out.println("获取锁成功,执行业务");
} finally {
lock.release();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3. 手写 ZooKeeper 锁
csharp
public class ZkDistributedLock implements Watcher {
private ZooKeeper zk;
private String lockPath;
private String currentNode;
private CountDownLatch connectedLatch = new CountDownLatch(1);
private CountDownLatch waitLatch;
public ZkDistributedLock(String connectString, String lockName) throws Exception {
zk = new ZooKeeper(connectString, 5000, this);
connectedLatch.await();
this.lockPath = "/locks/" + lockName;
// 确保父节点存在
if (zk.exists(lockPath, false) == null) {
zk.create(lockPath, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
}
public void lock() throws Exception {
// 创建临时顺序节点
currentNode = zk.create(lockPath + "/lock", new byte[0],
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 检查是否最小
List<String> children = zk.getChildren(lockPath, false);
Collections.sort(children);
String nodeName = currentNode.substring(lockPath.length() + 1);
if (nodeName.equals(children.get(0))) {
return; // 获取锁成功
}
// 不是最小,监听前一个节点
int index = Collections.binarySearch(children, nodeName);
String prevNode = children.get(index - 1);
waitLatch = new CountDownLatch(1);
zk.exists(lockPath + "/" + prevNode, event -> {
if (event.getType() == Event.EventType.NodeDeleted) {
waitLatch.countDown();
}
});
waitLatch.await();
}
public void unlock() throws Exception {
zk.delete(currentNode, -1);
zk.close();
}
@Override
public void process(WatchedEvent event) {
if (event.getState() == Event.KeeperState.SyncConnected) {
connectedLatch.countDown();
}
}
}
五:MySQL 唯一索引实现分布式锁
1. 核心原理
利用 MySQL 唯一索引的排他性:多个线程同时 INSERT 同一条记录,只有一个能成功,成功的那个获取锁;释放锁时 DELETE 这条记录。
sql
CREATE TABLE `distributed_lock` (
`lock_name` VARCHAR(64) NOT NULL COMMENT '锁名称,如 order:10086',
`lock_value` VARCHAR(128) NOT NULL COMMENT '锁持有者标识,如 UUID+线程ID',
`expire_time` DATETIME NOT NULL COMMENT '锁过期时间',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`lock_name`),
UNIQUE KEY `uk_lock_name` (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分布式锁表';
2. 手写 MySQL 分布式锁
typescript
@Component
public class MysqlDistributedLock {
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 获取锁
* @param lockName 锁名称
* @param requestId 请求标识(UUID + 线程ID)
* @param expireSeconds 过期时间(秒)
* @return true=获取成功
*/
public boolean tryLock(String lockName, String requestId, int expireSeconds) {
try {
jdbcTemplate.update(
"INSERT INTO distributed_lock(lock_name, lock_value, expire_time) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL ? SECOND))",
lockName, requestId, expireSeconds
);
return true;
} catch (DuplicateKeyException e) {
// 唯一索引冲突,获取锁失败
return false;
}
}
/**
* 释放锁(必须校验归属权,防止误删)
*/
public boolean unlock(String lockName, String requestId) {
int affected = jdbcTemplate.update(
"DELETE FROM distributed_lock WHERE lock_name = ? AND lock_value = ?",
lockName, requestId
);
return affected > 0;
}
/**
* 续期(看门狗思想)
*/
public boolean renew(String lockName, String requestId, int expireSeconds) {
int affected = jdbcTemplate.update(
"UPDATE distributed_lock SET expire_time = DATE_ADD(NOW(), INTERVAL ? SECOND) " +
"WHERE lock_name = ? AND lock_value = ? AND expire_time > NOW()",
expireSeconds, lockName, requestId
);
return affected > 0;
}
}
typescript
@Service
public class OrderService {
@Autowired
private MysqlDistributedLock lock;
public void deductStock(String productId) {
String lockName = "stock:" + productId;
String requestId = UUID.randomUUID() + ":" + Thread.currentThread().getId();
int expireSeconds = 30;
boolean acquired = lock.tryLock(lockName, requestId, expireSeconds);
if (!acquired) {
throw new RuntimeException("获取锁失败");
}
try {
// 启动看门狗续期(实际用 ScheduledExecutorService)
// ...
// 执行业务:扣减库存
} finally {
lock.unlock(lockName, requestId);
}
}
}
3. 存在的问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 死锁 | 客户端宕机,锁记录不删除 | 加 expire_time 字段,定时任务清理过期锁 |
| 锁过期业务没完 | 业务执行时间超过 expireSeconds | 看门狗续期,或设置足够长的过期时间 |
| 误删他人锁 | 释放时不校验归属 | DELETE 必须带 lock_value 条件 |
| 性能差 | 每次加锁都是一次磁盘 INSERT | 仅用于低并发场景,高并发用 Redis |
4. 基于 SELECT ... FOR UPDATE 的悲观锁方案
另一种 MySQL 锁实现,利用 InnoDB 行锁:
typescript
@Component
public class MysqlPessimisticLock {
@Autowired
private JdbcTemplate jdbcTemplate;
@Transactional // 必须开启事务
public boolean tryLock(String lockName, int waitSeconds) {
try {
// 先确保锁记录存在
jdbcTemplate.update(
"INSERT IGNORE INTO distributed_lock(lock_name, lock_value, expire_time) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 1 DAY))",
lockName, "placeholder"
);
// FOR UPDATE 加行锁,其他线程阻塞等待
jdbcTemplate.queryForObject(
"SELECT lock_name FROM distributed_lock WHERE lock_name = ? FOR UPDATE",
String.class,
lockName
);
return true;
} catch (CannotAcquireLockException e) {
return false;
}
}
}
缺点:
- 必须开启事务,事务持有锁期间不能干别的
- 高并发下大量线程阻塞,性能极差(实测 QPS 仅约 10)
- 容易死锁,需要设置
innodb_lock_wait_timeout