笔者最近在做一个项目时候使用Redis
存放客户端展示的订单列表,列表需要进行分页。由于笔者先前对Redis
的各种数据类型的使用场景并不是十分熟悉,于是先入为主地看到Hash
类型:
USER_ID:1
ORDER_ID:ORDER_XX: {"amount": "100","orderId":"ORDER_XX"}
ORDER_ID:ORDER_YY: {"amount": "200","orderId":"ORDER_YY"}
感觉Hash
类型完全满足需求实现的场景。然后想当然地考虑使用HSCAN
命令进行分页,引发了后面遇到的问题。
SCAN
命令如下:
SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
// 返回值如下:
// 1. cursor,数值类型,下一轮的起始游标值,0代表遍历结束
// 2. 遍历的结果集合,列表
SCAN
命令在Redis
2.8.0版本中新增,时间复杂度计算如下:每一轮遍历的时间复杂度为O(1)
,所有元素遍历完毕直到游标cursor
返回0的时间复杂度为O(N)
,其中N
为集合内元素的数量。SCAN
是针对整个Database
内的所有KEY进行渐进式的遍历,它不会阻塞Redis
,也就是使用SCAN
命令遍历KEY的性能会优于KEY *
命令。对于Hash
类型有一个衍生的命令HSCAN
专门用于遍历Hash
类型及其相关属性(Field
)的字段:
HSCAN key cursor [MATCH pattern] [COUNT count]
// 返回值如下:
// 1. cursor,数值类型,下一轮的起始游标值,0代表遍历结束
// 2. 遍历的结果集合,是一个映射
笔者当时没有详细查阅Redis
的官方文档,想当然地认为Hash
类型的分页简单如下(假设每页数据只有1条):
// 第一页
HSCAN USER_ID:1 0 COUNT 1 <= 这里认为返回的游标值为1
// 第二页
HSCAN USER_ID:1 1 COUNT 1 <= 这里认为返回的游标值为0,结束迭代
实际上,执行的结果如下:
HSCAN USER_ID:1 0 COUNT 1
// 结果
0
ORDER_ID:ORDER_XX
{"amount": "100","orderId":"ORDER_XX"}
ORDER_ID:ORDER_YY
{"amount": "200","orderId":"ORDER_YY"}
也就是在第一轮遍历的时候,KEY对应的所有Field-Value
已经全量返回。笔者尝试增加哈希集合KEY = USER_ID:1
里面的元素,但是数据量相对较大的时候,依然没有达到预期的分页效果;另一个方面,尝试修改命令中的COUNT
值,发现无论如何修改COUNT
值都不会对遍历的结果产生任何影响(也就是还是在第一轮迭代返回全部结果)。百思不得其解的情况下,只能仔细翻阅官方文档寻找解决方案。在SCAN
命令的COUNT
属性描述中找到了原因:
简单翻译理解一下:
SCAN
命令以及其衍生命令并不保证每一轮迭代返回的元素数量,但是可以使用COUNT
属性凭经验调整SCAN
命令的行为。COUNT
指定每次调用应该完成遍历的元素的数量,以便于遍历集合,本质只是一个提示值。
COUNT
默认值为10。Set
、Hash
、Sorted Set
或者Key
空间足够大可以使用一个哈希表表示并且不使用MATCH
属性的前提下,Redis
服务端会返回COUNT
或者比COUNT
大的遍历元素结果集合。Integer
值的Set
集合(也称为intsets
),或者ziplists
类型编码的Hash
或者Sorted Set
集合(说明这些集合里面的元素占用的空间足够小),那么SCAN
命令会返回集合中的所有元素,直接忽略COUNT
属性。注意第3点,这个就是在Hash
集合中使用HSCAN
命令COUNT
属性失效的根本原因。Redis
配置中有两个和Hash
类型ziplist
编码的相关配置值:
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
在如下两个条件之一满足的时候,Hash
集合的编码会由ziplist
会转成dict
:
Hash
集合中的数据项(即Field-Value
对)的数目超过512的时候。Hash
集合中插入的任意一个Field-Value
对中的Value
长度超过64。当Hash
集合的编码会由ziplist
会转成dict
,Redis
为Hash
类型的内存空间占用优化相当于失败了,降级为相对消耗更多内存的字典类型编码,这个时候,HSCAN
命令COUNT
属性才会起效。
简单验证一下上一节得出的结论,写入一个测试数据如下:
// 70个X
HSET USER_ID:2 ORDER_ID:ORDER_XXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
// 70个Y
HSET USER_ID:2 ORDER_ID:ORDER_YYY YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
接着开始测试一下HSCAN
命令:
// 查看编码
object encoding USER_ID:2
// 编码结果
hashtable
// 第一轮迭代
HSCAN USER_ID:2 0 COUNT 1
// 第一轮迭代返回结果
2
ORDER_ID:ORDER_YYY
YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
// 第二轮迭代
HSCAN USER_ID:2 2 COUNT 1
0
ORDER_ID:ORDER_XXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
测试案例中故意让两个值的长度为70,大于64,也就是让Hash
集合转变为dict(hashtable)
类型,使得COUNT
属性生效。但是,这种做法是放弃了Redis
为Hash
集合的内存优化。显然,HSCAN
命令天然不是为了做数据分页而设计的,而是为了渐进式的迭代(也就是如果需要迭代的集合很大,也不会阻塞Redis
服务)。所以笔者最后放弃了使用HSCAN
命令,寻找更适合做数据分页查询的其他Redis
命令。
通过这简单的踩坑案例,笔者得到一些经验:
Redis
提供的API十分丰富,后面应该还会遇到更多的踩坑经验。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有