详解 Jedis 的 SCAN、SSCAN、HSCAN、ZSCAN 用法

JAVA herman 4399浏览
公告:“业余草”微信公众号提供免费CSDN下载服务(只下Java资源),关注业余草微信公众号,添加作者微信:xttblog,发送下载链接帮助你免费下载!
本博客日IP超过1800,PV 2600 左右,急需赞助商。
极客时间所有课程通过我的二维码购买后返现24元微信红包,请加博主新的微信号:xttblog,之前的微信号好友位已满,备注:返现
所有面试题(java、前端、数据库、springboot等)一网打尽,请关注文末小程序
视频教程免费领

网上百度和谷歌花了大量的时间去搜索 Jedis 的相关用法,要么不全,要么乱用。基本上没有完整的用例,于是我就写了这篇文章。

参考我前面的那篇文章《删除 Redis 大 Key 让程序出现雪崩导致程序员被开除!》,当我们使用 keys * 进行查询 key 的时候会进行堵塞,导致 redis 整体不可用,而使用 scan 命令则不会。

SCAN、SSCAN、HSCAN、ZSCAN 4 个命令,分别用于集合、哈希键及有序集等。

  • SCAN:命令用于迭代当前数据库中的数据库键。
  • SSCAN:命令用于迭代集合键中的元素。
  • HSCAN:命令用于迭代哈希键中的键值对。
  • ZSCAN:命令用于迭代有序集合中的元素(包括元素成员和元素分值)。

命令格式如下:

SCAN cursor [MATCH pattern] [COUNT count]

scan 游标 MATCH <返回和给定模式相匹配的元素> count 每次迭代所返回的元素数量 SCAN 命令是增量的循环,每次调用只会返回一小部分的元素。所以不会有 KEYS 命令的坑(key 的数量比较多,一次 KEYS 查询会 block 其他操作)。

在 Redis 中的具体用法如下:

scan 0 match xttblog.com* count 5 
sscan myset 0 match 业余草*

SCAN 命令对应的 Jedis 中的操作如下:

public static void delLargeListKey(Jedis jedis){
    // 游标初始值为0
    String cursor = ScanParams.SCAN_POINTER_START;
    String key = "test:xttblog:*";
    ScanParams scanParams = new ScanParams();
    scanParams.match(key);// 匹配以 test:xttblog:* 为前缀的 key
    scanParams.count(1000);
    while (true){
        //使用scan命令获取500条数据,使用cursor游标记录位置,下次循环使用
        ScanResult<String> scanResult = jedis.scan(cursor, scanParams);
        cursor = scanResult.getStringCursor();// 返回0 说明遍历完成
        List<String> list = scanResult.getResult();
        long t1 = System.currentTimeMillis();
        for(int m = 0;m < list.size();m++){
            String mapentry = list.get(m);
            //jedis.del(key, mapentry);
            jedis.ltrim("test:xttblog:", 0 ,1);
        }
        long t2 = System.currentTimeMillis();
        System.out.println("删除" + list.size()
            + "条数据,耗时: " + (t2-t1) + "毫秒,cursor:" + cursor);
        if ("0".equals(cursor)){
            break;
        }
    }
}

SSCAN 命令对应的 Jedis 中的操作如下:

public static void delLargeSetKey(Jedis jedis){
    // 游标初始值为0
    String cursor = ScanParams.SCAN_POINTER_START;
    ScanParams scanParams = new ScanParams();
    scanParams.count(1000);
    String key = "test:xttblog";
    while (true){
        //使用sscan命令获取500条数据,使用cursor游标记录位置,下次循环使用
        ScanResult<String> sscanResult = jedis.sscan(key, cursor, scanParams);
        cursor = sscanResult.getStringCursor();// 返回0 说明遍历完成
        List<String> scanResult = sscanResult.getResult();
        long t1 = System.currentTimeMillis();
        for(int m = 0;m < scanResult.size();m++){
            String mapentry = scanResult.get(m);
            jedis.srem(key, mapentry);
        }
        long t2 = System.currentTimeMillis();
        System.out.println("删除" + scanResult.size()
            + "条数据,耗时: " + (t2-t1) + "毫秒,cursor:" + cursor);
        if ("0".equals(cursor)){
            break;
        }
    }
}

HSCAN 命令对应的 Jedis 中的操作如下:

public static void delLargeHashKey(Jedis jedis){
    // 游标初始值为0
    String cursor = ScanParams.SCAN_POINTER_START;
    ScanParams scanParams = new ScanParams();
    scanParams.count(1000);
    String key = "test:xttblog";
    while (true){
        //使用hscan命令获取500条数据,使用cursor游标记录位置,下次循环使用
        ScanResult<Map.Entry<String, String>> hscanResult =
            jedis.hscan(key, cursor, scanParams);
        cursor = hscanResult.getStringCursor();// 返回0 说明遍历完成
        List<Map.Entry<String, String>> scanResult =
            hscanResult.getResult();
        long t1 = System.currentTimeMillis();
        for(int m = 0;m < scanResult.size();m++){
            Map.Entry<String, String> mapentry = scanResult.get(m);
            jedis.hdel(key, mapentry.getKey());
        }
        long t2 = System.currentTimeMillis();
        System.out.println("删除" + scanResult.size()
            + "条数据,耗时: " + (t2-t1) + "毫秒,cursor:" + cursor);
        if ("0".equals(cursor)){
            break;
        }
    }
}

ZSCAN 命令对应的 Jedis 中的操作如下:

public static void delLargeZSetKey(Jedis jedis){
    // 游标初始值为0
    String cursor = ScanParams.SCAN_POINTER_START;
    String key = "test:xttblog:*";
    ScanParams scanParams = new ScanParams();
    scanParams.match(key);// 匹配以 test:xttblog:* 为前缀的 key
    scanParams.count(1000);
    while (true){
        //使用 zscan 命令获取 500 条数据,使用cursor游标记录位置,下次循环使用
        ScanResult<Tuple> scanResult = jedis.zscan(key, cursor, scanParams);
        cursor = scanResult.getStringCursor();// 返回0 说明遍历完成
        List<Tuple> list = scanResult.getResult();
        long t1 = System.currentTimeMillis();
        for(int m = 0;m < list.size();m++){
            Tuple tuple = list.get(m);
            System.out.println("Element:" + tuple.getElement()
                + ",Score:" + tuple.getScore());
        }
        long t2 = System.currentTimeMillis();
        System.out.println("删除" + list.size()
            + "条数据,耗时: " + (t2-t1) + "毫秒,cursor:" + cursor);
        if ("0".equals(cursor)){
            break;
        }
    }
}

上面 4 个案例都没有对应具体的业务。基本上写的都很明白了。

SCAN 命令、 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都返回一个包含两个元素的 multi-bulk 回复。

回复的第一个元素是字符串表示的无符号 64 位整数(游标),SCAN 命令每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束。

回复的第二个元素是另一个 multi-bulk 回复, 这个 multi-bulk 回复包含了本次被迭代的元素。

注意:SCAN 命令不能保证每次返回的值都是有序的,另外同一个 key 有可能返回多次,不做区分,需要应用程序去处理。不要在使用 scan(int) 这个方法,它存在一个 bug,参数应该是 unsigned long 而不是 int,这个方法在以后 jedis 版本大改时会被删除。

业余草公众号

最后,欢迎关注我的个人微信公众号:业余草(yyucao)!可加QQ1群:135430763(2000人群已满),QQ2群:454796847(已满),QQ3群:187424846(已满)。QQ群进群密码:xttblog,想加微信群的朋友,之前的微信号好友已满,请加博主新的微信号:xttblog,备注:“xttblog”,添加博主微信拉你进群。备注错误不会同意好友申请。再次感谢您的关注!后续有精彩内容会第一时间发给您!原创文章投稿请发送至532009913@qq.com邮箱。商务合作可添加助理微信进行沟通!

本文原文出处:业余草: » 详解 Jedis 的 SCAN、SSCAN、HSCAN、ZSCAN 用法