Java缓存键名结构如何统一

wen java案例 29

本文目录导读:

Java缓存键名结构如何统一

  1. 核心原则
  2. 推荐方案:业务域:实体名:操作:唯一标识[:属性]
  3. 如何用代码实现(Java + Spring Cache)
  4. 特殊场景处理

设计统一的 Java 缓存键名结构,核心目标是 可读性、可管理性、避免冲突、以及方便失效,没有绝对的“最佳”,但有公认的“最佳实践”。

下面为你提供一套层次分明、经过大量生产环境验证的统一键名结构方案。


核心原则

  1. 分层命名:像包名一样,从大到小排列,用分隔符分开。
  2. 可读性强:看到键名就能大致知道它缓存了什么。
  3. 唯一性:不同业务、不同参数组合的键不会碰撞。
  4. 支持通配符:方便批量失效(这是为什么结构比无规则拼接重要的原因)。
  5. 避免过长:在可读性和性能之间取得平衡(Redis 等键越短,查询效率越高,但可读性优先)。

推荐方案:业务域:实体名:操作:唯一标识[:属性]

这是一个经典的冒号分隔的树形结构。

格式: [系统]:[模块]:[业务域]:[实体名]:[操作]:[唯一标识]

详细解析

  • 系统 (可选):多系统共用缓存时区分。
  • 模块:粗粒度分组,user, order, product
  • 业务域:进一步细分,user 模块下的 account, profile, permission
  • 实体名:具体的数据对象,User, OrderItem, ProductSku
  • 操作:缓存是为了什么操作生成的,detail, list, count
  • 唯一标识:通常是数据库主键 ID,或复杂的组合键(用下划线或进一步冒号连接)。
  • 属性 (可选):需要缓存实体的某个具体字段时使用。

例子

业务场景 缓存键 解释
用户详情 user:account:User:detail:123 用户模块,账户域,User实体,详情,ID=123
用户订单列表 user:order:Order:list:456 用户模块,订单域,Order实体,列表,用户ID=456
商品SKU详情(带租户) product:goods:ProductSku:detail:SKU001 商品模块,商品域,SKU实体,详情,SKU编码
用户权限列表 user:permission:Permission:list:123 用户模块,权限域,Permission实体,列表,用户ID=123
过期时间刷新 user:session:Session:token:a1b2c3 用户模块,会话域,Session实体,token值
热点文章标签 content:article:ArticleTag:list:789 内容模块,文章域,ArticleTag实体,列表,文章ID=789
用户最近登录时间 user:account:User:attr:lastLoginTime:123 用户模块,账户域,User实体,属性(最近登录时间),用户ID=123

如何用代码实现(Java + Spring Cache)

使用 RedisCacheManager 时,可以自定义 CacheKeyPrefix 或通过 @Cacheablekey 属性生成。

定义常量 + 工具类(推荐)

public final class CacheKeyBuilder {
    private static final String SEPARATOR = ":";
    // 1. 基础方法
    public static String build(String... parts) {
        return String.join(SEPARATOR, parts);
    }
    // 2. 针对常见场景的封装方法
    public static String userDetail(Long userId) {
        return build("user", "account", "User", "detail", String.valueOf(userId));
    }
    public static String userOrderList(Long userId) {
        return build("user", "order", "Order", "list", String.valueOf(userId));
    }
    public static String productSkuDetail(String skuCode) {
        return build("product", "goods", "ProductSku", "detail", skuCode);
    }
    // 3. 针对组合键的场景
    public static String buildCompositeKey(Object... keys) {
        // 用下划线连接多个标识,作为唯一标识的一部分
        // user:profile:User:detail:123_456 (如果ID是组合主键)
        return Arrays.stream(keys)
                .map(String::valueOf)
                .collect(Collectors.joining("_"));
    }
}

使用示例:

@Service
public class UserServiceImpl implements UserService {
    @Override
    @Cacheable(value = "user_detail_cache", key = "#userId")
    public User getUserById(Long userId) {
        // ... 查询数据库
    }
    // 在Service中使用KeyBuilder手动生成key,更灵活
    @Override
    @Cacheable(value = "user_detail_cache", 
               key = "T(com.example.utils.CacheKeyBuilder).userDetail(#userId)")
    public User getCachedUser(Long userId) {
        // ...
    }
}

使用 Spring Expression (SpEL) 在注解中直接构造

适用于简单场景,但复杂时影响可读性。

@Override
@Cacheable(value = "user_detail_cache", 
           key = "'user:account:User:detail:' + #userId")
public User getUserById(Long userId) {
    // ...
}
// 更复杂的组合
@Override
@Cacheable(value = "order_detail_cache", 
           key = "'order:order:Order:detail:' + #orderId")
public Order getOrderById(String orderId) {
    // ...
}

特殊场景处理

带租户的数据 (SaaS)

格式: [系统]:[租户ID]:[模块]:[实体]:[操作]:[ID]

// tenantId = 1001
// user:1001:account:User:detail:123

带版本号的数据(用于缓存刷新或平滑迁移)

格式: v[版本号]:[业务域]:[实体]:[ID]

// v2:user:account:User:detail:123
// 当数据结构变更时,只需要修改版本号,旧缓存自动失效

分页/列表查询 (谨慎使用!)

列表查询的键通常包含查询参数,这会导致缓存键爆炸,不可控。

  • 不推荐: product:goods:Product:list:page=1&size=10&sort=price
  • 推荐方案:
    • 对列表查询使用本地内存缓存(Caffeine),并设置极短的 TTL(如 1 秒)
    • 如果非要缓存,使用 Bloom Filter 先判断,然后回源 DB
    • 如果非要缓存列表结果,将键设计为统计维度,而不是用户参数维度: product:goods:Product:list:hot_100 (缓存热门前100的商品,而非用户任意搜索参数)。

缓存键前缀(Namespace)

在 Redis 或 Caffeine 中,可以在配置时统一添加一个系统级别的前缀,防止不同应用或环境冲突。

格式: [应用名称]:[环境]:[自定义键]

// 配置类
@Bean
public StringRedisTemplate redisTemplate(RedisConnectionFactory factory) {
    StringRedisTemplate template = new StringRedisTemplate(factory);
    // 全局键前缀(可选)
    // 实际存储:myapp:prod:user:account:User:detail:123
    template.setKeySerializer(new StringRedisSerializer() {
        @Override
        public String serialize(String key) {
            return "myapp:prod:" + key;
        }
    });
    return template;
}

场景 推荐键结构
单实体详情 user:account:User:detail:{id}
列表(热点数据) product:goods:Product:list:hot_100
用户维度列表 user:order:Order:list:{userId} (注意缓存失效和穿透问题)
组合主键 order:item:OrderItem:detail:{orderId}_{itemId}
租户隔离 user:{tenantId}:account:User:detail:{id}
需要版本管理 v2:user:account:User:detail:{id}

最终建议:

  1. 全员对齐:整个团队或项目组统一使用一套规则,建立文档。
  2. 避免复杂:键的可读性 > 性能微优化,不建议使用 MD5 或 64位数字。
  3. 列表缓存慎用:列表查询(尤其是分页、排序、搜索)尽量不加缓存,或使用本地缓存 + 极短 TTL。
  4. 利用冒号: 在 Redis 中并不是特殊字符,但在 keys * 扫描时可以配合 pattern 实现批量模糊删除(生产环境谨慎使用 keys,可以考虑使用 SCANredis-cli--pattern)。

抱歉,评论功能暂时关闭!