Java Redis SCAN KEYS

使用SCAN代替KEYS

  1. /**
  2. * redis扩展工具
  3. *
  4. * @author Fcant
  5. * @since 2020/2/21 23:35
  6. */
  7. public abstract class RedisHelper {
  8. private static Logger logger = LoggerFactory.getLogger(RedisHelper.class);
  9. /**
  10. * scan 实现
  11. *
  12. * @param redisTemplate redisTemplate
  13. * @param pattern 表达式,如:abc*,找出所有以abc开始的键
  14. */
  15. public static Set<String> scan(RedisTemplate<String, Object> redisTemplate, String pattern) {
  16. return redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
  17. Set<String> keysTmp = new HashSet<>();
  18. try (Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder()
  19. .match(pattern)
  20. .count(10000).build())) {
  21. while (cursor.hasNext()) {
  22. keysTmp.add(new String(cursor.next(), "Utf-8"));
  23. }
  24. } catch (Exception e) {
  25. logger.error(e.getMessage(), e);
  26. throw new RuntimeException(e);
  27. }
  28. return keysTmp;
  29. });
  30. }
  31. }

源码分析

很多文章说这种实现方式cursor只会被执行一次,其实这是错误的,使用这种方式cursor会将所有的符合条件的key都返回回来,他只是将游标的移动给封装了起来而已,真正执行查询的语句起始在cursor.hasNext()里面,源码如下:

  1. /*
  2. * (non-Javadoc)
  3. * @see java.util.Iterator#hasNext()
  4. */
  5. @Override
  6. public boolean hasNext() {
  7. assertCursorIsOpen();
  8. // 存放结果集的容器没有值,并且游标状态是未完成的时候进行Scan动作
  9. while (!delegate.hasNext() && !CursorState.FINISHED.equals(state)) {
  10. scan(cursorId);
  11. }
  12. // 如果结果容器还有值直接返回true,进行循环
  13. if (delegate.hasNext()) {
  14. return true;
  15. }
  16. // 如果结果容器没有值,但是游标不为0则表示还有值,需要进行下一次循环
  17. if (cursorId > 0) {
  18. return true;
  19. }
  20. return false;
  21. }
  22. private void scan(long cursorId) {
  23. // 进行scan操作
  24. ScanIteration<T> result = doScan(cursorId, this.scanOptions);
  25. // 结果集处理
  26. processScanResult(result);
  27. }
  28. private void processScanResult(ScanIteration<T> result) {
  29. if (result == null) {
  30. // 重置结果集容器
  31. resetDelegate();
  32. // 设置游标状态为完成
  33. state = CursorState.FINISHED;
  34. return;
  35. }
  36. // 获取当前游标位置
  37. cursorId = Long.valueOf(result.getCursorId());
  38. if (isFinished(cursorId)) {
  39. // 游标返回0,设置游标状态为完成
  40. state = CursorState.FINISHED;
  41. }
  42. if (!CollectionUtils.isEmpty(result.getItems())) {
  43. // 将查询结果放到容器中
  44. delegate = result.iterator();
  45. } else {
  46. resetDelegate();
  47. }
  48. }

由上面源码可以看到游标的移动是在processScanResult()方法中完成。通过state来记录当前游标状态,大致过程为:

  1. 当存放结果集的容器没有值,并且游标状态是未完成的时候进行Scan动作
  2. 如果结果容器还有值直接返回true,进行循环
  3. 如果结果容器没有值,但是游标不为0则表示还有值,需要进行下一次循环