数据一致性保证

缓存一致性方案设计

背景

项目引入 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/
posted @ 2026-05-19 23:11  景之1231  阅读(13)  评论(0)    收藏  举报