分页列表缓存,你真的会吗

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 开源中国的红薯哥写了很多关于缓存的文章,其中多级缓存思路,分页列表缓存这些知识点给了我很大的启发性。写这篇文章,我们聊聊分页列表缓存,希望能帮助大家提升缓存技术认知。

开源中国的红薯哥写了很多关于缓存的文章,其中多级缓存思路,分页列表缓存这些知识点给了我很大的启发性。

写这篇文章,我们聊聊分页列表缓存,希望能帮助大家提升缓存技术认知。

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 功能。

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

  1. 使用 ZSet 的 ZREVRANGE 命令 ,传入分页参数,查询出动态 ID 列表 ;
  2. 传递动态 ID 列表参数,通过 Redis 的 pipleline 功能从缓存中批量获取动态的详情,评论,点赞,收藏这些功能数据 ,组装成列表 。

4 总结

本文介绍了实现分页列表缓存的三种方式:

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

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

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

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

细粒度的控制缓存批量加载对象


十年的编程时光里,我也积累了很多的电子书籍,有些书写得非常经典,分享给各位。

这些电子书 我已经打包好了 ,下载地址是:电子书下载

如果您觉得我的回答对您有帮助,麻烦点个赞 ,您的鼓励对我很重要。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
3月前
|
SQL 缓存 关系型数据库
MySQL技能完整学习列表6、查询优化——3、查询缓存——4、SQL优化技巧
MySQL技能完整学习列表6、查询优化——3、查询缓存——4、SQL优化技巧
65 0
|
6月前
|
存储 缓存 Linux
系统内存管理:虚拟内存、内存分段与分页、页表缓存TLB以及Linux内存管理
虚拟内存的主要作用是提供更大的地址空间,使得每个进程都可以拥有大量的虚拟内存,而不受物理内存大小的限制。此外,虚拟内存还可以提供内存保护和共享的机制,保护每个进程的内存空间不被其他进程非法访问,并允许多个进程共享同一份物理内存数据,提高了系统的资源利用率。虚拟内存的实现方式有分段和分页两种,其中分页机制更为常用和灵活。分页机制将虚拟内存划分为固定大小的页,将每个进程的虚拟地址空间映射到物理内存的页框中。为了减少页表的大小和访问时间,采用了多级页表的方式,将大的页表划分为多个小的页表,只加载需要的页表项,节约了内存空间。
205 0
系统内存管理:虚拟内存、内存分段与分页、页表缓存TLB以及Linux内存管理
|
缓存
jira学习案例90-用useQuery缓存列表
jira学习案例90-用useQuery缓存列表
76 0
jira学习案例90-用useQuery缓存列表
|
缓存 算法
总结两种常见的长列表分页缓存策略
通常,对于长列表加载的场景,都需要进行分页, 如最近的世界杯体育垂站项目中的赛程页,评论流,直播流。而为了提高分页加载的性能,往往需要对分页进行缓存。 下面总结对两种常见的分页缓存的策略, 适用场景以及各自的优缺点。     策略一: 直接对分页结果缓存 顾名思义,就是直接缓存每次分页查询的结果。  
7840 0
|
缓存 开发框架 .NET
实体列表缓存(最土的办法实现百万级性能)
在实际项目开发中,经常遇到有一些表数据很少(1000行以内),不会频繁修改(平均每行几个小时才会修改一次),例如配置表、分类表等。 这样的表,往往可以接受三五秒甚至更长的延迟,正是最适合使用缓存的地方。 实体缓存:一次性加载全表数据进入内存,供上层多维度查询!
210 0
|
8天前
|
缓存 NoSQL 安全
【Redis系列笔记】缓存三剑客
缓存穿透是指请求一个不存在的数据,缓存层和数据库层都没有这个数据,这种请求会穿透缓存直接到数据库进行查询。它通常发生在一些恶意用户可能故意发起不存在的请求,试图让系统陷入这种情况,以耗尽数据库连接资源或者造成性能问题。 缓存击穿发生在访问热点数据,大量请求访问同一个热点数据,当热点数据失效后同时去请求数据库,瞬间耗尽数据库资源,导致数据库无法使用。 缓存雪崩是缓存中大量key失效后当高并发到来时导致大量请求到数据库,瞬间耗尽数据库资源,导致数据库无法使用。
33 1
|
2天前
|
存储 缓存 NoSQL
【Go语言专栏】Go语言中的Redis操作与缓存应用
【4月更文挑战第30天】本文探讨了在Go语言中使用Redis进行操作和缓存应用的方法。文章介绍了Redis作为高性能键值存储系统,用于提升应用性能。推荐使用`go-redis/redis`库,示例代码展示了连接、设置、获取和删除键值对的基本操作。文章还详细阐述了缓存应用的步骤及常见缓存策略,包括缓存穿透、缓存击穿和缓存雪崩的解决方案。利用Redis和合适策略可有效优化应用性能。
|
5天前
|
存储 缓存 NoSQL
Redis多级缓存指南:从前端到后端全方位优化!
本文探讨了现代互联网应用中,多级缓存的重要性,特别是Redis在缓存中间件的角色。多级缓存能提升数据访问速度、系统稳定性和可扩展性,减少数据库压力,并允许灵活的缓存策略。浏览器本地内存缓存和磁盘缓存分别优化了短期数据和静态资源的存储,而服务端本地内存缓存和网络内存缓存(如Redis)则提供了高速访问和分布式系统的解决方案。服务器本地磁盘缓存因I/O性能瓶颈和复杂管理而不推荐用于缓存,强调了内存和网络缓存的优越性。
24 1
|
22小时前
|
缓存 NoSQL Java
优化Redis缓存:解决性能瓶颈和容量限制
优化Redis缓存:解决性能瓶颈和容量限制
4 0