前提

这是一篇Redis命令使用不当的踩坑经历分享

笔者最近在做一个项目时候使用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和HSCAN命令

SCAN命令如下:

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]// 返回值如下:// 1. cursor,数值类型,下一轮的起始游标值,0代表遍历结束// 2. 遍历的结果集合,列表

SCAN命令在Redis2.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属性描述中找到了原因:

r-h-p-1

简单翻译理解一下:

SCAN命令以及其衍生命令并不保证每一轮迭代返回的元素数量,但是可以使用COUNT属性凭经验调整SCAN命令的行为。COUNT指定每次调用应该完成遍历的元素的数量,以便于遍历集合,「本质只是一个提示值」(just a hint,hint意思为暗示)。

  1. COUNT默认值为10。
  2. 当遍历的目标Set、Hash、Sorted Set或者Key空间足够大可以使用一个哈希表表示并且不使用MATCH属性的前提下,Redis服务端会返回COUNT或者比COUNT大的遍历元素结果集合。
  3. 当遍历只包含Integer值的Set集合(也称为intsets),或者ziplists类型编码的Hash或者Sorted Set集合(说明这些集合里面的元素占用的空间足够小),那么SCAN命令会返回集合中的所有元素,直接忽略COUNT属性。

注意第3点,这个就是在Hash集合中使用HSCAN命令COUNT属性失效的根本原因。Redis配置中有两个和Hash类型ziplist编码的相关配置值:

hash-max-ziplist-entries 512hash-max-ziplist-value 64

在如下两个条件之一满足的时候,Hash集合的编码会由ziplist会转成dict(字典类型编码是哈希表,即hashtable):

  • 当Hash集合中的数据项(即Field-Value对)的「数目超过512」的时候。
  • 当Hash集合中插入的任意一个Field-Value对中的「Value长度超过64」的时候。

当Hash集合的编码会由ziplist会转成dict,Redis为Hash类型的内存空间占用优化相当于失败了,降级为相对消耗更多内存的字典类型编码,这个时候,HSCAN命令COUNT属性才会起效。

案例验证

查询Redis中Key的编码类型的命令为:object encoding $KEY

简单验证一下上一节得出的结论,写入一个测试数据如下:

// 70个XHSET USER_ID:2 ORDER_ID:ORDER_XXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX   // 70个YHSET 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 10  ORDER_ID:ORDER_XXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

测试案例中故意让两个值的长度为70,大于64,也就是让Hash集合转变为dict(hashtable)类型,使得COUNT属性生效。但是,这种做法是放弃了Redis为Hash集合的内存优化。此前验证的是hash-max-ziplist-value配置项的临界值,还可以编写一个例子验证hash-max-ziplist-entries的临界值:

// 下面的代码需要确保本地安装了Redis,并且引入Redis的客户端依赖:io.lettuce:lettuce-core:5.3.3.RELEASEpublic class HashScanCountSample {    static String KEY = "HS";    static int THRESHOLD = 513;    static int COUNT = 5;    public static void main(String[] args) throws Exception {        ScanArgs scanArgs = new ScanArgs().limit(COUNT);        RedisURI redisUri = RedisURI.create("127.0.0.1", 6379);        RedisClient redisClient = RedisClient.create(redisUri);        RedisCommands commands = redisClient.connect().sync();        commands.del(KEY);        int total = 10;        for (int i = 1; i <= total; i++) {            String fv = String.valueOf(i);            commands.hset(KEY, fv, fv);        }        ScanCursor scanCursor = ScanCursor.INITIAL;        int idx = 1;        processScan(total, scanArgs, commands, scanCursor, idx);        for (int i = 11; i <= THRESHOLD; i++) {            String fv = String.valueOf(i);            commands.hset(KEY, fv, fv);        }        scanCursor = ScanCursor.INITIAL;        total = THRESHOLD;        idx = 1;        processScan(total, scanArgs, commands, scanCursor, idx);    }    private static void processScan(int total, ScanArgs scanArgs, RedisCommands commands, ScanCursor scanCursor, int idx) {        System.out.println(String.format("%d个F-V的HS的编码:%s", total, commands.objectEncoding(KEY)));        System.out.println(String.format("%d个F-V的HS进行HSCAN...", total));        MapScanCursor result;        while (!(result = commands.hscan(KEY, scanCursor, scanArgs)).isFinished()) {            System.out.println(String.format("%d个F-V的HS进行HSCAN第%d次遍历,size=%d", total, idx, result.getMap().size()));            scanCursor = new ScanCursor(result.getCursor(), result.isFinished());            idx++;        }        System.out.println(String.format("%d个F-V的HS进行HSCAN第%d次遍历,size=%d", total, idx, result.getMap().size()));    }}// 某次输出结果10个F-V的HS的编码:ziplist10个F-V的HS进行HSCAN...10个F-V的HS进行HSCAN第1次遍历,size=10......513个F-V的HS的编码:hashtable513个F-V的HS进行HSCAN...513个F-V的HS进行HSCAN第1次遍历,size=5......513个F-V的HS进行HSCAN第92次遍历,size=6513个F-V的HS进行HSCAN第93次遍历,size=6513个F-V的HS进行HSCAN第94次遍历,size=5

这里看到,最终遍历513个F-V的Hash类型的KEY,最多每次能遍历出9个F-V对,这里只是其中一次的测试数据,也就是说COUNT值即使固定为一个常量,但是遍历出来的数据集合中的元素数量不一定为COUNT,但是大多数情况下为COUNT。

不过可以推断出一点,如果Hash中的F-V对的数量小于512,并且所有的V的长度都比较短,HSCAN命令会一次遍历出该KEY的所有的F-V对

显然,HSCAN命令天然不是为了做数据分页而设计的,而是为了渐进式的迭代(也就是如果需要迭代的集合很大,也不会一直阻塞Redis服务)。所以笔者最后放弃了使用HSCAN命令,寻找更适合做数据分页查询的其他Redis命令。

小结

通过这简单的踩坑案例,笔者得到一些经验:

  • 切忌先入为主,使用中间件的时候要结合实际的场景。
  • 使用工具的之前要仔细阅读工具的使用手册。
  • 要通过一些案例验证自己的猜想或者推导的结果。

HSCAN命令中的COUNT属性的功能和Redis服务的配置项hash-max-ziplist-value、hash-max-ziplist-entries以及KEY的编码类型息息相关。Redis提供的API十分丰富,这些API的版本兼容性做得十分优秀,后面应该还会遇到更多的踩坑经验。

如何修改Series和DataFrame类型中的元素值_Redis的HSCAN命令中COUNT参数的失效场景相关推荐

  1. 如何修改Series和DataFrame类型中的元素值_「ES6基础」Symbol介绍:独一无二的值

    ES6之前我们都清楚JS有六种数据类型:Undefined.Null.布尔值(Boolean).字符串(String).数值(Number).对象(Object),今天笔者讲的Symbol类型是ES6 ...

  2. key redis 遍历_解惑:Redis的HSCAN命令中COUNT参数的quot;失效quot;场景

    前提 ❝ 这是一篇Redis命令使用不当的踩坑经历分享 ❞ 笔者最近在做一个项目时候使用Redis存放客户端展示的订单列表,列表需要进行分页.由于笔者先前对Redis的各种数据类型的使用场景并不是十分 ...

  3. Pads/Powerpcb 将BOM中的元件值导入到PCB文件中

    这段代码是将文件文件"reflist.txt"中的元件值导入到pcb文件中的元件属性中,使用操作步骤: 1. 将代码复制到文本文件中,保存为loadBomValue2PCB.bas ...

  4. JAVA之删除数组中某个元素值

    1.删除数组中某个元素值.[1,4,6,9,13,16,19,28,40,100]  打开记事本,写如下一段代码: import java.util.*; public class DeleteEle ...

  5. Python列表(获取列表中指定元素的索引、获取列表中的多个元素、判断指定元素是否在列表中存在、列表元素的遍历、列表元素的增加操作、 列表元素的删除操作、列表元素的修改操作、列表元素的排序操作)

    1.获取列表中指定元素的索引 eg1:未指定索引范围查找索引 zyr=['憨憨','憨宝'] print(zyr.index('憨宝')) print(zyr[1]) eg2:在指定索引范围内查找元素 ...

  6. c++中判断某个值在字典的value中_Python核心知识系列:字典

    1 定义 字典是一种映射对象类型,由键值对构成的一个可变容器.字典中的每个键值对内用冒号 ( : ) 分隔,键值对间用逗号 ( , ) 分隔,所有键值对包括在{}中.字典的键必须是唯一的,只有可散列的 ...

  7. c++中判断某个值在字典的value中_Python元组、字典、集合的简单介绍

    元组 Python的元组与列表类似,不同之处在于元组的元素不能修改. 元组使用小括号,列表使用方括号.元组和字符串都是是不可变序列. 语法:定义元组语法 () 和 , 访问元组:和列表一样:a[1] ...

  8. Linux中ls -al(ls -l)命令中的各个参数的含义

    今天来详细说一下ls -l命令中的各个参数的含义,见下图是博主列出的ls -al命令: 下面来说一说各个参数的含义: 第一列是类型与权限,各个字母的含义如下: 第一个字符代表这个文件的类型(目录,文件 ...

  9. js判断数组中重复元素并找出_面试中常遇见的数组去重

    导读 JS数组去重是面试中并不少见的问题,听起来很简单,当你兴高采烈地回答了你的方法之后,你以为这道题就结束了吗?No,一般来说,面试官会继续问你"除了你刚刚说的方法,还有其他更好的方法吗? ...

最新文章

  1. 这就是爱?英特尔处理器将整合AMD HBM2 GPU
  2. python 线型_CCF 202006-1 线性分类器 python
  3. Ubuntu安装VMware Workstation详解
  4. php80端口改成8080,jetty,_JETTY 8080端口改为80端口无法访问,jetty - phpStudy
  5. 文件创建时间、访问时间、修改时间
  6. Ubuntu 20.04 安装 CUDA10.0和cudnn7.6
  7. 台大李宏毅Machine Learning 2017Fall学习笔记 (4)Gradient Descent
  8. python实现ssh登录后捕获回显_针对python的paramiko库ssh登录h3c设备print输出回显是空白的解决办法...
  9. 2021-06-29初识JQuery
  10. 软件工程课程实践-可行性研究报告
  11. JAVA实现Html转Pdf(wkhtmltopdf)
  12. qrc路径_c – 在Qt中获取qrc文件的路径
  13. 网络云盘项目——总体介绍、附源码链接
  14. java事务传播机制事例,spring事务传播机制
  15. mvvm框架 android,mvvm框架原理 android 怎么使用mvvm框架
  16. MySQL 查询各年龄段
  17. Unity2D愤怒的小鸟技术点总结 ---- 制作动画特效
  18. 微观经济学--第4章 供给与需求的市场力量
  19. 学生管理系统(完整版)
  20. 初学者如何阅读源码?

热门文章

  1. 归并排序相关题目(待编辑)
  2. linux deepin ubuntu apt安装openjdk-8-jdk
  3. ubuntu cpu频率控制
  4. 【网址收藏】win10 VirtualBox安装CentOS 7教程
  5. Linux echo输出文本改变颜色
  6. 【Java面试题视频讲解】字符串按指定长度分隔
  7. jQuery EasyUI布局容器layout实例精讲
  8. 人大金仓数据库sql语句_人大金仓数据库总结(SQL和JDBC)
  9. Matlab 矩阵计算例子
  10. 轻量级3d模型查看器_耐能取得两项软件著作权,自研轻量级3D人脸识别算法领先业界...