[037][缓存模块]基于 Guava Striped 的声明式本地锁设计与实现

简介: 本文介绍基于Guava Striped与Spring AOP实现的声明式本地锁:通过`@LocalLockable`注解+SpEL动态生成细粒度锁key,支持超时控制与中断处理,内存可控、低延迟,适用于单机高并发场景。

[037][缓存模块]基于 Guava Striped 的声明式本地锁设计与实现

本文章代码: gitee , gitcode , github

1. 概述

在单机应用开发中,为了避免并发操作导致的数据不一致或资源竞争,本地锁是最常用的同步手段之一。Java 提供了 synchronizedReentrantLock 等基础锁机制,但它们缺乏业务语义,锁粒度往往过粗(如整个方法或对象),且锁 key 无法灵活地与业务参数关联。

本文介绍一套轻量级的声明式本地锁解决方案,基于 Guava Striped 实现细粒度锁池,结合 Spring AOPSpEL 表达式,允许开发者通过注解优雅地完成本地锁控制。该方案已在生产环境中稳定运行,适用于对低延迟、无外部依赖(如 Redis)有要求的单机并发控制场景。

2. 设计思想

该方案的核心目标:

  • 声明式:通过注解 @LocalLockable 标记需要同步的方法,降低锁代码的侵入性。
  • 细粒度:锁的 key 由业务参数(SpEL 表达式)动态生成,不同 key 之间互不影响。
  • 可控超时:支持锁等待时间配置,避免死锁或长时间阻塞。
  • 低内存:使用 Striped.lazyWeakLock(1024) 创建固定数量的锁实例,通过哈希映射到不同 key,兼顾细粒度与内存效率。

整体架构由三个组件协作完成:

组件 职责
@LocalLockable 注解,定义锁的前缀、key 表达式、等待时间及单位
LocalLockableAspect AOP 切面,解析 SpEL,调用锁服务
LocalLockService 底层锁服务,封装 Guava Striped 的获取与释放逻辑

3. 核心实现解析

3.1 动态锁 key:SpEL 表达式支持

锁的粒度取决于 key 属性的 SpEL 表达式。例如 #userId 表示使用方法参数 userId 的值作为锁标识。切面通过 SpelMethodBasedExpressionEvaluator(一个自定义的 SpEL 工具类)解析表达式:

String value = spelMethodBasedExpressionEvaluator.getValue(method, args, localLockable.key(), String.class);
String key = localLockable.prefix() + value;

这种方式使得锁 key 可以依赖任意方法参数、嵌套属性甚至调用 Bean 方法,灵活性极高。prefix 属性可用于添加业务前缀(如 "user:"),方便区分不同模块的锁。

3.2 锁服务实现:Guava Striped 的精巧应用

LocalLockService 内部维护了一个 Striped<Lock>

private final Striped<Lock> stripedLock = Striped.lazyWeakLock(1024);

Striped 是 Guava 提供的锁池工具,它根据传入 key 的哈希值,从固定数量的锁中选取一个返回。lazyWeakLock(1024) 会创建 1024 个 ReentrantLock(弱引用缓存),当锁不再被引用时可被 GC 回收。相比 ConcurrentHashMap + 动态创建锁的方式,Striped 在并发量高时内存占用更稳定。

lock(String lockKey) 方法直接调用 stripedLock.get(lockKey) 获得对应的锁。由于底层锁数量有限(1024),理论上不同 key 可能共享同一个物理锁实例(哈希碰撞),但概率较低且对业务影响很小。如果应用要求绝对隔离,可增大 stripes 数量(如 4096)。

3.3 两种加锁语义:超时与非超时

LocalLockService 提供了四组重载方法,分别支持有/无返回值、有/无超时场景:

  • 无超时:直接调用 lock.lock(),线程会阻塞直到获取锁。
  • 有超时:调用 lock.tryLock(timeout, unit),若超时未获取则抛出 LockCreateException

这种设计让调用方可以根据业务容忍度选择合适的行为。例如,用户注册接口应快速失败,而非长时间等待。

切面根据 waitTime > 0 自动选择相应的重载。特别注意的是,对于带超时的 doInLock,切面传递的是 ThrowingCallable<T>,它可以抛出受检异常(如 joinPoint.proceed() 抛出的 Throwable),而 Supplier<T> 则要求将异常包装为 RuntimeException

3.4 中断处理与异常封装

tryLock 过程中,若当前线程被中断(InterruptedException),代码会恢复中断状态并抛出 LockException

catch (InterruptedException e) {
   
    Thread.currentThread().interrupt();
    throw new LockException(lockKey, e);
}

这样的处理符合 Java 并发编程的最佳实践:保留中断标志,同时让上层感知锁获取失败。

4. 使用示例

假设有一个用户积分更新方法,需要按用户 ID 互斥执行:

@Service
public class UserPointService {
   

    @LocalLockable(prefix = "user:point:", key = "#userId", waitTime = 500)
    public void updatePoint(Long userId, Integer delta) {
   
        // 业务逻辑:查询积分、计算、保存
    }
}

如果同一个 userId 的请求在 500ms 内未能获得锁,将抛出 LockCreateException,调用方可以捕获并返回“请稍后重试”的响应。

对于无需超时的场景(如数据初始化任务),可以设置 waitTime = 0,使用阻塞锁:

@LocalLockable(prefix = "task:", key = "'dataSync'", waitTime = 0)
public void syncData() {
   
    // 长时间运行的数据同步
}

5. 注意事项与最佳实践

5.1 适用范围:单机部署

该方案基于 JVM 内的 Striped 锁,无法跨进程工作。在分布式或多实例部署下,应改用分布式锁(如 Redis、ZooKeeper)。因此,本锁仅适用于保证单机内的一致性,或作为分布式锁的本地前置过滤(减少远程锁调用)。

5.2 SpEL 表达式必须保证非空且稳定

key 表达式求值结果将直接作为锁 key 的后半部分。如果表达式返回 null,拼接后的 key 可能为 "prefixnull",导致不同业务意外共享同一锁。建议在表达式中使用 #userId?.toString() ?: 'default' 等形式防御。

5.3 等待时间的选择

  • 对于短操作(如内存计算、单条 SQL),waitTime 可设为 100~500ms。
  • 对于涉及远程调用或复杂事务的操作,等待时间应大于预估的最大执行时间,否则容易触发超时异常。
  • 若业务允许重试,可以在调用方实现重试逻辑。

5.4 避免锁嵌套死锁

Striped 底层使用的是 ReentrantLock,可重入。但不同锁 key 之间的嵌套调用仍可能产生死锁(例如线程 A 持有 key1 等待 key2,线程 B 持有 key2 等待 key1)。建议在涉及多个锁时统一申请顺序,或使用超时锁作为兜底。

5.5 性能考量

1024 个锁实例足以支撑绝大多数场景。若应用有极高频的锁竞争(每秒数万次),可适当增加 stripes 数量(如 Striped.lock(4096)),以降低哈希碰撞概率。需注意,stripes 数量越大,内存开销略增,但远低于为每个 key 创建独立锁。

6. 与同类方案对比

方案 优点 缺点
synchronized(this) 简单 粒度粗,无法按参数隔离
ConcurrentHashMap + 动态锁 完全细粒度 每个 key 创建锁对象,内存膨胀,需手动清理
Guava Striped(本方案) 内存可控,锁池复用,声明式 存在极小哈希碰撞风险,仅限单机
Redisson 分布式锁 跨进程,功能丰富 网络开销,依赖 Redis

7. 总结

本文介绍的基于 Guava Striped 的声明式本地锁,通过注解与 AOP 实现了业务参数级别的细粒度锁控制,具有以下特性:

  • 简洁:一行注解替代手写 lock / unlock 模板代码。
  • 灵活:SpEL 支持动态构建锁 key。
  • 可靠:超时机制与中断处理防止死锁。
  • 高效:Striped 锁池在内存与并发性能间取得平衡。

对于不需要分布式协调的单机应用,这是一个轻量而强大的并发控制工具。其设计模式(注解 + 切面 + 锁服务)也可以轻松扩展至分布式锁,例如替换 LocalLockService 为基于 Redis 的实现,而注解层完全不变。

希望本文能帮助读者理解并合理应用声明式本地锁,提升代码的可维护性与并发安全性。

目录
相关文章
|
22天前
|
设计模式 人工智能 监控
智能体工作流引擎设计:LangGraph与状态机在企业生产中的应用
本文剖析企业级AI Agent工作流核心架构,对比状态机(强确定性、易审计)与LangGraph(图结构、动态规划)两大范式,提出“外层状态机+内层LangGraph”的混合生产模式,并详解状态持久化、事件溯源、人机协同、工具隔离等高可靠设计实践。
142 1
|
22天前
|
Web App开发 人工智能 JavaScript
推荐一款开源 Skill:让 AI Agent 给你做一份"能改"的 PPT,支持 上千套模板!
dashi-ppt-skill是一款开源AI PPT技能(4.3k stars),突破行业痛点:生成后可实时编辑。支持12套主题、1020种版式、8576个控件,网页端可视化修改(拖拽/换色/调图表),一键导出真正可编辑的PPTX(文字/图表保留可修改性),全程本地运行,商业文档零上传。
259 0
推荐一款开源 Skill:让 AI Agent 给你做一份"能改"的 PPT,支持 上千套模板!
|
4月前
|
缓存 安全 搜索推荐
[004][缓存模块]Caffeine缓存自定义:构建灵活的Spring Boot缓存管理器
本文介绍Spring Boot中Caffeine缓存的灵活定制方案:通过自定义`FlexibleCaffeineCacheManager`,支持按缓存名(如users/products)独立配置过期策略、容量等参数,兼顾全局默认与个性化需求;结合线程安全创建器、属性合并机制及无缝Spring集成,实现高性能、易扩展、零侵入的本地缓存管理。(239字)
232 2
|
4月前
|
缓存 NoSQL Java
[006][缓存模块] 两级缓存实战:基于 Caffeine + Redis 的多级缓存设计与实现
本文介绍基于Caffeine(本地)+ Redis(分布式)的两级缓存实战方案,通过自定义`MultiLevelCache`与`MultiLevelCacheManager`,实现Spring Cache标准接口下的透明多级缓存:读优先本地(纳秒级)、未命中查Redis并回填;写同步更新两级,兼顾高性能与数据共享。代码开源可直接集成。
352 0
|
22天前
|
存储 弹性计算 人工智能
阿里云服务器ECS机密计算解决方案,阿里云提供安全合规一站式解决方案
阿里云机密计算基于TEE硬件可信执行环境,提供启动度量、内存加密、远程证明等全生命周期安全能力,支持SGX/TDX/Enclave等多架构,实现“数据可用不可见”,广泛适用于AI推理、金融医疗等高敏场景。详细关于阿里云服务器ECS的机密计算场景解决方案,请参考官方文档:https://t.aliyun.com/U/YpNYIc
阿里云服务器ECS机密计算解决方案,阿里云提供安全合规一站式解决方案
|
22天前
|
消息中间件 Prometheus 监控
[058][调度模块]任务生命周期事件与监听器机制
本文介绍调度模块的任务生命周期事件机制,通过`ChangeStatusEvent`事件与`ChangeStatusEventConsumer`监听器,实现任务状态(CREATED/STARTED/COMPLETED等)变更的灵活监听。支持日志、持久化、消息推送等扩展,具备高解耦性与异常隔离能力。(239字)
64 2
|
20天前
|
存储 缓存 NoSQL
[038][验证码模块]基于 Hutool 的 Spring Boot 验证码组件设计与实现
本文介绍基于Hutool与Spring Boot深度集成的验证码组件,支持线段、圆圈、扭曲、GIF四类验证码,具备自动配置、多级缓存(Caffeine+Redis)、高斯模糊增强、忽略大小写校验及一次性使用等特性,架构清晰、扩展性强,开箱即用。
57 2
|
22天前
|
人工智能 自然语言处理 安全
大模型API Key散落在系统里,有什么风险
企业接入大模型时,很多团队会先把 API Key 放进各个业务系统里,快速完成试点。但当 AI 应用增多后,密钥分散会带来权限难回收、成本难分摊、调用难追溯和安全边界不清等问题。
大模型API Key散落在系统里,有什么风险
|
22天前
|
JSON 运维 前端开发
宜搭调用外部 API 失败?阿里云国际版代理商:鉴权与日志全维度排查指南
低代码平台集成外部系统时,API 调用的稳定性直接决定业务流程能否跑通。在宜搭的实际使用中,“调用外部 API 失败”几乎是最频繁出现的故障信息之一,但报错界面往往只给一句笼统提示,不告诉你具体卡在哪一环。根据大量集成项目的排障复盘,认证配置、参数格式与超时策略这三项问题占据了绝大多数失败原因,而且它们之间经常交叉影响,形成一种“哪儿都像问题”的假象。
宜搭调用外部 API 失败?阿里云国际版代理商:鉴权与日志全维度排查指南
|
22天前
|
人工智能 算法 安全
GEO行业的"奠基人",为什么拿不出一篇奠基性文章?
2026年GEO培训市场乱象丛生:大批讲师自封“奠基人”,却无一篇奠基性文章——缺新概念、缺可验证方法论、缺实战数据支撑。真奠基者如王耀恒,三年深耕AI搜索实践,输出可复现方法论与行业观察,从不自诩头衔,只以“亲手跑通者”自居。