[051][缓存模块]基于 StringRedisTemplate 的多租户 Key 隔离设计与实践——以 RedisBitmapUtils 为例
在多租户(SaaS)系统中,不同租户的数据必须严格隔离,而 Redis 作为高性能缓存/存储中间件,其 Key 的命名空间管理是隔离的关键。Spring Data Redis 提供的 StringRedisTemplate 默认不对 Key 做任何修饰,若直接使用,不同租户的相同业务 Key(如 user:online)会互相覆盖。
本文介绍一种透明、低侵入的方案:通过自定义 StringRedisSerializer,在序列化 Key 时自动注入租户前缀,从而实现租户级别的自然隔离。并以项目中的 RedisBitmapUtils 工具类为例,展示如何在实际业务中安全使用这一机制。
一、核心设计思路
我们希望达到的效果:
- 业务代码中只需写入逻辑 Key(例如
"login:2025-06-09"); - 实际存入 Redis 的 Key 自动变为
{tenantId}:login:2025-06-09; - 读取时同样自动处理前缀,业务层无需感知租户 ID。
实现这一目标不需要修改 StringRedisTemplate 的 API,只需要替换其 Key 的序列化器。StringRedisTemplate 默认使用 StringRedisSerializer(UTF-8 编码),我们扩展该类,在 serialize(String key) 方法中动态拼接租户前缀。
二、关键组件解析
1. 租户前缀策略接口 RedisKeyPrefix
@FunctionalInterface
public interface RedisKeyPrefix {
String SEPARATOR = ":";
String compute(String cacheName);
static RedisKeyPrefix tenant() {
return name -> TenantContextHolder.get() + SEPARATOR + name;
}
// ... 其他方法
}
tenant()工厂方法返回一个函数:租户ID + ":" + 原始Key。TenantContextHolder.get()是线程本地变量,保存当前请求的租户 ID(通常由拦截器或过滤器注入)。
2. 自定义序列化器 TenantStringRedisSerializer
public class TenantStringRedisSerializer extends StringRedisSerializer {
@Override
public byte[] serialize(String value) throws SerializationException {
// value 是业务传入的原始 Key,先计算带前缀的完整 Key,再交给父类序列化
return super.serialize(RedisKeyPrefix.tenant().compute(value));
}
}
- 继承
StringRedisSerializer,复用其字符串 → byte[] 的能力。 - 重写
serialize方法,在序列化前将原始 Key 转换为租户ID:原始Key。 deserialize方法无需重写(因为 Redis 返回的字节数组已经是带前缀的完整 Key,直接按原样反序列化即可)。但若想从完整 Key 中剥离租户前缀,可单独提供工具方法。
3. 配置注入:替换默认序列化器
在 RedisCacheConfiguration 中:
@Bean
RedisTemplateDecorator redisTemplateDecorator(StringRedisTemplate stringRedisTemplate,
RedisTemplate<Object, Object> redisTemplate) {
TenantStringRedisSerializer serializer = new TenantStringRedisSerializer();
stringRedisTemplate.setKeySerializer(serializer);
stringRedisTemplate.setHashKeySerializer(serializer);
redisTemplate.setKeySerializer(serializer);
redisTemplate.setHashKeySerializer(serializer);
// ... 存入全局装饰器
return RedisTemplateDecorator.instance;
}
- 同时配置
StringRedisTemplate和普通RedisTemplate的 Key 序列化器。 RedisTemplateDecorator是一个单例持有者,便于全局获取已配置好的 Template 实例(见下文示例)。
三、实践示例:RedisBitmapUtils 如何利用多租户 Template
RedisBitmapUtils 是一个对 Redis Bitmap 操作的封装,其核心依赖于 RedisTemplateDecorator.stringRedisTemplate() —— 该静态方法返回的正是已经替换了序列化器的 StringRedisTemplate。
public class RedisBitmapUtils {
public static final RedisBitmapUtils instance = new RedisBitmapUtils();
public Boolean setBit(String key, String param, boolean value) {
// 使用的 stringRedisTemplate 已自动添加租户前缀
return RedisTemplateDecorator.stringRedisTemplate()
.opsForValue().setBit(key, hash(param), value);
}
public boolean getBit(String key, String param) {
return Boolean.TRUE.equals(RedisTemplateDecorator.stringRedisTemplate()
.opsForValue().getBit(key, hash(param)));
}
public Long bitCount(String key) {
// 注意:这里需要先序列化 key 以保持前缀一致
byte[] newKey = RedisTemplateDecorator.instance.serializeKey(key);
return RedisTemplateDecorator.stringRedisTemplate().execute(
(RedisCallback<Long>) connection -> connection.stringCommands().bitCount(newKey)
);
}
// ... 其他方法
}
关键观察点
setBit/getBit:直接传入逻辑 Key(如"user:active"),底层自动转换为{tenantId}:user:active存储。不同租户的相同逻辑 Key 在 Redis 中完全独立。bitCount等底层命令:使用了RedisTemplateDecorator.instance.serializeKey(key),该方法内部调用了TenantStringRedisSerializer.serialize(key),确保传递给 Redis 原生connection的字节数组同样包含租户前缀。- Hash 结构:如果工具类涉及 Hash 操作,也需要使用
setHashKeySerializer(serializer),以保证 Hash 中的 field 也带租户前缀(除非明确不需要)。
四、租户上下文的传递
上述方案的前提是 TenantContextHolder.get() 能够正确返回当前租户 ID。通常的实现方式:
- Web 应用:使用过滤器或拦截器,从请求头(如
X-Tenant-ID)、JWT 或子域名解析出租户 ID,存入ThreadLocal。 - 非 Web 环境(如定时任务、MQ 消费):在任务开始处手动设置租户 ID,执行完毕后清理。
- 注意:使用
ThreadLocal务必在请求结束或任务完成后调用clear(),避免内存泄漏或跨租户污染。
五、注意事项与最佳实践
Key 长度控制
租户前缀会增加 Key 的长度,建议租户 ID 使用数字或短字符串(如t1001),避免过长影响内存和网络。适用于所有 Redis 数据结构
该方案不仅对 Bitmap(String 结构)有效,对 Hash、List、Set、ZSet 等同样适用,只需设置对应的 Key 序列化器。对于 Value 序列化器,通常保持 JSON/Jackson 不变。兼容性考虑
如果已有旧数据不带租户前缀,需要做数据迁移或在代码中做兼容判断(例如先尝试带租户前缀的 Key,若不存在再查不带前缀的)。建议统一设计。缓存管理器(
RedisCacheManager)的联动
在使用 Spring Cache 注解(@Cacheable)时,也需要确保RedisCacheManager配置了相同的 Key 前缀策略。通常通过RedisCacheManagerBuilderCustomizer设置RedisCacheConfiguration#prefixCacheNameWith或自定义CacheKeyPrefix。性能影响
TenantStringRedisSerializer仅在序列化时做一次字符串拼接,开销极小。hash计算等正常业务操作不受影响。
六、总结
通过自定义 StringRedisSerializer,我们以一种对业务代码几乎无感知的方式实现了 Redis Key 的多租户隔离。RedisBitmapUtils 作为示例清晰地展示了:只要全局使用同一个预先配置好的 StringRedisTemplate(或通过 RedisTemplateDecorator 获取),所有 Key 都会被自动装饰租户前缀,开发者只需关注业务逻辑本身。
这种模式不仅适用于 Bitmap,也适用于任何基于 StringRedisTemplate 或 RedisTemplate 的操作,是构建多租户 Redis 层的一个轻量且高效的解决方案。
延伸思考:如果需要支持动态切换租户前缀策略(例如不同租户使用不同的前缀规则),可将
RedisKeyPrefix设计为可配置的 Bean,并在TenantStringRedisSerializer中注入该策略。本文的tenant()是硬编码实现,实际生产中可以更灵活。