|
|
@@ -0,0 +1,121 @@
|
|
|
+package com.fs.course.config;
|
|
|
+
|
|
|
+import org.springframework.data.redis.connection.RedisConnection;
|
|
|
+import org.springframework.data.redis.core.Cursor;
|
|
|
+import org.springframework.data.redis.core.RedisCallback;
|
|
|
+import org.springframework.data.redis.core.ScanOptions;
|
|
|
+import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
+import org.springframework.data.redis.serializer.RedisSerializer;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.HashSet;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+@Component
|
|
|
+public class RedisKeyScanner {
|
|
|
+
|
|
|
+ private final StringRedisTemplate redisTemplate;
|
|
|
+
|
|
|
+ public RedisKeyScanner(StringRedisTemplate redisTemplate) {
|
|
|
+ this.redisTemplate = redisTemplate;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Set<String> scan(String pattern) {
|
|
|
+ Set<String> keys = new HashSet<>();
|
|
|
+
|
|
|
+ // 这里指定 match + count
|
|
|
+ ScanOptions options = ScanOptions.scanOptions()
|
|
|
+ .match(pattern)
|
|
|
+ .count(1000)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ // 使用 RedisConnection 提供的 scan
|
|
|
+ redisTemplate.execute((RedisConnection connection) -> {
|
|
|
+ try (Cursor<byte[]> cursor = connection.scan(options)) {
|
|
|
+ RedisSerializer<String> keySerializer = redisTemplate.getStringSerializer();
|
|
|
+ cursor.forEachRemaining(item -> keys.add(keySerializer.deserialize(item)));
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new RuntimeException(e);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ return keys;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Set<String> scanKeys(String pattern) {
|
|
|
+ Set<String> keys = new HashSet<>();
|
|
|
+ ScanOptions options = ScanOptions.scanOptions().match(pattern).count(1000).build();
|
|
|
+
|
|
|
+ redisTemplate.execute((RedisConnection connection) -> {
|
|
|
+ try (Cursor<byte[]> cursor = connection.scan(options)) {
|
|
|
+ while (cursor.hasNext()) {
|
|
|
+ byte[] rawKey = cursor.next();
|
|
|
+ String key = (String) redisTemplate.getKeySerializer().deserialize(rawKey);
|
|
|
+ keys.add(key);
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new RuntimeException("Error during scan", e);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ return keys;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用 SCAN 替代 KEYS,避免阻塞 Redis
|
|
|
+ *
|
|
|
+ * @param pattern key 匹配模式,例如 "h5user:watch:duration:*"
|
|
|
+ * @return 匹配到的 key 集合
|
|
|
+ */
|
|
|
+ public Collection<String> scanPatternKeys(final String pattern) {
|
|
|
+ Set<String> keys = new HashSet<>();
|
|
|
+
|
|
|
+ // 每次扫描的数量,可根据实际情况调整(越大速度越快,但对 CPU 影响越大)
|
|
|
+ ScanOptions options = ScanOptions.scanOptions()
|
|
|
+ .match(pattern)
|
|
|
+ .count(1000)
|
|
|
+ .build();
|
|
|
+
|
|
|
+ // 使用 redisTemplate 的 execute 保证连接获取 & 释放正确
|
|
|
+ redisTemplate.execute((RedisConnection connection) -> {
|
|
|
+ try (Cursor<byte[]> cursor = connection.scan(options)) {
|
|
|
+ RedisSerializer<String> keySerializer = redisTemplate.getStringSerializer();
|
|
|
+ while (cursor.hasNext()) {
|
|
|
+ keys.add(keySerializer.deserialize(cursor.next()));
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ throw new RuntimeException("Error during redis SCAN", e);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ });
|
|
|
+
|
|
|
+ return keys;
|
|
|
+ }
|
|
|
+
|
|
|
+ public Set<String> scanMatchKey(String pattern) {
|
|
|
+ return (Set<String>) redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
|
|
|
+ Set<String> keys = new HashSet<>();
|
|
|
+ // 使用 try-with-resources 确保 Cursor 无论是否异常都会被关闭
|
|
|
+ try (Cursor<byte[]> cursor = connection.scan(ScanOptions.scanOptions()
|
|
|
+ .match(pattern)
|
|
|
+ .count(1000) // 每次扫描的批量大小,可根据实际情况调整
|
|
|
+ .build())) {
|
|
|
+
|
|
|
+ while (cursor.hasNext()) {
|
|
|
+ keys.add(new String(cursor.next(), StandardCharsets.UTF_8)); // 显式指定字符集,避免依赖默认值
|
|
|
+ }
|
|
|
+ } catch (IOException e) {
|
|
|
+ // 在实际项目中,应该使用更合适的日志记录和异常处理方式
|
|
|
+ // 例如抛出自定义异常或记录错误日志
|
|
|
+ throw new RuntimeException("Error during SCAN operation", e);
|
|
|
+ }
|
|
|
+ return keys;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+}
|