特殊说明以下内容涉及到公司代码部分均已用demo替代
背景
为了节约阿里云硬件成本,组内打算将某应用的redis迁移到一个公共的redis中,从而下线该redis。
redis区别
| 原redis | 目标redis | |
|---|---|---|
| 版本 | Redis 4.0社区办 | Redis 4.0 社区版 |
| 部署方式 | 单机版 | 阿里云集群版(代理模式) |
代码逻辑图(商品查询 + 商品修改)
具体代码
public class TestDemo {
private Jedis jedis;
public List<Goods> search(GoodsSearch goodsSearch) {
TestDemo testDemo = new TestDemo();
//1.根据goodsSearch里面的每个字段获取md5加密
String md5Key = testDemo.md5Key("Goods_PREFIX", goodsSearch);
String cacheData = jedis.get(md5Key);
if (!StringUtils.isBlank(cacheData)) {
return JSON.parseArray(cacheData, Goods.class);
}
//2.从es搜索数据
List<Goods> result = new ArrayList<>();
//3.设置redis数据,key:md5加密,value:搜索结果值
jedis.set(md5Key, JSON.toJSONString(result));
jedis.expire(md5Key, 60 * 5);
//4.执行lua脚本建立md5跟搜索结果集的映射关系,方便后续商品变更及时取消md5对应的redis缓存
List<String> goodsNo = result.stream().map(Goods::getGoodsNo).collect(Collectors.toList());
this.createRelation(md5Key, goodsNo);
return result;
}
public void modifyGoods(String goodsNo) {
TestDemo testDemo = new TestDemo();
//1.修改商品属性
//2.删除该商品对应的查询条件md5结果
testDemo.deleteGoodsCache("CACHE_REFRESH:" + goodsNo + ":*");
}
private void createRelation(String searchRequestMd5Key, List<String> goodsNos) {
Jedis jedis = new Jedis();
String batchAddlua = "local value = ARGV[1];\n" +
"local exp = ARGV[2];\n" +
"local prefix = "CACHE_REFRESH:"\n" +
"for i=1, #(KEYS) do\n" +
"redis.call("sadd", prefix..KEYS[i], value)\n" +
"redis.call("expire", prefix..KEYS[i], exp)\n" +
"end";
Integer expireTime = 1000 * 60 * 5;
jedis.eval(batchAddlua, goodsNos, Arrays.asList(searchRequestMd5Key, expireTime.toString()));
}
private void deleteGoodsCache(String goodsNo) {
String redisKey = "CACHE_REFRESH:" + goodsNo;
Set<String> smembers = jedis.smembers(redisKey);
smembers.forEach(
key -> {
jedis.del(key);
}
);
}
public static final class GoodsSearch {
private String goodsNo;
private String picUrl;
}
public static final class Goods {
private String goodsNo;
private String picUrl;
private Integer price;
public String getGoodsNo() {
return goodsNo;
}
public void setGoodsNo(String goodsNo) {
this.goodsNo = goodsNo;
}
public String getPicUrl() {
return picUrl;
}
public void setPicUrl(String picUrl) {
this.picUrl = picUrl;
}
public Integer getPrice() {
return price;
}
public void setPrice(Integer price) {
this.price = price;
}
}
/**
* 获取搜搜条件所有字段的md5加密结果
*
* @param prefix
* @param objects
* @return
*/
public String md5Key(String prefix, Object... objects) {
StringBuilder sb = new StringBuilder();
Arrays.stream(objects).forEach(e -> {
sb.append(JSONObject.toJSONString(e));
});
return prefix + DigestUtils.md5DigestAsHex(sb.toString().getBytes());
}
}
代码中涉及到的lua脚本以及使用分布式redis调用lua脚本存在的问题
local value = ARGV[1];
local exp = ARGV[2];
local prefix = "CACHE_REFRESH:"
for i=1, #(KEYS) do
redis.call("sadd", prefix..KEYS[i], value)
redis.call("expire", prefix..KEYS[i], exp)
end
- redis.call方法中的key不能用变量,只能直接用KYES[i],
- lua脚本操作的key一定要在一个slot下,redis官方文档-slot
解决方案
1.技术层面解决
综上得知,因为查询结果集多个key注定无法放在一个redis-slot下面, 如果强制放在一个slot下会导致key分布不均匀, 查询性能无法提升,
如上使用lua脚本去批量设置过期时间,主要是为了解决redis多次网络交互的消耗。那么目前我能想到的就是用redis的pipelining(管道),管道简介
2.方案层面解决
我们可以问一下,自己用redis的目的是什么?是为了应付流量高峰期那几分钟或者几秒,而不是尽量用redis,而不走搜索。明白了这一点,我们来看看上面方案有什么缺点。
缺点
- 需要建立md5Key(请求参数)跟多个goods的映射关系,缓存的清除需要人为代码操作
- 设置过长的缓存过期时间,可能会导致意外的长时间的数据不一致
- 删除缓存需要代码逻辑进行,增加了代码的复杂度
解决方案如下:
- 设置比较短的过期时间,按照业务高峰期时间来定
- 告知上游,如果对数据一致性要求比较高,不要走缓存逻辑,走数据库接口
- 根据接口查询QPS,在es层面进行优化,目前理论上es能解决大部分普通场景的搜索