数据一致性保证
缓存一致性方案设计
背景
项目引入 Redis 替代 MyBatis 二级缓存,为 param_config 表及未来实体提供统一的缓存层。核心诉求是保证 DB 与 Redis 之间的数据一致性。
场景假设
- 读写模式: 读多写少
- 一致性要求: 允许短暂不一致(最终一致性即可)
- 核心查询维度: 按主键 ID + 按联合唯一键
(tenantId, paramKey),两种都需要缓存 - 环境: 内部应用,无需考虑恶意穿透攻击和大量 Key 同时过期
- 未来扩展: 需要支持新实体快速接入缓存,而非 ParamConfig 专属方案
方案选择:延迟双删(Cache-Aside 增强版)
在标准 Cache-Aside 基础上,写入 DB 后增加一次异步延迟删除,消除并发读写导致的极端脏数据窗口。
为什么选延迟双删
标准 Cache-Aside 在极端并发时序下存在脏数据风险:
T1: 线程 B 查询 → cache miss → 读到 DB 旧值(此时线程 A 正在写 DB)
T2: 线程 A 写 DB 完成 → 删除缓存
T3: 线程 B 把旧值回写到缓存 ← 脏数据进缓存,直到 TTL 过期才纠正
延迟双删通过写后再次删除来覆盖这个窗口:
1. 删除缓存(一删)
2. 更新 DB
3. 等待 500ms 后再次删除缓存(二删) ← 覆盖并发读回写的窗口
为什么不用更重的方案
- 消息队列订阅 binlog(Canal): 引入外部依赖,本项目规模不匹配
- 分布式事务(如 Seata): 对单表 CRUD 来说过度重
- Read/Write-Through: 需要缓存层代理 DB,与现有 MyBatis 架构冲突
核心架构
分层结构
Controller 层 ← 不变
↓
Service 层 ← 加入 AbstractCacheHandler 调用
↓
AbstractCacheHandler(通用缓存模板) ← 新建,核心
↓ ↓
Redis ParamConfigMapper(DB)
抽象模板设计
通过 AbstractCacheHandler<T> 将缓存流程抽象为通用基础设施。未来任何实体只需实现 3 个方法即可完整接入缓存。
// 子类需要实现的 3 个抽象方法
abstract T loadFromDb(Long id); // 按 ID 查 DB
abstract T loadFromDbByKey(String... parts); // 按联合键查 DB
abstract String cacheName(); // 实体名,用于 Key 前缀
对外暴露的核心方法
| 方法 | 说明 |
|---|---|
T getById(Long id) |
Cache-Aside 读取(按 ID) |
T getByKey(String... parts) |
Cache-Aside 读取(按联合键) |
void evict(Long id, String... keyParts) |
一删(同步)+ 延迟二删(异步) |
Key 命名规范
统一格式: learn:{entity}:{dimension}:{value}
示例:
learn:param:id:42
learn:param:key:t001:maxRetry
learn:user:id:100
learn:user:key:org1:admin
learn为项目命名空间{entity}由cacheName()提供{dimension}区分按 ID 还是按联合键{value}为实际查询值
核心流程
读流程
1. 用 Key 模板拼出 Redis Key
2. 查 Redis
├─ 命中 → 反序列化返回
└─ 未命中
3. 调用子类的 loadFromDb() / loadFromDbByKey()
4. DB 返回实体 → 序列化写入 Redis(TTL 含随机偏移)→ 返回
5. DB 返回 null → 直接返回 null(不缓存空值)
不做空值缓存。内部应用,穿透不是威胁,避免增加复杂度。
写流程(延迟双删)
Service 层调用 evict(id, keyParts):
1. 拼出所有相关 Key(ID Key + 联合键 Key)
2. redis.delete(keys) ← 一删,同步
3. ScheduledExecutorService.schedule(fn, 500ms) ← 二删,异步延迟 500ms
fn:
重试 delete(keys),最多 3 次
间隔递增: 200ms → 400ms → 600ms
全部失败 → 日志告警,由 TTL 兜底
调用方(Service 层)使用约定
// 写入操作的标准调用顺序
public void update(ParamConfigUpdateDTO dto) {
ParamConfigDO record = toEntity(dto);
// 1. 先 evict 缓存(一删同步)
cacheHandler.evict(record.getId(), record.getTenantId(), record.getParamKey());
// 2. 再更新 DB
mapper.updateById(record);
// 3. 二删由 evict 内部异步执行
}
并发与安全
缓存击穿防护
不做。当前单实例 + 读多写少,热点 Key 过期瞬间的大量 miss 对 PostgreSQL 压力可控。
缓存雪崩防护
通过 TTL 随机偏移解决:
实际 TTL = TTL_BASE + random(0, TTL_DEVIATION)
默认: TTL_BASE = 1800s (30min), TTL_DEVIATION = 300s (5min)
→ 实际 TTL 分布在 1800~2100s
子类可覆盖这两个参数。
异常降级
Redis 任何操作异常时,打印 WARN 日志并降级直接查 DB,不抛出异常:
try {
return redis.get(key);
} catch (RedisException e) {
log.warn("Redis 不可用,降级到 DB", e);
return loadFromDb(id); // 直接走 DB
}
序列化
使用 Jackson2JsonRedisSerializer,包裹在 GenericJackson2JsonRedisSerializer 中:
- 序列化结果: JSON 字符串,Redis 中可读
- 反序列化: 自动还原为 Java 对象
- ObjectMapper 配置: 自动注册 JavaTimeModule(处理 LocalDateTime)
参数配置
| 参数 | 默认值 | 说明 | 是否可被子类覆盖 |
|---|---|---|---|
| TTL_BASE | 1800s(30min) | 基础过期时间 | ✅ |
| TTL_DEVIATION | 300s(5min) | 随机偏移上限 | ✅ |
| DELAY_DELETE_MS | 500ms | 一删与二删的间隔 | ❌ |
| MAX_RETRY | 3 | 二删失败最大重试 | ❌ |
线程池
二删使用的 ScheduledExecutorService:
| 参数 | 值 |
|---|---|
| 核心线程数 | 2 |
| 拒绝策略 | CallerRunsPolicy |
文件变更清单
| 文件 | 操作 | 职责 |
|---|---|---|
common/cache/AbstractCacheHandler.java |
新建 | 通用缓存模板(get/evict/序列化/Key 构建) |
config/CacheConfig.java |
新建 | RedisTemplate 序列化配置 + 线程池 Bean |
service/cache/ParamConfigCacheHandler.java |
新建 | ParamConfig 的 DB 加载实现 |
service/impl/ParamConfigServiceImpl.java |
修改 | 注入 CacheHandler,替换直接 Mapper 调用 |
resources/application.yaml |
修改 | 关闭 MyBatis 二级缓存 cache-enabled: false |
.gitignore |
修改 | 添加 .superpowers/ 目录 |
新实体接入示例
假设未来需要为 user_config 表加缓存,只需新建一个 Handler:
@Service
public class UserConfigCacheHandler extends AbstractCacheHandler<UserConfigDO> {
@Override
String cacheName() { return "user_config"; }
@Override
UserConfigDO loadFromDb(Long id) {
return userConfigMapper.selectById(id);
}
@Override
UserConfigDO loadFromDbByKey(String... parts) {
return userConfigMapper.selectByUserId(parts[0]);
}
}
然后在 UserConfigService 中注入 UserConfigCacheHandler,读写操作前调用 getById/getByKey,写入后调用 evict。无需修改任何框架代码。
设计决策回顾
| 决策 | 选择 | 理由 |
|---|---|---|
| 一致性方案 | 延迟双删 | 消除极端并发脏读窗口,比纯 Cache-Aside 更可靠 |
| 抽象层级 | AbstractCacheHandler 模板 | 新实体 3 方法接入,零框架改动 |
| 序列化 | Jackson2JsonRedisSerializer | JSON 可读、天然支持复杂对象 |
| 二删实现 | ScheduledExecutorService | 不阻塞调用线程,重试逻辑可控 |
| 穿透防护 | 不做 | 内部应用,无恶意请求 |
| 击穿防护 | 不做 | 单实例,DB 扛得住 |
| MyBatis 二级缓存 | 关闭 | 避免与 Redis 双写导致不一致 |
| Key 前缀 | learn:{entity}:... |
项目命名空间隔离,多实体不冲突 |
缓存一致性方案 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 为项目引入基于 Redis 的延迟双删缓存一致性方案,通过 AbstractCacheHandler<T> 抽象模板实现通用缓存基础设施
Architecture: Service 层注入实体对应的 CacheHandler 子类,CacheHandler 内部封装 Cache-Aside 读取 + 延迟双删驱逐逻辑,底层使用 StringRedisTemplate + ObjectMapper 做 JSON 序列化
Tech Stack: Spring Boot 3.5, MyBatis 3.0.5, Redis (Lettuce), Jackson, Java 21
文件结构
src/main/java/com/liang/learn/
├── common/
│ └── cache/
│ └── AbstractCacheHandler.java ← 新建,通用缓存模板
├── config/
│ └── CacheConfig.java ← 新建,线程池 Bean
└── service/
├── cache/
│ └── ParamConfigCacheHandler.java ← 新建,ParamConfig 缓存实现
└── impl/
└── ParamConfigServiceImpl.java ← 修改,接入缓存
src/main/resources/
└── application.yaml ← 修改,关闭 MyBatis 二级缓存
.gitignore ← 修改,排除 .superpowers/
Task 1: 关闭 MyBatis 二级缓存 + 更新 .gitignore
Files:
-
Modify:
src/main/resources/application.yaml:56 -
Modify:
.gitignore
在 src/main/resources/application.yaml 中,将第 56 行的 cache-enabled: true 改为 cache-enabled: false:
# 修改前(第 56 行):
cache-enabled: true # 开启二级缓存
# 修改后:
cache-enabled: false # 关闭二级缓存,统一使用 Redis
在 .gitignore 文件末尾追加一行:
.superpowers/
git add src/main/resources/application.yaml .gitignore
git commit -m "chore: 关闭 MyBatis 二级缓存,统一使用 Redis 缓存;排除 .superpowers 目录"
Task 2: 新建 CacheConfig — 线程池 Bean
Files:
-
Create:
src/main/java/com/liang/learn/config/CacheConfig.java
package com.liang.learn.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 缓存配置类
*
* @author liang
* @since 2026-05-19 22:00:00
*/
@Configuration
public class CacheConfig {
/**
* 延迟双删专用线程池
*
* @return ScheduledExecutorService
*/
@Bean
public ScheduledExecutorService cacheEvictExecutor() {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
mvn compile -q
预期:BUILD SUCCESS
git add src/main/java/com/liang/learn/config/CacheConfig.java
git commit -m "feat: 添加缓存延迟删除线程池配置"
Task 3: 新建 AbstractCacheHandler — 通用缓存模板
Files:
-
Create:
src/main/java/com/liang/learn/common/cache/AbstractCacheHandler.java
package com.liang.learn.common.cache;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* 缓存操作抽象模板
* <p>
* 子类只需实现 {@link #loadFromDb(Long)}、{@link #loadFromDbByKey(String...)}、
* {@link #cacheName()} 三个方法即可完整接入缓存。
* </p>
*
* @param <T> 实体类型
* @author liang
* @since 2026-05-19 22:00:00
*/
@Slf4j
public abstract class AbstractCacheHandler<T> {
private static final String KEY_PREFIX = "learn";
private static final long DELAY_DELETE_MS = 500;
private static final int MAX_RETRY = 3;
private static final long RETRY_BASE_MS = 200;
protected final StringRedisTemplate stringRedisTemplate;
protected final ObjectMapper objectMapper;
protected final ScheduledExecutorService cacheEvictExecutor;
private final Class<T> entityClass;
protected AbstractCacheHandler(StringRedisTemplate stringRedisTemplate,
ObjectMapper objectMapper,
ScheduledExecutorService cacheEvictExecutor,
Class<T> entityClass) {
this.stringRedisTemplate = stringRedisTemplate;
this.objectMapper = objectMapper;
this.cacheEvictExecutor = cacheEvictExecutor;
this.entityClass = entityClass;
}
/**
* 实体标识名,用于构造缓存 Key 前缀
*
* @return 实体名,如 "param"
*/
protected abstract String cacheName();
/**
* 按主键 ID 从 DB 加载实体
*
* @param id 主键 ID
* @return 实体,不存在返回 null
*/
protected abstract T loadFromDb(Long id);
/**
* 按联合键从 DB 加载实体
*
* @param parts 联合键的各部分,由子类定义含义
* @return 实体,不存在返回 null
*/
protected abstract T loadFromDbByKey(String... parts);
/**
* 基础 TTL(秒),子类可覆盖
*/
protected long ttlBaseSeconds() {
return 1800;
}
/**
* TTL 随机偏移上限(秒),子类可覆盖
*/
protected long ttlDeviationSeconds() {
return 300;
}
/**
* 按主键 ID 读取(走缓存)
*
* @param id 主键 ID
* @return 实体,不存在返回 null
*/
public T getById(Long id) {
String key = buildIdKey(id);
String json = getFromRedis(key);
if (json != null) {
return deserialize(json);
}
T entity = loadFromDb(id);
if (entity != null) {
putToCache(key, entity);
}
return entity;
}
/**
* 按联合键读取(走缓存)
*
* @param parts 联合键的各部分
* @return 实体,不存在返回 null
*/
public T getByKey(String... parts) {
String key = buildKeyByParts(parts);
String json = getFromRedis(key);
if (json != null) {
return deserialize(json);
}
T entity = loadFromDbByKey(parts);
if (entity != null) {
putToCache(key, entity);
}
return entity;
}
/**
* 驱逐缓存(一删同步 + 二删异步延迟)
*
* @param id 主键 ID,可为 null(只驱逐 key 维度)
* @param keyParts 联合键的各部分,可为空
*/
public void evict(Long id, String... keyParts) {
List<String> keys = new ArrayList<>();
if (id != null) {
keys.add(buildIdKey(id));
}
if (keyParts.length > 0) {
keys.add(buildKeyByParts(keyParts));
}
if (keys.isEmpty()) {
return;
}
String[] keyArray = keys.toArray(new String[0]);
stringRedisTemplate.delete(keyArray);
log.debug("一删完成, keys={}", keys);
cacheEvictExecutor.schedule(() -> retryDelete(keyArray), DELAY_DELETE_MS, TimeUnit.MILLISECONDS);
}
private String getFromRedis(String key) {
try {
return stringRedisTemplate.opsForValue().get(key);
} catch (Exception e) {
log.warn("Redis 读取异常,降级查 DB, key={}", key, e);
return null;
}
}
private void putToCache(String key, T entity) {
try {
String json = objectMapper.writeValueAsString(entity);
long ttl = ttlBaseSeconds() + ThreadLocalRandom.current().nextLong(ttlDeviationSeconds());
stringRedisTemplate.opsForValue().set(key, json, ttl, TimeUnit.SECONDS);
} catch (JsonProcessingException e) {
log.error("序列化失败, key={}", key, e);
} catch (Exception e) {
log.warn("Redis 写入异常, key={}", key, e);
}
}
private T deserialize(String json) {
try {
return objectMapper.readValue(json, entityClass);
} catch (JsonProcessingException e) {
log.error("反序列化失败, json={}", json, e);
return null;
}
}
private String buildIdKey(Long id) {
return KEY_PREFIX + ":" + cacheName() + ":id:" + id;
}
private String buildKeyByParts(String... parts) {
return KEY_PREFIX + ":" + cacheName() + ":key:" + String.join(":", parts);
}
private void retryDelete(String[] keys) {
for (int i = 0; i < MAX_RETRY; i++) {
try {
stringRedisTemplate.delete(keys);
log.debug("二删成功, retry={}, keys={}", i, List.of(keys));
return;
} catch (Exception e) {
log.debug("二删失败, retry={}, keys={}", i, List.of(keys), e);
try {
Thread.sleep(RETRY_BASE_MS * (i + 1));
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
return;
}
}
}
log.error("二删最终失败,由 TTL 兜底, keys={}", List.of(keys));
}
}
mvn compile -q
预期:BUILD SUCCESS
git add src/main/java/com/liang/learn/common/cache/AbstractCacheHandler.java
git commit -m "feat: 添加通用缓存抽象模板 AbstractCacheHandler"
Task 4: 新建 ParamConfigCacheHandler — ParamConfig 缓存实现
Files:
-
Create:
src/main/java/com/liang/learn/service/cache/ParamConfigCacheHandler.java
package com.liang.learn.service.cache;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.liang.learn.common.cache.AbstractCacheHandler;
import com.liang.learn.mapper.ParamConfigMapper;
import com.liang.learn.model.entity.ParamConfigDO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.ScheduledExecutorService;
/**
* 参数配置缓存处理器
*
* @author liang
* @since 2026-05-19 22:00:00
*/
@Component
public class ParamConfigCacheHandler extends AbstractCacheHandler<ParamConfigDO> {
private final ParamConfigMapper paramConfigMapper;
public ParamConfigCacheHandler(StringRedisTemplate stringRedisTemplate,
ObjectMapper objectMapper,
ScheduledExecutorService cacheEvictExecutor,
ParamConfigMapper paramConfigMapper) {
super(stringRedisTemplate, objectMapper, cacheEvictExecutor, ParamConfigDO.class);
this.paramConfigMapper = paramConfigMapper;
}
@Override
protected String cacheName() {
return "param";
}
@Override
protected ParamConfigDO loadFromDb(Long id) {
return paramConfigMapper.selectById(id);
}
@Override
protected ParamConfigDO loadFromDbByKey(String... parts) {
return paramConfigMapper.selectByTenantIdAndParamKey(parts[0], parts[1]);
}
}
mvn compile -q
预期:BUILD SUCCESS
git add src/main/java/com/liang/learn/service/cache/ParamConfigCacheHandler.java
git commit -m "feat: 添加 ParamConfig 缓存处理器"
Task 5: 修改 ParamConfigServiceImpl — 接入缓存
Files:
-
Modify:
src/main/java/com/liang/learn/service/impl/ParamConfigServiceImpl.java
在类顶部新增依赖注入字段 paramConfigCacheHandler,替换原来的 paramConfigMapper 直接查询场景。
修改后的完整文件内容如下(只标注变更部分,其余不变):
修改 1:顶部新增 import
在第 4 行之后插入:
import com.liang.learn.service.cache.ParamConfigCacheHandler;
修改 2:新增字段
在第 35 行 private final ParamConfigMapper paramConfigMapper; 之后新增:
private final ParamConfigCacheHandler cacheHandler;
修改 3:修改 detail 方法(第 67-76 行)
将:
@Override
public ParamConfigVO detail(Long id) {
log.info("查询参数配置详情,id={}", id);
ParamConfigDO paramConfig = paramConfigMapper.selectById(id);
if (paramConfig == null) {
throw new BusinessException(ErrorCode.NOT_FOUND, "参数配置不存在");
}
return convertToVO(paramConfig);
}
改为:
@Override
public ParamConfigVO detail(Long id) {
log.info("查询参数配置详情,id={}", id);
ParamConfigDO paramConfig = cacheHandler.getById(id);
if (paramConfig == null) {
throw new BusinessException(ErrorCode.NOT_FOUND, "参数配置不存在");
}
return convertToVO(paramConfig);
}
修改 4:修改 add 方法(第 78-104 行)
在 paramConfigMapper.insert(paramConfig); 之后、return convertToVO(paramConfig); 之前,新增一行 evict:
paramConfigMapper.insert(paramConfig);
cacheHandler.evict(null, paramConfig.getTenantId(), paramConfig.getParamKey());
return convertToVO(paramConfig);
修改 5:修改 update 方法(第 106-139 行)
在末尾 paramConfigMapper.updateById(paramConfig); 之前执行驱逐,方法改为:
@Override
@Transactional(rollbackFor = Exception.class)
public ParamConfigVO update(ParamConfigUpdateDTO updateDTO) {
log.info("更新参数配置,id={}", updateDTO.getId());
ParamConfigDO paramConfig = cacheHandler.getById(updateDTO.getId());
if (paramConfig == null) {
throw new BusinessException(ErrorCode.NOT_FOUND, "参数配置不存在");
}
boolean keyChanged = false;
if (StringUtils.hasText(updateDTO.getParamKey()) && !updateDTO.getParamKey().equals(paramConfig.getParamKey())) {
ParamConfigDO existing = cacheHandler.getByKey(paramConfig.getTenantId(), updateDTO.getParamKey());
if (existing != null && !existing.getId().equals(updateDTO.getId())) {
throw new BusinessException(ErrorCode.BUSINESS_ERROR, "参数key已存在");
}
paramConfig.setParamKey(updateDTO.getParamKey());
keyChanged = true;
}
if (updateDTO.getParamValue() != null) {
paramConfig.setParamValue(updateDTO.getParamValue());
}
if (updateDTO.getUpdaterId() != null) {
paramConfig.setUpdaterId(updateDTO.getUpdaterId());
}
paramConfig.setUpdateTime(LocalDateTime.now());
cacheHandler.evict(paramConfig.getId(), paramConfig.getTenantId(), paramConfig.getParamKey());
if (keyChanged) {
cacheHandler.evict(null, paramConfig.getTenantId(), updateDTO.getParamKey());
}
paramConfigMapper.updateById(paramConfig);
return convertToVO(paramConfig);
}
修改 6:修改 delete 方法(第 141-154 行)
在 paramConfigMapper.deleteById(id, updaterId); 之前执行驱逐:
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long id, Long updaterId) {
log.info("删除参数配置,id={}", id);
ParamConfigDO paramConfig = cacheHandler.getById(id);
if (paramConfig == null) {
throw new BusinessException(ErrorCode.NOT_FOUND, "参数配置不存在");
}
cacheHandler.evict(paramConfig.getId(), paramConfig.getTenantId(), paramConfig.getParamKey());
paramConfigMapper.deleteById(id, updaterId);
}
修改 7:修改 add 方法中的唯一性检查
将 add 方法中的:
ParamConfigDO existing = paramConfigMapper.selectByTenantIdAndParamKey(addDTO.getTenantId(), addDTO.getParamKey());
改为:
ParamConfigDO existing = cacheHandler.getByKey(addDTO.getTenantId(), addDTO.getParamKey());
mvn compile -q
预期:BUILD SUCCESS
git add src/main/java/com/liang/learn/service/impl/ParamConfigServiceImpl.java
git commit -m "feat: ParamConfigService 接入 Redis 缓存,使用延迟双删策略"
Task 6: 验证 — 启动应用并测试 CRUD
Files: 无新建/修改
mvn spring-boot:run
预期:应用在 9001 端口正常启动,日志中无 Redis 连接错误。
curl -X POST http://localhost:9001/api/param-config/detail -H "Content-Type: application/json" -d "{\"id\":1}"
预期:返回数据。查看 Redis:learn:param:id:1 应有缓存数据。
curl -X POST http://localhost:9001/api/param-config/update -H "Content-Type: application/json" -d "{\"id\":1,\"paramValue\":\"updated-value\",\"updaterId\":1}"
预期:更新成功。Redis 中 learn:param:id:1 被删除,下次 detail 查询时重建。
curl -X POST http://localhost:9001/api/param-config/add -H "Content-Type: application/json" -d "{\"tenantId\":\"t001\",\"paramKey\":\"test.new.key\",\"paramValue\":\"new-value\",\"creatorId\":1}"
预期:新增成功,对应 key 缓存被驱逐。
curl -X POST http://localhost:9001/api/param-config/delete -H "Content-Type: application/json" -d "{\"id\":2,\"updaterId\":1}"
预期:逻辑删除成功,相关缓存被驱逐。
手动停止 Redis 服务,再次调用 detail 接口。预期:接口正常返回数据,日志中出现 WARN 级别的 Redis 降级日志。
git add .
git commit -m "test: 验证缓存读写及延迟双删功能"
变更汇总
| 文件 | 操作 | 说明 |
|---|---|---|
common/cache/AbstractCacheHandler.java |
新建 | 通用缓存模板,getById/getByKey/evict |
config/CacheConfig.java |
新建 | 延迟删除线程池 ScheduledExecutorService Bean |
service/cache/ParamConfigCacheHandler.java |
新建 | 实现 cacheName + loadFromDb + loadFromDbByKey |
service/impl/ParamConfigServiceImpl.java |
修改 | detail/add/update/delete 全部接入缓存 |
resources/application.yaml |
修改 | cache-enabled: false |
.gitignore |
修改 | 追加 .superpowers/ |
浙公网安备 33010602011771号