# memo-agent **Repository Path**: ingrun/memo-agent ## Basic Information - **Project Name**: memo-agent - **Description**: 轻量 Java 方法级缓存组件。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-13 - **Last Updated**: 2026-08-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # MemoAgent 轻量 Java 方法级缓存组件。支持**内存缓存**和 **Redis 缓存**两种后端,对外暴露统一的静态门面 `MemoAgent`。 同时兼容 **Spring Boot 2.x** 与 **Spring Boot 3.x**(`memo-agent-sb-redis` 同一份代码)。 --- ## 特性 - 内存 / Redis 双后端,`MemoCache` SPI 可扩展 - 静态门面 `MemoAgent`,方法级 `computeIfAbsent` 缓存 - 双重检查锁**防击穿**(同一 key 并发未命中时 supplier 只执行一次) - 支持**缓存 null 值**(通过 `containsKey` 区分"缓存了 null"和"没缓存过") - 无 Spring 依赖的 core 模块可单独用于纯 Java 项目 --- ## 模块结构 ``` memo-agent (父 pom,仅聚合,=1.0.0) ├── memo-agent-core 核心库,无任何 Spring 依赖(仅 jackson-databind) ├── memo-agent-sb-redis Spring Boot 自动装配模块(兼容 Boot 2 / Boot 3),依赖 core ├── memo-agent-demo Spring Boot 2 演示应用(独立项目,不参与聚合) └── memo-agent-sb3-demo Spring Boot 3 演示应用(独立项目,不参与聚合) ``` > `memo-agent-demo` / `memo-agent-sb3-demo` 是**独立 Maven 项目**,不继承 memo-agent 父 pom,分别使用 > `spring-boot-starter-parent 2.7.5` / `3.5.13` 验证 sb-redis 在两个版本下的行为。 --- ## 快速开始 ### 1. 纯内存缓存(无需 Spring) ```java // 手动装配 MemoHandler handler = new MemoHandler(); handler.setMemoCache(new MemoryMemoCache()); MemoAgent.setMemoHandler(handler); // 使用 String key = Util.genKey("user", userId); UserInfo r1 = MemoAgent.computeIfAbsent(key, 30_000, () -> queryDB(userId)); UserInfo r2 = MemoAgent.computeIfAbsent(key, 30_000, () -> queryDB(userId)); // r2 命中缓存,不会执行 queryDB ``` ### 2. Redis 缓存(Spring Boot 项目) 引入 `memo-agent-sb-redis` 后自动装配,无需手动创建 Handler: ```xml cn.ingrun memo-agent-sb-redis 1.0.0 ``` ```yaml # application.yml(Boot 3 用 spring.data.redis.*,Boot 2 用 spring.redis.*) spring: data: redis: host: localhost port: 6379 ``` ```java @RestController public class UserController { @GetMapping("/user/{id}") public UserInfo getUser(@PathVariable String id) { return MemoAgent.computeIfAbsent("user:" + id, 30_000, () -> queryDB(id)); } } ``` 自动装配内容: | Bean | 说明 | |------|------| | `RedisSerializer` | 优先用户自定义 Bean,否则默认 `GenericJackson2JsonRedisSerializer` | | `RedisMemoCache` | 基于 Redis 的 `MemoCache` 实现 | | `MemoHandler` | 创建后自动调用 `MemoAgent.setMemoHandler` 初始化静态门面 | **自定义序列化器**:实现 Spring 的 `RedisSerializer` 并注册为 Bean 即可覆盖默认(参考 demo 中的 `FastJsonRedisSerializer`): ```java @Bean public RedisSerializer redisSerializer() { return new FastJsonRedisSerializer(); } ``` --- ## 核心概念 ### computeIfAbsent — 防击穿 `MemoHandler.computeIfAbsent` 的逻辑: 1. 先用 `containsKey` 区分"缓存了 null"和"没缓存过"; 2. 未命中时加 `ReentrantLock`(static,全局粒度)做双重检查锁,**只让第一个线程执行 supplier 并回填**,其余线程等待后直接命中缓存。 ### TTL 语义 | ttl 值 | 含义 | |--------|------| | `ttl <= 0` | **永不过期**(`computeIfAbsent(key, supplier)` 无 ttl 重载即此语义) | | `ttl > 0` | 毫秒数,到期后重新执行 supplier | ### null 值缓存 supplier 返回 `null` 时也会写入缓存(内存后端直接存 null,Redis 后端用哨兵 `"\0MEMO_NULL\0"`)。再次访问直接返回 null,**不会重复执行 supplier**。`containsKey` 可区分"缓存了 null"和"key 不存在"。 ### Key 生成 `Util.genKey(prefix, args...)` → `prefix:json(arg1):json(arg2)...`,String 直接拼接,其他类型走 Jackson 序列化。保证"值相等则 key 相等"。 --- ## API 参考 ### MemoAgent — 唯一入口(全部静态方法) | 方法 | 说明 | |------|------| | ` R computeIfAbsent(String key, long ttl, Supplier)` | 缓存读取,未命中执行 supplier 并缓存 | | ` R computeIfAbsent(String key, Supplier)` | 同上,TTL 永不过期 | | ` R get(String key)` | 只读查询,不触发回源 | | `put(String key, Object value, long ttl)` | 直接覆盖缓存 | | `remove(String key)` | 移除缓存 | | `updateTtl(String key, long ttl)` | 修改 TTL(-1 永不过期) | ### Util — Key 生成 | 方法 | 说明 | |------|------| | `genKey(String prefix, Object... args)` | `prefix:json(arg1):json(arg2)...` | --- ## SPI — MemoCache 缓存后端 ```java public interface MemoCache { void setCache(String key, Object value, long ttl); Object getCache(String key); void removeCache(String key); void updateTtl(String key, long ttl); long getRemainingTtl(String key); boolean containsKey(String key); // 区分"缓存了 null"和"key 不存在" } ``` 内置实现: | 实现 | 模块 | 存储 | 分布式 | |------|------|------|:---:| | `MemoryMemoCache` | core | ConcurrentHashMap + 惰性删除 | 不支持 | | `RedisMemoCache` | sb-redis | Redis String + 原生 EXPIRE | 支持 | > `getRemainingTtl` 语义:Memory 后端不存在返回 `-1`、永不过期返回 `Long.MAX_VALUE`; > Redis 后端遵循 Redis 的 TTL 约定(`-2`=key 不存在、`-1`=永不过期,由 `RedisMemoCache` 归一化处理)。 > 新增后端时务必同时保证 `containsKey` 与 `getRemainingTtl` 的语义正确。 --- ## Spring Boot 双版本支持 `memo-agent-sb-redis` 同时兼容 Boot 2 / Boot 3: - 自动配置类使用 `@AutoConfiguration` 注解; - 同时提供两份注册文件: - `META-INF/spring.factories`(Boot 2.x 读取) - `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`(Boot 2.7+ / Boot 3 读取) - 代码仅使用 Java 8 语法 + 两个版本共有的 API(`StringRedisTemplate`、`RedisSerializer` 等),不涉及 `javax/jakarta` 迁移。 **Redis 配置前缀差异**:Boot 2 用 `spring.redis.*`,Boot 3 用 `spring.data.redis.*`。 --- ## 运行 Demo 需要本地可用的 Redis。 ### Boot 2 demo(端口 8080) ```bash mvn install # 先把 core + sb-redis 装到本地仓库 mvn -f memo-agent-demo/pom.xml spring-boot:run ``` ### Boot 3 demo(端口 8081) ```bash mvn install # 先把 core + sb-redis 装到本地仓库 mvn -f memo-agent-sb3-demo/pom.xml spring-boot:run ``` > demo 是独立项目,因此先 `mvn install` 根项目(flatten-maven-plugin 会在安装时展开 `${revision}` 占位符)。 > Redis 连接配置在各自 `src/main/resources/application-local.yml`(**已 gitignore**,含真实凭据,勿提交)。 ### 演示接口 | 接口 | 说明 | |------|------| | `GET /demo/memo?userId=1001` | 获取用户(首次慢,30 秒内重复访问命中缓存) | | `GET /demo/stats` | 查看每个 userId 的真实查询次数 | | `DELETE /demo/memo?userId=1001` | 清除缓存 | --- ## 构建与测试 ```bash # 全量构建 + 安装(root 聚合 core + sb-redis) mvn clean install # 只跑 core 模块测试 mvn -pl memo-agent-core test ``` - 测试框架为 **JUnit 4**,共 **29 个**用例,全部在 `memo-agent-core` 模块(不依赖 Spring): - `MemoHandlerTest`(11)— computeIfAbsent、null 缓存、过期重算、并发防击穿 - `MemoryMemoCacheTest`(14)— 六个方法全量覆盖、惰性删除、getRemainingTtl 语义 - `MemoAgentTest`(4)— 静态门面委托 - `sb-redis` 模块无单测,通过两个 demo 做集成验证。 --- ## 技术栈 - **Java 8**(core / sb-redis,target 8;sb3-demo 为 target 17) - **Spring Boot 2.7.5 / 3.5.13**(sb-redis 双版本兼容,demo 各自使用) - **Spring Data Redis** — Redis 缓存后端 - **Jackson** — key 生成与默认序列化(`GenericJackson2JsonRedisSerializer`) - **SLF4J** — 日志门面 - **Lombok** — 编译期,provided scope - **JUnit 4** — core 模块单元测试