缓存

缓存设计与实践

IMS 系统中大量读操作(菜单、配置、字典项)适合用缓存提升性能。Redis 是最常用的缓存中间件,本文介绍 IMS 中的缓存应用场景与避坑指南。

一、缓存策略选型

  • Cache-Aside:应用自己维护缓存,命中率最高,推荐使用
  • Read-Through:缓存负责加载数据,应用无感知
  • Write-Through:写入时同步更新缓存,一致性好但性能略差
  • Write-Behind:异步写入缓存,性能最好但有丢数据风险
/* Cache-Aside 模式示例 */
// 读取
String cacheKey = "user:" + userId;
String cached = redis.get(cacheKey);
if (cached != null) {
    return JSON.parse(cached);
}
User user = db.findById(userId);
redis.setex(cacheKey, 3600, JSON.stringify(user));
return user;

二、常见缓存问题与解决

1. 缓存穿透

大量请求查询不存在的数据,直接打到数据库。

  • 布隆过滤器:用少量内存存储所有存在的 key
  • 空值缓存:对不存在的结果也缓存,但设置较短过期时间

2. 缓存击穿

热点 key 过期瞬间,大量请求涌入数据库。

  • 互斥锁:只允许一个请求去加载数据
  • 永不过期:逻辑过期而非物理过期

3. 缓存雪崩

大量 key 同时过期,导致数据库压力突增。

  • 随机过期时间:给过期时间加随机偏移量
  • 多级缓存:本地缓存 + Redis 缓存

三、分布式锁

/* Redis 分布式锁实现 */
String lockKey = "lock:order:" + orderId;
String lockValue = UUID.randomUUID().toString();

/* 获取锁(NX + PX) */
Boolean acquired = redis.set(lockKey, lockValue,
    SetArgs.Builder.nx().px(10000));

if (Boolean.TRUE.equals(acquired)) {
    try {
        /* 执行业务逻辑 */
        processOrder(orderId);
    } finally {
        /* 释放锁( Lua 脚本保证原子性) */
        redis.eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end",
            ScriptOutput.VALUE, lockKey, lockValue);
    }
}

四、热点数据处理

  • 本地缓存:使用 Caffiene 或 Guava Cache 缓存热点数据
  • 预加载:系统启动时加载高频数据到缓存
  • 熔断降级:缓存失效时返回默认值而非直接查库
IMS 缓存应用场景:
  • 菜单权限(变化少,查询频繁)
  • 字典项(全局配置)
  • 用户信息(热点数据)
  • 流程定义(审批节点配置)

五、总结

  • Cache-Aside 是最常用的缓存模式
  • 处理缓存穿透、击穿、雪崩三大问题
  • 分布式锁用 Redis SET NX + Lua 脚本实现
  • 热点数据用本地缓存 + Redis 多级缓存