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

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

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

    Hash集合的编码会由ziplist会转成dictRedisHash类型的内存空间占用优化相当于失败了,降级为相对消耗更多内存的字典类型编码,这个时候,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
    // 第一轮迭代返回结果

     ORDER_ID:ORDER_YYY
     YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY

    // 第二轮迭代 
    HSCAN USER_ID:2 2 COUNT 1

     ORDER_ID:ORDER_XXX
     XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

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

    // 下面的代码需要确保本地安装了Redis,并且引入Redis的客户端依赖:io.lettuce:lettuce-core:5.3.3.RELEASE
    public 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<String, String> 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<String, String> 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<String, String> 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的编码:ziplist
    10个F-V的HS进行HSCAN...
    10个F-V的HS进行HSCAN第1次遍历,size=10
    ......
    513个F-V的HS的编码:hashtable
    513个F-V的HS进行HSCAN...
    513个F-V的HS进行HSCAN第1次遍历,size=5
    ......
    513个F-V的HS进行HSCAN第92次遍历,size=6
    513个F-V的HS进行HSCAN第93次遍历,size=6
    513个F-V的HS进行HSCAN第94次遍历,size=5

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

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

    小结

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

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

    (本文完 r-a-2020812 c-2-d 封面来源于动漫《青春之旅》)

    本文分享自微信公众号 - Throwable文摘(throwable-doge)。
    如有侵权,请联系 support@oschina.cn 删除。
    本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

    09-03 13:19