Redis实践
缓存key命名
缓存key的构建,统一放在一个类里面,key需要有前,后缀,前缀使用系统的标识,后缀使用版本号。
后缀的版本号建议在配置中心进行配置。
@Component
public class CacheKeyBuilder {
// 值形如 demo
@Value("${cache.demo.prefix}")
private String prefix;
// 值形如 v1, v2
@Value("${cache.demo.suffix}")
private String suffix;
public String buildUserInfoKey(String userId) {
return Joiner.on(":").join(prefix, userId, suffix);
}
}
优势:
1、缓存key统一管理,清晰明确
2、当有共用缓存时,不会发生冲突
3、当需要批量淘汰key时,只需要增长后缀版本号
数据需要设置过期时间
集合数据类型,里面的field无法直接设置过期时间
public void addCase() {
stringRedisTemplate.opsForValue().set("stringKey", "stringValue", 5, TimeUnit.MINUTES);
// 操作集合类,可以使用lua脚本同时赋值和设置过期时间
}
批量读/写
1、同时操作多个key时,建议使用multi get 或者 pipeline功能,但是要注意一次发送的命令的数量,如果数据巨大依然会造成阻塞,可以根据业务场景进行测试,找到合适的数量。
Note:在Redis cluster模式下,pipeline不被支持
// 批量获取
public ~<T>~ List~<T>~ multiGetCase(List~<String>~ keys, Class~<T>~ returnType) {
List<String> result = stringRedisTemplate.opsForValue().multiGet(keys);
if (CollectionUtils.isEmpty(result)) {
return new ArrayList<>(0);
}
List<T> data = new ArrayList<>(result.size());
for (String res : result) {
data.add(JSON.parseObject(res, returnType));
}
return data;
}
// pipeline 使用
public List~<Object>~ pipelineCase() {
RedisSerializer keySerializer = stringRedisTemplate.getKeySerializer();
List<Object> List = stringRedisTemplate.executePipelined(new RedisCallback<Long>() {
@Nullable
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
// 调用与否均可
// connection.openPipeline();
for (int i = 0; i < 1000; i++) {
String key = "123" + i;
connection.get(keySerializer.serialize(key));
}
//依靠代理的返回,此处应该固定返回null
return null;
}
});
return List;
}
2、针对大集合,有些操作需要谨慎使用,会耗时很长,阻塞Redis。
// 批量获取
public ~<T>~ List~<T>~ multiGetCase(List~<String>~ keys, Class~<T>~ returnType) {
List<String> result = stringRedisTemplate.opsForValue().multiGet(keys);
if (CollectionUtils.isEmpty(result)) {
return new ArrayList<>(0);
}
List<T> data = new ArrayList<>(result.size());
for (String res : result) {
data.add(JSON.parseObject(res, returnType));
}
return data;
}
// pipeline 使用
public List~<Object>~ pipelineCase() {
RedisSerializer keySerializer = stringRedisTemplate.getKeySerializer();
List<Object> List = stringRedisTemplate.executePipelined(new RedisCallback<Long>() {
@Nullable
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
// 调用与否均可
// connection.openPipeline();
for (int i = 0; i < 1000; i++) {
String key = "123" + i;
connection.get(keySerializer.serialize(key));
}
//依靠代理的返回,此处应该固定返回null
return null;
}
});
return List;
}
2、针对大集合,有些操作需要谨慎使用,会耗时很长,阻塞Redis。
删除操作
针对大object,使用unlink进行删除,避免阻塞.
private StringRedisTemplate stringRedisTemplate;
public void delBigObject(String cacheKey) {
stringRedisTemplate.unlink(cacheKey);
}
案例:
1、统计在线人数案例
背景:使用zset存储用户的心跳请求时间,来统计在线人数。
优化前:同步写入zset,并计算zcount,获取数量
优化后:
写操作:异步+时间窗口,每经过一个时间窗口,异步写入redis
读操作:有调度任务定期查询zset的长度,写入一个专门的key,业务读这个key,时间复杂度降为O(1)。
2、通用热key优化案例:
提供热点key的监控机制,发现热点key时,自动在缓存SDK内形成本地缓存。数据更新时,需要下发更新通知,清除本地缓存,待后续把key打热。
京东开源热key探测(JD-hotkey)中间件单机qps 2万提升至35万实录 - 武伟峰的个人空间 - OSCHINA - 中文开源技术交流社区
3、缓存和数据库操作的顺序,需要保证一致性,前提都需要设置过期时间
a、简单场景方案1:
如后台类操作,可以同时操作缓存和数据库
b、cache aside
先操作数据库,在删缓存。 只有在删缓存失败的时候,会导致读到旧数据,删除缓存失败概率较低。
c、方案2:异步双删
删除缓存,更新数据库,异步删除缓存(可以在加上失败重试)
d、方案3:读写分离
读操作就读缓存
写操作就操作数据库
监听binlog的变更,准实时淘汰key(避免上述方案2对业务代码的大量侵入)
e、方案3:读写分离2
读操作就读缓存
写操作就操作数据库
监听binlog的变更,准实时更新缓存,重要的场景下,在加上定期的任务去校对缓存和数据库(此时需要注意对同一条记录的操作的顺序)
4、使用RedisTemplate的注意事项
一些没有直接提供相关api的操作,不建议通过RedisTemplate直接获取connection进行一些操作(不手动释放链接会导致连接池耗尽),如果确实使用该方式,一定要使用RedisConnectionUtil对链接进行释放
RedisConnection redisConnection = stringRedisTemplate.getConnectionFactory().getConnection();
// do xxx
RedisConnectionUtils.releaseConnection(redisConnection, stringRedisTemplate.getConnectionFactory(), /按实际情况传值/false);
建议的方式是使用execute方法。