Skip to content

Commit 879b94c

Browse files
committed
fix(redis): use SCAN instead of KEYS for bucket deletion
- KEYS command blocks Redis and is disabled in production. - SCAN provides production-safe incremental scanning.
1 parent d6332ac commit 879b94c

File tree

4 files changed

+76
-8
lines changed

4 files changed

+76
-8
lines changed

fluxgate-redis-ratelimiter/src/main/java/org/fluxgate/redis/connection/ClusterRedisConnection.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package org.fluxgate.redis.connection;
22

3+
import io.lettuce.core.KeyScanCursor;
34
import io.lettuce.core.RedisURI;
5+
import io.lettuce.core.ScanArgs;
46
import io.lettuce.core.ScriptOutputType;
57
import io.lettuce.core.cluster.RedisClusterClient;
68
import io.lettuce.core.cluster.api.StatefulRedisClusterConnection;
@@ -179,11 +181,29 @@ public long ttl(String key) {
179181
}
180182

181183
@Override
184+
@Deprecated
182185
public List<String> keys(String pattern) {
183186
// In cluster mode, this scans all nodes
184187
return new ArrayList<>(commands.keys(pattern));
185188
}
186189

190+
@Override
191+
public List<String> scan(String pattern) {
192+
// In cluster mode, Lettuce's scan() automatically scans all master nodes
193+
List<String> result = new ArrayList<>();
194+
ScanArgs scanArgs = ScanArgs.Builder.matches(pattern).limit(100);
195+
196+
KeyScanCursor<String> cursor = commands.scan(scanArgs);
197+
result.addAll(cursor.getKeys());
198+
199+
while (!cursor.isFinished()) {
200+
cursor = commands.scan(cursor, scanArgs);
201+
result.addAll(cursor.getKeys());
202+
}
203+
204+
return result;
205+
}
206+
187207
@Override
188208
public String flushdb() {
189209
// In cluster mode, this flushes all nodes

fluxgate-redis-ratelimiter/src/main/java/org/fluxgate/redis/connection/RedisConnectionProvider.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,42 @@ public interface RedisConnectionProvider extends AutoCloseable {
127127
/**
128128
* Finds all keys matching the given pattern.
129129
*
130-
* <p>Warning: This operation can be slow on large databases. Use with caution in production.
130+
* <p><b>WARNING: DEPRECATED - Use {@link #scan(String)} instead.</b>
131+
*
132+
* <p>This operation uses the KEYS command which:
133+
*
134+
* <ul>
135+
* <li>Blocks Redis during execution (O(N) complexity)
136+
* <li>Is often disabled in production environments
137+
* <li>Can cause performance issues with large datasets
138+
* </ul>
131139
*
132140
* @param pattern the pattern to match (e.g., "fluxgate:*")
133141
* @return list of matching keys
142+
* @deprecated Use {@link #scan(String)} instead for production-safe key scanning
134143
*/
144+
@Deprecated
135145
java.util.List<String> keys(String pattern);
136146

147+
/**
148+
* Scans keys matching the given pattern using SCAN command.
149+
*
150+
* <p>This is the production-safe alternative to {@link #keys(String)}:
151+
*
152+
* <ul>
153+
* <li>Non-blocking - scans incrementally without blocking Redis
154+
* <li>Production-safe - not disabled in production environments
155+
* <li>Memory-efficient - processes keys in batches
156+
* </ul>
157+
*
158+
* <p>Note: SCAN may return duplicates in some cases (e.g., during rehashing). The caller should
159+
* handle deduplication if needed.
160+
*
161+
* @param pattern the pattern to match (e.g., "fluxgate:*")
162+
* @return list of matching keys (may contain duplicates)
163+
*/
164+
java.util.List<String> scan(String pattern);
165+
137166
/**
138167
* Flushes the current database (deletes all keys).
139168
*

fluxgate-redis-ratelimiter/src/main/java/org/fluxgate/redis/connection/StandaloneRedisConnection.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package org.fluxgate.redis.connection;
22

3+
import io.lettuce.core.KeyScanCursor;
34
import io.lettuce.core.RedisClient;
45
import io.lettuce.core.RedisURI;
6+
import io.lettuce.core.ScanArgs;
57
import io.lettuce.core.ScriptOutputType;
68
import io.lettuce.core.api.StatefulRedisConnection;
79
import io.lettuce.core.api.sync.RedisCommands;
810
import java.time.Duration;
11+
import java.util.ArrayList;
912
import java.util.Collections;
1013
import java.util.List;
1114
import java.util.Map;
@@ -154,10 +157,27 @@ public long ttl(String key) {
154157
}
155158

156159
@Override
160+
@Deprecated
157161
public List<String> keys(String pattern) {
158162
return commands.keys(pattern);
159163
}
160164

165+
@Override
166+
public List<String> scan(String pattern) {
167+
List<String> result = new ArrayList<>();
168+
ScanArgs scanArgs = ScanArgs.Builder.matches(pattern).limit(100);
169+
170+
KeyScanCursor<String> cursor = commands.scan(scanArgs);
171+
result.addAll(cursor.getKeys());
172+
173+
while (!cursor.isFinished()) {
174+
cursor = commands.scan(cursor, scanArgs);
175+
result.addAll(cursor.getKeys());
176+
}
177+
178+
return result;
179+
}
180+
161181
@Override
162182
public String flushdb() {
163183
return commands.flushdb();

fluxgate-redis-ratelimiter/src/main/java/org/fluxgate/redis/store/RedisTokenBucketStore.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,7 @@ public RedisConnectionProvider.RedisMode getMode() {
146146
* <p>This is used when rules are changed to reset rate limit state. The pattern matches keys
147147
* like: {@code fluxgate:{ruleSetId}:*}
148148
*
149-
* <p>Warning: Uses KEYS command which can be slow on large databases. Consider using SCAN in
150-
* high-traffic production environments.
149+
* <p>Uses SCAN command which is production-safe (non-blocking, incremental scanning).
151150
*
152151
* @param ruleSetId the rule set ID to match
153152
* @return the number of buckets deleted
@@ -156,9 +155,9 @@ public long deleteBucketsByRuleSetId(String ruleSetId) {
156155
Objects.requireNonNull(ruleSetId, "ruleSetId must not be null");
157156

158157
String pattern = "fluxgate:" + ruleSetId + ":*";
159-
log.debug("Deleting token buckets matching pattern: {}", pattern);
158+
log.debug("Scanning token buckets matching pattern: {}", pattern);
160159

161-
java.util.List<String> keys = connectionProvider.keys(pattern);
160+
java.util.List<String> keys = connectionProvider.scan(pattern);
162161
if (keys.isEmpty()) {
163162
log.debug("No token buckets found for ruleSetId: {}", ruleSetId);
164163
return 0;
@@ -175,15 +174,15 @@ public long deleteBucketsByRuleSetId(String ruleSetId) {
175174
* <p>This is used when a full reload is triggered to reset all rate limit state. The pattern
176175
* matches all FluxGate keys: {@code fluxgate:*}
177176
*
178-
* <p>Warning: Uses KEYS command which can be slow on large databases.
177+
* <p>Uses SCAN command which is production-safe (non-blocking, incremental scanning).
179178
*
180179
* @return the number of buckets deleted
181180
*/
182181
public long deleteAllBuckets() {
183182
String pattern = "fluxgate:*";
184-
log.debug("Deleting all token buckets matching pattern: {}", pattern);
183+
log.debug("Scanning all token buckets matching pattern: {}", pattern);
185184

186-
java.util.List<String> keys = connectionProvider.keys(pattern);
185+
java.util.List<String> keys = connectionProvider.scan(pattern);
187186
if (keys.isEmpty()) {
188187
log.debug("No token buckets found");
189188
return 0;

0 commit comments

Comments
 (0)