演进和最佳实践
好的,我将以资深程序设计专家的角度,为你系统地讲解分布式锁的演进过程与最佳实践,涵盖设计思想、常见技术方案、优劣对比与生产经验。
一、为什么需要分布式锁?
在分布式系统中,多个节点(如多个服务实例)可能会并发地访问共享资源(如数据库、缓存、文件、队列等),这就可能引发 数据不一致、重复执行、脏写等并发问题,需要一种机制来实现 互斥与同步控制 —— 这就是分布式锁的作用。
二、分布式锁的演进路线
我们按技术实现方式和复杂性来梳理一下主流分布式锁的演进过程:
1. 基于数据库的分布式锁(最早期)
实现方式:
- 使用数据库中的一行记录(如
LOCK_TABLE
)进行加锁。 - 利用数据库的唯一约束、事务与悲观/乐观锁控制。
示例 SQL:
INSERT INTO lock_table (lock_key, owner, expire_time) VALUES ('job_1', 'node_1', NOW() + INTERVAL 30 SECOND);
优点:
- 易于实现,不引入新组件;
- 利用数据库事务天然支持锁粒度控制。
缺点:
- 性能差、开销大;
- 无法很好处理死锁、异常宕机时锁释放问题;
- 不适合高并发、高性能场景。
2. 基于 Redis 的分布式锁(主流方案)
实现方式:
使用 Redis 的原子操作实现,如 SET key value NX PX 30000
。
RedLock(Redisson / 官方推荐方案):
由 Redis 作者提出,适用于多主架构,使用 多个 Redis 实例 进行加锁确认。
优点:
- 高性能、低延迟;
- 基于内存操作,吞吐大;
- 开源客户端多(Redisson、Lettuce、Jedis);
缺点:
- 单节点 Redis 容错性差;
- RedLock 多节点需要网络同步,复杂;
- 客户端必须定期续租,防止锁失效。
3. 基于 ZooKeeper 的分布式锁
实现方式:
利用 ZooKeeper 的临时顺序节点 + Watch 机制,谁节点最小谁获得锁。
优点:
- 强一致性;
- 自动过期释放(临时节点);
- 适用于需要顺序、公平锁的场景。
缺点:
- 性能一般(ZooKeeper 本质是 CP 系统);
- 实现复杂度高;
- 容易导致“羊群效应”(过多 Watch)。
4. 基于 etcd / Consul 的分布式锁(云原生)
实现方式:
使用 etcd 的 Compare-And-Swap(CAS)机制与租约机制。
优点:
- 强一致性,符合 Raft 协议;
- 与 K8s 等云原生系统天然集成。
缺点:
- 社区支持度一般;
- 上手曲线较高。
三、分布式锁的最佳实践总结
1. 明确需求,选择合适方案
- 如果高性能:优选 Redis + Redisson;
- 如果需要强一致性或顺序执行:考虑 ZooKeeper;
- 如果是K8s / 云原生环境:建议 etcd;
- 如果仅仅是小系统:可以用数据库实现。
2. 锁设计注意点
- 唯一标识(requestId / UUID):锁的拥有者标识,释放时需校验;
- 超时机制:防止死锁,锁应有过期时间;
- 续租机制:长任务需自动延长锁有效期;
- 锁粒度控制:避免全局大锁;
- 可重入性:根据业务选择是否支持;
- 异常释放机制:如服务崩溃后,自动释放。
四、生产中的经验建议
项目 | 建议做法 |
---|---|
Redis 锁 | 使用 SET NX PX ,同时定期续约(Redisson 自动化) |
RedLock | 仅适合极高一致性要求场景,否则单 Redis 足够 |
ZooKeeper 锁 | 控制节点数,避免“惊群” |
监控 | 锁失败次数、锁持有时长、锁争用次数建议监控 |
日志 | 锁获取/释放过程加详细日志,便于排查死锁问题 |
五、推荐组件与工具
工具 | 特点 |
---|---|
Redisson | Redis 客户端,支持锁、限流、延迟队列等,强烈推荐 |
Apache Curator | ZooKeeper 客户端封装,简化节点操作 |
Etcd Java Client | 云原生友好,适合 K8s/微服务架构 |
六、redisson最佳实践之代码实现
好的,我们以目前在生产中最常用的 Redis 分布式锁 + Redisson 为例,提供一个 完整可落地的 Java 示例。内容涵盖:
- Redisson 的引入和配置
- 获取和释放锁(可重入锁)
- 支持自动续约(watchdog)
- 注意事项与错误用法防范
🧱 一、Maven 引入 Redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.28.0</version> <!-- 请使用最新版 -->
</dependency>
⚙️ 二、Redisson 配置
# 单节点
redisson:
single-server:
address: redis://127.0.0.1:6379
password: your_password # 可选
connection-minimum-idle-size: 5
connection-pool-size: 10
# 集群模式(Redis Cluster)
redisson:
cluster-servers:
node-addresses:
- redis://127.0.0.1:7001
- redis://127.0.0.1:7002
- redis://127.0.0.1:7003
scan-interval: 2000
password: your_password
---
### 🔒 三、分布式锁使用示例(可重入 + 自动续约)
```java
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import java.util.concurrent.TimeUnit;
public class DistributedLockExample {
private static final RedissonClient redisson = RedissonManager.getClient();
public void doBusinessWithLock() {
String lockKey = "my-lock-key";
RLock lock = redisson.getLock(lockKey);
try {
// 尝试获取锁,最多等 5 秒,锁定后 10 秒自动释放(如果不使用 watchdog)
if (lock.tryLock(5, 10, TimeUnit.SECONDS)) {
try {
System.out.println("获取到锁,执行业务逻辑...");
Thread.sleep(8000); // 模拟业务处理
} finally {
lock.unlock();
System.out.println("释放锁");
}
} else {
System.out.println("未能获取到锁,稍后再试");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
⛑️ 四、Redisson Watchdog 自动续约机制(默认)
若使用
lock.lock()
而不传leaseTime
,Redisson 会启动看门狗机制自动续期,直到手动 unlock。
lock.lock(); // 会自动续约,避免执行太久锁过期
try {
// 执行业务
} finally {
lock.unlock();
}
Redisson 的 watchdog 默认续约时间是 30 秒,间隔 10 秒续一次,可通过配置修改:
config.setLockWatchdogTimeout(30000); // 30秒
🧷 五、常见错误示范(避免踩坑)
错误 | 原因 |
---|---|
多线程共享 RLock 实例 | RLock 是非线程安全的,必须每次通过 redisson.getLock() 获取 |
获取锁但未释放 | 可能引发死锁,应使用 try-finally |
自定义过短 leaseTime 未自动续期 | 使用 tryLock(timeout, leaseTime) 时 Watchdog 不生效 |
Redis 崩溃后锁失效 | 应配置 Redis 高可用(主从或哨兵)或 RedLock |
✅ 六、可扩展建议
- 封装通用的锁模板方法(AOP 或注解方式);
- 锁状态落地持久化,用于监控或审计;
- 设置 Prometheus 监控锁争用与异常;
📌 总结
场景 | 推荐做法 |
---|---|
普通业务互斥控制 | tryLock(timeout, lease) |
长时间任务执行 | lock() + 自动续约(watchdog) |
强一致场景 | 可扩展至 RedLock 多节点锁 |
云原生环境 | 可切换为 etcd / Consul 锁 |
七、redisson最佳实践之注解式的分布式锁封装,结合 Spring AOP + Redisson
好的,我们接下来实现一个注解式的分布式锁封装,结合 Spring AOP + Redisson,达到:
✅ 用注解就能加分布式锁
✅ 自动处理锁获取、释放、异常
✅ 支持锁的 key 表达式、重试机制、自动续约等
🔧 结构概览
com.example.lock
├── annotation
│ └── DistributedLock.java <-- 自定义注解
├── aspect
│ └── DistributedLockAspect.java <-- AOP切面处理锁逻辑
├── service
│ └── SomeBusinessService.java <-- 使用示例
└── RedissonConfig.java <-- Redisson配置
🏷️ 一、自定义注解 @DistributedLock
package com.example.lock.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedLock {
/**
* 锁的key支持Spring EL表达式,例如 "#userId"
*/
String key();
/**
* 最大等待时间(秒)默认5秒
*/
long waitTime() default 5;
/**
* 锁租期(秒)默认10秒
*/
long leaseTime() default 10;
/**
* 获取锁失败是否抛异常
*/
boolean throwOnFail() default false;
}
🧠 二、AOP 切面实现 DistributedLockAspect
package com.example.lock.aspect;
import com.example.lock.annotation.DistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.expression.*;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
@Slf4j
@Aspect
@Component
public class DistributedLockAspect {
@Autowired
private RedissonClient redisson;
private final ExpressionParser parser = new SpelExpressionParser();
private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(com.example.lock.annotation.DistributedLock)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Method method = getMethod(joinPoint);
DistributedLock lockAnnotation = method.getAnnotation(DistributedLock.class);
String lockKey = parseKey(lockAnnotation.key(), method, joinPoint.getArgs());
RLock lock = redisson.getLock(lockKey);
boolean locked = false;
try {
locked = lock.tryLock(lockAnnotation.waitTime(), lockAnnotation.leaseTime(), TimeUnit.SECONDS);
if (!locked) {
log.warn("获取分布式锁失败: {}", lockKey);
if (lockAnnotation.throwOnFail()) {
throw new RuntimeException("无法获取分布式锁");
}
return null;
}
log.info("获取锁成功: {}", lockKey);
return joinPoint.proceed();
} finally {
if (locked && lock.isHeldByCurrentThread()) {
lock.unlock();
log.info("释放锁: {}", lockKey);
}
}
}
private Method getMethod(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Class<?> targetClass = joinPoint.getTarget().getClass();
try {
return AopUtils.getMostSpecificMethod(signature.getMethod(), targetClass);
} catch (Exception e) {
return signature.getMethod();
}
}
private String parseKey(String keySpEL, Method method, Object[] args) {
EvaluationContext context = new StandardEvaluationContext();
String[] paramNames = nameDiscoverer.getParameterNames(method);
if (paramNames != null) {
for (int i = 0; i < paramNames.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
}
Expression expression = parser.parseExpression(keySpEL);
return "lock:" + expression.getValue(context, String.class); // 加前缀避免冲突
}
}
🔧 三、Redisson 配置类
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setPassword(null)
.setConnectionMinimumIdleSize(5);
return Redisson.create(config);
}
}
🚀 四、使用示例
import com.example.lock.annotation.DistributedLock;
import org.springframework.stereotype.Service;
@Service
public class SomeBusinessService {
@DistributedLock(key = "#userId", waitTime = 3, leaseTime = 8, throwOnFail = true)
public void doSomethingCritical(String userId) {
System.out.println("正在执行关键业务逻辑,用户ID:" + userId);
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {}
}
}
✅ 五、优点
- 业务代码无侵入,只用注解即可锁定方法
- 支持 SpEL 表达式,可按业务动态生成 key
- 防止锁不释放、异常抛出、并发冲突
📌 总结
特性 | 说明 |
---|---|
自动锁处理 | 通过 AOP 自动加锁/释放 |
锁 key 支持 SpEL | 业务灵活控制锁粒度 |
可重入与续期 | Redisson 原生支持,无需手动处理 |
可统一监控日志 | 支持打点锁日志,接入监控系统方便 |