Skip to content

缓存规范 ​

TIP

规定后端缓存的使用方式,适用于 Redis + Caffeine 二级缓存 架构。

设计原则 ​

原则说明
二级缓存Caffeine(一级/本地缓存)→ Redis(二级/分布式缓存)→ DB
缓存 Key 统一Key 前缀集中管理,避免散落在业务代码中
过期策略不同业务数据设置合理过期时间,避免缓存永久有效
一致性数据变更时主动清除/更新缓存,非仅依赖过期

缓存架构 ​

二级缓存流程 ​

请求 → Caffeine(一级/本地缓存) → Redis(二级/分布式缓存) → DB
  • Caffeine:本地缓存,JVM 内存级,访问速度最快,但服务间不共享
  • Redis:分布式缓存,多服务共享,网络开销但数据一致性好
  • DB:MySQL 持久化存储

缓存层级选择 ​

层级适用场景特点
Caffeine高频读取、低频变更、数据量小本地访问、速度快、服务内有效
Redis多服务共享、数据量较大网络开销、服务间共享、持久化
DB数据持久化、强一致性磁盘存储、最慢、最终一致

Redis Key 规范 ​

Key 格式 ​

{业务前缀}:{模块}:{标识}
组成部分说明示例
业务前缀区分业务域user、dept、menu、dict
模块具体功能info、list、tree
标识唯一标识(ID/编码)1001、admin

Key 示例 ​

text
✅ user:info:1001              # 用户信息,ID=1001
✅ dept:tree:100               # 部门树,根部门 ID=100
✅ menu:user:admin             # 用户菜单,账号=admin
✅ dict:zzlx                   # 字典-组织类型
❌ user_1001                    # 缺少业务前缀分隔
❌ 1001                         # 无业务语义
❌ user_info_admin_extra_long_key_name  # 过长

Key 常量管理 ​

缓存 Key 前缀统一定义在常量类中:

java
public class RedisAdminConstants {
    /** 用户信息缓存前缀 */
    public static final String USER_INFO_PREFIX = "user:info:";

    /** 部门树缓存前缀 */
    public static final String DEPT_TREE_PREFIX = "dept:tree:";

    /** 用户菜单缓存前缀 */
    public static final String USER_MENU_PREFIX = "menu:user:";

    /** 字典缓存前缀 */
    public static final String DICT_PREFIX = "dict:";
}

Redis 客户端使用 ​

RedisAdminClient ​

通过 RedisAdminClient 封装 Redis 操作:

java
@Component
public class RedisAdminClient {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 设置缓存
     */
    public void set(String key, Object value, long timeout, TimeUnit unit) {
        redisTemplate.opsForValue().set(key, value, timeout, unit);
    }

    /**
     * 获取缓存
     */
    public Object get(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    /**
     * 删除缓存
     */
    public void delete(String key) {
        redisTemplate.delete(key);
    }

    /**
     * 设置过期时间
     */
    public void expire(String key, long timeout, TimeUnit unit) {
        redisTemplate.expire(key, timeout, unit);
    }
}

调用示例 ​

java
@Service
public class SysUserServiceImpl implements SysUserService {

    @Resource
    private RedisAdminClient redisAdminClient;

    /**
     * 获取用户信息(先查缓存,再查 DB)
     */
    public Map<String, Object> getUserInfoById(String userId) {
        String key = RedisAdminConstants.USER_INFO_PREFIX + userId;

        // 1. 查缓存
        Object cached = redisAdminClient.get(key);
        if (cached != null) {
            return (Map<String, Object>) cached;
        }

        // 2. 查 DB
        Map<String, Object> userInfo = sysUserMapper.selectUserInfoById(userId);

        // 3. 写入缓存(过期时间 30 分钟)
        if (userInfo != null) {
            redisAdminClient.set(key, userInfo, 30, TimeUnit.MINUTES);
        }

        return userInfo;
    }

    /**
     * 更新用户信息(更新 DB + 清除缓存)
     */
    public Map<String, Object> updateUser(Long id, Map<String, Object> param) {
        // 1. 更新 DB
        sysUserMapper.updateById(buildUserPO(param));

        // 2. 清除缓存
        String key = RedisAdminConstants.USER_INFO_PREFIX + id;
        redisAdminClient.delete(key);

        return RestResult.success();
    }
}

过期时间规范 ​

数据类型建议过期时间说明
用户信息30 分钟变更频率低,可适当延长
字典数据60 分钟几乎不变,可长过期
菜单/权限30 分钟变更需及时生效
部门树30 分钟组织架构变动少
验证码5 分钟短时效,安全要求
Token按业务根据登录态有效期

缓存同步 ​

服务间缓存同步 ​

通过 Feign 调用 guava-redis 服务同步缓存:

java
@FeignClient(name = "guava-redis", path = "/user")
public interface RedisUserFeignService {

    @PostMapping("/updateUser")
    void updateUser(@RequestBody SysUserPO sysUserPO);

    @PostMapping("/deleteUser")
    void deleteUser(@RequestParam Long userId);
}

数据变更时的缓存处理 ​

操作缓存处理
新增无需处理(首次查询时写入缓存)
更新删除缓存(下次查询时重新加载)
删除删除缓存
java
@Service
public class SysUserServiceImpl implements SysUserService {

    @Resource
    private RedisUserFeignService redisUserFeignService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Map<String, Object> updateUser(Long id, Map<String, Object> param) {
        // 1. 更新 DB
        sysUserMapper.updateById(buildUserPO(param));

        // 2. 通过 Feign 通知 Redis 服务清除/更新缓存
        SysUserPO updatedUser = sysUserMapper.selectById(id);
        redisUserFeignService.updateUser(updatedUser);

        return RestResult.success();
    }
}

缓存穿透/击穿/雪崩 ​

缓存穿透(查询不存在的数据) ​

问题:请求的数据在 DB 中也不存在,每次都会打到 DB。

解决方案:对空结果也缓存较短时间。

java
public Map<String, Object> getUserInfoById(String userId) {
    String key = RedisAdminConstants.USER_INFO_PREFIX + userId;

    // 查缓存
    Object cached = redisAdminClient.get(key);
    if (cached != null) {
        // 空值标记
        if ("NULL".equals(cached)) {
            return null;
        }
        return (Map<String, Object>) cached;
    }

    // 查 DB
    Map<String, Object> userInfo = sysUserMapper.selectUserInfoById(userId);

    if (userInfo == null) {
        // 空值缓存 5 分钟,避免穿透
        redisAdminClient.set(key, "NULL", 5, TimeUnit.MINUTES);
    } else {
        redisAdminClient.set(key, userInfo, 30, TimeUnit.MINUTES);
    }

    return userInfo;
}

缓存击穿(热点 Key 过期) ​

问题:某个热点 Key 过期,大量请求同时打到 DB。

解决方案:加互斥锁,只允许一个线程回源。

java
public Map<String, Object> getUserInfoById(String userId) {
    String key = RedisAdminConstants.USER_INFO_PREFIX + userId;

    Object cached = redisAdminClient.get(key);
    if (cached != null) {
        return (Map<String, Object>) cached;
    }

    // 加锁回源
    String lockKey = "lock:" + key;
    boolean locked = tryLock(lockKey);
    try {
        if (locked) {
            // 二次检查
            cached = redisAdminClient.get(key);
            if (cached != null) {
                return (Map<String, Object>) cached;
            }
            Map<String, Object> userInfo = sysUserMapper.selectUserInfoById(userId);
            redisAdminClient.set(key, userInfo, 30, TimeUnit.MINUTES);
            return userInfo;
        }
    } finally {
        if (locked) {
            unlock(lockKey);
        }
    }

    return null;
}

缓存雪崩(大量 Key 同时过期) ​

问题:大量缓存 Key 在同一时间过期,请求全部打到 DB。

解决方案:过期时间加随机值。

java
// 基础过期时间 30 分钟 + 随机 0~10 分钟
long expireTime = 30 + ThreadLocalRandom.current().nextInt(10);
redisAdminClient.set(key, value, expireTime, TimeUnit.MINUTES);

Caffeine 本地缓存 ​

配置 ​

java
@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(1000)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .build();
    }
}

使用 ​

java
@Service
public class DictServiceImpl implements DictService {

    @Resource
    private Cache<String, Object> caffeineCache;

    /**
     * 获取字典数据(Caffeine → Redis → DB)
     */
    public List<Map<String, Object>> getDictItems(String dictType) {
        String key = RedisAdminConstants.DICT_PREFIX + dictType;

        // 1. 查 Caffeine
        Object localCached = caffeineCache.getIfPresent(key);
        if (localCached != null) {
            return (List<Map<String, Object>>) localCached;
        }

        // 2. 查 Redis
        Object redisCached = redisAdminClient.get(key);
        if (redisCached != null) {
            // 回填 Caffeine
            caffeineCache.put(key, redisCached);
            return (List<Map<String, Object>>) redisCached;
        }

        // 3. 查 DB
        List<Map<String, Object>> items = sysDictMapper.selectDictItems(dictType);

        // 4. 写入 Redis + Caffeine
        redisAdminClient.set(key, items, 60, TimeUnit.MINUTES);
        caffeineCache.put(key, items);

        return items;
    }
}

反例与正例 ​

Key 散落在业务代码 ​

java
// ❌ Key 硬编码在业务逻辑中
redisTemplate.opsForValue().set("user_" + userId, userInfo, 30, TimeUnit.MINUTES);

// ✅ Key 前缀统一定义在常量类中
redisTemplate.opsForValue().set(RedisAdminConstants.USER_INFO_PREFIX + userId, userInfo, 30, TimeUnit.MINUTES);

数据变更不清缓存 ​

java
// ❌ 更新 DB 后未清除缓存
public void updateUser(Long id, Map<String, Object> param) {
    sysUserMapper.updateById(buildUserPO(param));
    // 缓存中的数据仍是旧的
}

// ✅ 更新 DB 后清除缓存
public void updateUser(Long id, Map<String, Object> param) {
    sysUserMapper.updateById(buildUserPO(param));
    redisAdminClient.delete(RedisAdminConstants.USER_INFO_PREFIX + id);
}

过期时间过长 ​

java
// ❌ 字典数据缓存 24 小时,变更后无法及时生效
redisAdminClient.set(key, items, 24, TimeUnit.HOURS);

// ✅ 合理过期时间 + 变更时主动清除
redisAdminClient.set(key, items, 60, TimeUnit.MINUTES);

缓存 Checklist ​

  • Key 格式统一({业务}:{模块}:{标识})
  • Key 前缀定义在常量类中
  • 过期时间设置合理(非永久)
  • 数据变更时清除/更新缓存
  • 空值缓存处理(防穿透)
  • 过期时间加随机值(防雪崩)
  • 热点数据加锁回源(防击穿)

相关规范 ​