本文共享自华为云社区《分页列表缓存,你真的会吗》,作者: 勇哥java实战共享 。

1 直接缓存分页列表成果

显而易见,这是最简略易懂的方法。

3种分页列表缓存方式,速收藏~~

咱们依照不同的分页条件来缓存分页成果 ,伪代码如下:

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列表,再缓存每个目标条目

缓存分页成果尽管好用,但缓存的颗粒度太大,确保数据一致性比较费事。

所以咱们的目标是更细粒度的操控缓存

3种分页列表缓存方式,速收藏~~

咱们查询出产品分页目标ID列表,然后为每一个产品目标创建缓存 , 经过产品ID和产品目标缓存聚合成列表返回给前端。

伪代码如下:

3种分页列表缓存方式,速收藏~~

中心流程:

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 等等。

下图是开源我国的搜索流程:

3种分页列表缓存方式,速收藏~~

精华在于:搜索的分页成果只包括事务目标 ID ,目标的详细资料需求从缓存 + MySQL 中获取。

3 缓存目标ID列表,同时缓存每个目标条目

笔者曾经重构过相似朋友圈的服务,进入班级页面 ,瀑布流的方法展示班级成员的一切动态。

3种分页列表缓存方式,速收藏~~

咱们运用推形式将每一条动态 ID 存储在 Redis ZSet 数据结构中 。Redis ZSet 是一种类型为有序调集的数据结构,它由多个有序的唯一的字符串元素组成,每个元素都相关着一个浮点数分值。

ZSet 运用的是 member -> score 结构 :

  • member : 被排序的标识,也是默许的第二排序维度( score 相同时,Redis 以 member 的字典序摆放)
  • score : 被排序的分值,存储类型是 double

3种分页列表缓存方式,速收藏~~

如上图所示:ZSet 存储动态 ID 列表 , member 的值是动态编号 , score 值是创建时刻

经过 ZSet 的 ZREVRANGE 指令就能够完成分页的效果。

ZREVRANGE 是 Redis 中用于有序调集(sorted set)的指令之一,它用于依照成员的分数从大到小返回有序调集中的指定范围的成员。

3种分页列表缓存方式,速收藏~~

为了达到分页的效果,传递如下的分页参数 :

3种分页列表缓存方式,速收藏~~

经过 ZREVRANGE 指令,咱们能够查询出动态 ID 列表。

查询出动态 ID 列表后,还需求缓存每个动态目标条目,动态目标包括了概况,谈论,点赞,保藏这些功用数据 ,咱们需求为这些数据提供单独做缓存装备。

3种分页列表缓存方式,速收藏~~

无论是查询缓存,还是从头写入缓存,为了提升系统功用,批量操作效率更高。

若**缓存目标结构简略,运用 mget 、hmget 指令;若结构复杂,能够考虑运用 pipleline,Lua 脚本形式 。**笔者挑选的批量计划是 Redis 的 pipleline 功用。

咱们再来模仿获取动态分页列表的流程:

  1. 运用 ZSet 的 ZREVRANGE 指令 ,传入分页参数,查询出动态 ID 列表 ;
  2. 传递动态 ID 列表参数,经过 Redis 的 pipleline 功用从缓存中批量获取动态的概况,谈论,点赞,保藏这些功用数据 ,拼装成列表 。

4 总结

本文介绍了完成分页列表缓存的三种方法:

  1. 直接缓存分页列表成果

  2. 查询目标ID列表,只缓存每个目标条目

  3. 缓存目标ID列表,同时缓存每个目标条目

这三种方法是一层一层递进的,要诀是:

细粒度的操控缓存批量加载目标

点击关注,第一时刻了解华为云新鲜技术~