本文共享自华为云社区《分页列表缓存,你真的会吗》,作者: 勇哥java实战共享 。
1 直接缓存分页列表成果
显而易见,这是最简略易懂的方法。
咱们依照不同的分页条件来缓存分页成果 ,伪代码如下:
public List<Product> getPageList(String param,int page,int size) {
String key = "productList:page:" + page + ”size:“ + size +
"param:" + param ;
List<Product> dataList = cacheUtils.get(key);
if(dataList != null) {
return dataList;
}
dataList = queryFromDataBase(param,page,size);
if(dataList != null) {
cacheUtils.set(key , dataList , Constants.ExpireTime);
}
}
这种计划的长处是工程简略,功用也快,但是有一个非常明显的缺陷基因:列表缓存的颗粒度非常大。
假设列表中数据产生增删,为了确保数据的一致性,需求修正分页列表缓存。
有两种方法 :
1、依托缓存过期来惰性的完成 ,但事务场景有必要包容;
2、运用 Redis 的 keys 找到该事务的分页缓存,执行删去指令。 但 keys 指令对功用影响很大,会导致 Redis 很大的推迟 。
生产环境运用 keys 指令比较风险,产生事端的几率高,非常不引荐运用。
2 查询目标ID列表,再缓存每个目标条目
缓存分页成果尽管好用,但缓存的颗粒度太大,确保数据一致性比较费事。
所以咱们的目标是更细粒度的操控缓存 。
咱们查询出产品分页目标ID列表,然后为每一个产品目标创建缓存 , 经过产品ID和产品目标缓存聚合成列表返回给前端。
伪代码如下:
中心流程:
1、从数据库中查询分页 ID 列表
// 从数据库中查询分页产品 ID 列表
List<Long> productIdList = queryProductIdListFromDabaBase(
param,
page,
size);
对应的 SQL 相似:
SELECT id FROM products
ORDER BY id
LIMIT (page - 1) * size , size
2、批量从缓存中获取产品目标
Map<Long, Product> cachedProductMap = cacheUtils.mget(productIdList);
假设咱们运用本地缓存,直接一条一条从本地缓存中聚合也极快。
假设咱们运用分布式缓存,Redis 天然支撑批量查询的指令 ,比如 mget ,hmget 。
3、拼装没有射中的产品ID
List<Long> noHitIdList = new ArrayList<>(cachedProductMap.size());
for (Long productId : productIdList) {
if (!cachedProductMap.containsKey(productId)) {
noHitIdList.add(productId);
}
}
由于缓存中可能由于过期或许其他原因导致缓存没有射中的状况,所以咱们需求找到哪些产品没有在缓存里。
4、批量从数据库查询未射中的产品信息列表,从头加载到缓存
首先从数据库里批量查询出未射中的产品信息列表 ,请注意是批量。
List<Product> noHitProductList = batchQuery(noHitIdList);
参数是未射中缓存的产品ID列表,拼装成对应的 SQL,这样功用更快 :
SELECT * FROM products WHERE id IN
(1,
2,
3,
4);
然后这些未射中的产品信息存储到缓存里 , 运用 Redis 的 mset 指令。
//将没有射中的产品加入到缓存里
Map<Long, Product> noHitProductMap =
noHitProductList.stream()
.collect(
Collectors.toMap(Product::getId, Function.identity())
);
cacheUtils.mset(noHitProductMap);
//将没有射中的产品加入到聚合map里
cachedProductMap.putAll(noHitProductMap);
5、 遍历产品ID列表,拼装目标列表
for (Long productId : productIdList) {
Product product = cachedProductMap.get(productId);
if (product != null) {
result.add(product);
}
}
当前计划里,缓存都有射中的状况下,经过两次网络 IO ,第一次数据库查询 IO ,第二次 Redis 查询 IO , 功用都会比较好。
一切的操作都是批量操作,就算有缓存没有射中的状况,全体速度也较快。
”查询目标ID列表,再缓存每个目标条目“ 这个计划比较灵敏,当咱们查询目标ID列表,能够不限于数据库,还能够是搜索引擎,Redis 等等。
下图是开源我国的搜索流程:
精华在于:搜索的分页成果只包括事务目标 ID ,目标的详细资料需求从缓存 + MySQL 中获取。
3 缓存目标ID列表,同时缓存每个目标条目
笔者曾经重构过相似朋友圈的服务,进入班级页面 ,瀑布流的方法展示班级成员的一切动态。
咱们运用推形式将每一条动态 ID 存储在 Redis ZSet 数据结构中 。Redis ZSet 是一种类型为有序调集的数据结构,它由多个有序的唯一的字符串元素组成,每个元素都相关着一个浮点数分值。
ZSet 运用的是 member -> score 结构 :
- member : 被排序的标识,也是默许的第二排序维度( score 相同时,Redis 以 member 的字典序摆放)
- score : 被排序的分值,存储类型是 double
如上图所示:ZSet 存储动态 ID 列表 , member 的值是动态编号 , score 值是创建时刻。
经过 ZSet 的 ZREVRANGE 指令就能够完成分页的效果。
ZREVRANGE 是 Redis 中用于有序调集(sorted set)的指令之一,它用于依照成员的分数从大到小返回有序调集中的指定范围的成员。
为了达到分页的效果,传递如下的分页参数 :
经过 ZREVRANGE 指令,咱们能够查询出动态 ID 列表。
查询出动态 ID 列表后,还需求缓存每个动态目标条目,动态目标包括了概况,谈论,点赞,保藏这些功用数据 ,咱们需求为这些数据提供单独做缓存装备。
无论是查询缓存,还是从头写入缓存,为了提升系统功用,批量操作效率更高。
若**缓存目标结构简略,运用 mget 、hmget 指令;若结构复杂,能够考虑运用 pipleline,Lua 脚本形式 。**笔者挑选的批量计划是 Redis 的 pipleline 功用。
咱们再来模仿获取动态分页列表的流程:
- 运用 ZSet 的 ZREVRANGE 指令 ,传入分页参数,查询出动态 ID 列表 ;
- 传递动态 ID 列表参数,经过 Redis 的 pipleline 功用从缓存中批量获取动态的概况,谈论,点赞,保藏这些功用数据 ,拼装成列表 。
4 总结
本文介绍了完成分页列表缓存的三种方法:
-
直接缓存分页列表成果
-
查询目标ID列表,只缓存每个目标条目
-
缓存目标ID列表,同时缓存每个目标条目
这三种方法是一层一层递进的,要诀是:
细粒度的操控缓存和批量加载目标。
点击关注,第一时刻了解华为云新鲜技术~