[036][缓存模块]基于 Redis 自定义缓存锁的设计与实现
本文章代码: gitee , gitcode , github
![[036][缓存模块]基于 Redis 自定义缓存锁的设计与实现](/uploadfile/2026/0815/83977b9df9d6ec58c7cb6f54f0e2e85d.webp)
在高并发场景里,缓存(比如 Redis)早已不只是个数据翻跟斗,它更是实现分布式协调原语的关键角色,其中分布式锁便是最典型的应用。这里要剖析的,是一套轻量且可扩展的 Redis 自定义锁组件——它巧妙融合了注解、AOP 与 Lua 脚本,提供固定租期和自动续期两种模式。这种设计能干净地嵌入业务代码,有效化解缓存击穿、重复计算以及资源竞争等棘手问题。
一、为什么需要“缓存锁”
当我们使用 Redis 作为缓存时,常会遇到以下痛点:
缓存击穿:热点数据过期瞬间,大量请求同时回源 DB,造成数据库压力骤增。 重复计算:多个节点同时执行同一耗时任务(如生成报表、刷新缓存),导致资源浪费。 资源互斥:对共享资源(库存、唯一流水号)需要串行化操作。传统做法通常是在业务代码里手动去写 setnx、expire、del 这类 Redis 命令。这样做的问题很直接:代码重复不说,还特别容易在边界条件上踩坑,比如忘了释放锁,或者压根没处理锁超时。更稳妥也更清爽的方式,是借助自定义注解和 AOP,把加锁这套逻辑用声明式的方式织入目标方法里;用起来和 @Cacheable 一样省心、自然。
下文将完整展示一个生产就绪的 Redis 分布式锁组件的设计思路和核心实现。
二、整体设计
2.1 设计目标
声明式锁:通过@RedisLockable 注解标注需要加锁的方法。 SpEL 动态 Key:锁的 Key 支持方法参数表达式,灵活区分不同资源。 双模式支持: 固定租期:锁持有超时自动释放,适合短时间任务。 自动续期:后台定时续期,避免长任务锁过期,类似 Redisson 的看门狗。 原子性保证:加锁、解锁、续期均使用 Lua 脚本,避免竞态条件。 低侵入:业务代码只需关注核心逻辑,锁的获取与释放完全由 AOP 管理。 2.2 架构组件
| 组件 | 职责 |
|---|---|
@RedisLockable | 方法级注解,配置前缀、SpEL key、过期时间、时间单位。 |
RedisLockableAspect | AOP 切面,解析注解,调用 RedisLockService 执行锁逻辑。 |
RedisLockService | 核心锁服务,封装 Lua 脚本,提供 FixedLease 和 AutoRenewal 两种内部类。 |
SpelMethodBasedExpressionEvaluator | 解析 SpEL 表达式,将方法参数转换为动态 Key。 |
2.3 工作流程
方法执行前,切面拦截到@RedisLockable 注解。 通过 SpEL 计算出完整的锁 Key(prefix 动态部分)。 根据 expireTime 判断模式: expireTime > 0 → 调用 FixedLease,指定租期执行任务。 expireTime <= 0 → 调用 AutoRenewal,启用看门狗。 锁获取成功则执行目标方法,最终自动释放锁(或续期直到完成)。 三、核心实现深度解析
3.1 自定义注解 @RedisLockable
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface RedisLockable { String prefix() default "";String key(); // SpEL 表达式,如 "#userId"long expireTime() default -1;TimeUnit timeUnit() default TimeUnit.MILLISECONDS;} 设计要点:
prefix 用于区分业务模块,例如 "order:"、"stock:"。 key 支持 SpEL,可访问方法参数、属性等,例如 #dto.id。 expireTime <= 0 作为自动续期的约定开关,自然语义清晰。 3.2 AOP 切面:非侵入锁控制
@Around("@annotation(redisLockable)")public Object around(ProceedingJoinPoint joinPoint, RedisLockable redisLockable) { String dynamicKey = spelEvaluator.getValue(method, args, redisLockable.key(), String.class);String fullKey = redisLockable.prefix() dynamicKey;if (redisLockable.expireTime() > 0) { return redisLockService.fixedLease().doInLock(fullKey, Duration.of(expireTime, unit), () -> joinPoint.proceed());} else { return redisLockService.autoRenewal().doInLock(fullKey, () -> joinPoint.proceed());}} 亮点:切面与锁服务完全解耦,新增模式只需扩展 RedisLockService,无需修改切面。
3.3 加锁的 Lua 脚本
return redis.call('set', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2]) NX:仅当 Key 不存在时设置,实现互斥。 PX:毫秒级过期时间,避免死锁。 返回值 OK 表示加锁成功,否则表示锁已被占用。 3.4 自动续期(看门狗)
自动续期的核心是一个递归的 TimerTask:
private void renewalLock(String lockKey, String lockId) { new Timer().schedule(new TimerTask() { @Overridepublic void run() { if (续期成功) { renewalLock(lockKey, lockId);// 递归,形成持续续期链}}}, DEFAULT_EXPIRE_TIME.toMillis() / 3); // 每 1/3 租期续期一次} 注意:生产环境中建议使用 ScheduledExecutorService 替代 Timer,并考虑取消任务的机制。当前代码在锁释放后递归链条会自然终止(因为续期脚本返回 false),但 Timer 线程可能仍存活一段时间,这是一个可优化点。
3.5 解锁的 Lua 脚本
if redis.call('get', KEYS[1]) == ARGV[1] thenreturn tostring(redis.call('del', KEYS[1]) == 1)elsereturn 'false'end 为什么要比对 value?
防止误删其他线程的锁。每个锁的 value 是一个 UUID(IdUtil.fastSimpleUUID()),只有持有者才能释放,这是分布式锁的安全红线。
四、使用示例
4.1 固定租期模式(防缓存击穿)
@RedisLockable(prefix = "cache:refresh:", key = "#cacheName", expireTime = 5, timeUnit = TimeUnit.SECONDS)public void refreshCache(String cacheName) { // 从 DB 加载最新数据并写入 Redis} 保证同一时刻只有一个节点执行刷新任务,其他节点等待锁释放后直接从缓存读取。
4.2 自动续期模式(长任务保护)
@RedisLockable(prefix = "report:gen:", key = "#reportId")// expireTime = -1public String generateLargeReport(String reportId) throws InterruptedException { // 可能耗时几分钟的任务,锁会自动续期return reportService.build(reportId);} 无需担心任务未完成锁就过期,看门狗会在后台持续续期,任务结束后自动释放。
五、优缺点及生产环境改进建议
✅ 优点
声明式:业务代码零污染,锁逻辑与业务逻辑分离。 灵活 Key:SpEL 表达式覆盖绝大多数动态资源标识场景。 双模式:固定租期适合短任务,自动续期适合长任务,按需选择。 原子操作:Lua 脚本保证加锁/解锁的原子性,避免 race condition。 安全释放:基于 value 匹配的释放机制,杜绝锁误删。⚠️ 缺点与改进方向
| 问题点 | 改进建议 |
|---|---|
Timer 续期导致线程泄露风险 | 改用 ScheduledExecutorService,并返回 ScheduledFuture 以便取消。 |
| 续期任务无法被显式停止 | 在 unlock 时增加一个取消续期的回调机制。 |
| 没有重试机制 | 增加重试策略(如重试间隔、最大次数),或提供阻塞等待的 API。 |
| 锁的可重入性缺失 | 扩展脚本支持重入计数,或结合 ThreadLocal 实现。 |
| 监控与告警缺失 | 添加 Micrometer 指标(加锁耗时、失败次数、续期次数)。 |
六、总结
本套基于 Redis 的自定义锁组件,通过注解 AOP Lua 脚本实现了生产可用的分布式锁,既解决了缓存场景下的资源互斥问题,也为更广义的并发控制提供了优雅的声明式方案。其设计思想——将基础设施能力以声明式 API 暴露给上层业务——值得在缓存、限流、幂等等场景中反复借鉴。
在实际使用时,建议根据业务压测调整租期时长,并配合 Redis 集群模式保证锁服务本身的高可用。锁不是银弹,但对于缓存击穿、重复计算等典型问题,这套组件能显著提升代码的可维护性和系统的稳定性。
