From e9f35651dc647444b62fcef979002ffd3be8724b Mon Sep 17 00:00:00 2001 From: Binbin Date: Tue, 16 Jul 2024 11:13:40 +0800 Subject: [PATCH] Make KEYS to be an exact match if there is no pattern Although KEYS is a dangerous command and we recommend people to avoid using it, some people who are not familiar with it still using it, and even use KEYS with no pattern at all. Once KEYS is using with no pattern, we can convert it to an exact match to avoid iterating over all data. Signed-off-by: Binbin --- src/db.c | 35 ++++++++++++++++++++++++++++++++ tests/unit/keyspace.tcl | 45 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/src/db.c b/src/db.c index 5a6562a1e2..95f07c459f 100644 --- a/src/db.c +++ b/src/db.c @@ -792,6 +792,21 @@ void randomkeyCommand(client *c) { decrRefCount(key); } +/* Returns 1 if the pattern can be an exact match in KEYS context. */ +int patternExactMatch(const char *pattern, int length) { + for (int i = 0; i < length; i++) { + if (pattern[i] == '*' || pattern[i] == '?' || pattern[i] == '[') { + /* Wildcard or character class found. Keys can be in anywhere. */ + return 0; + } else if (pattern[i] == '\\') { + /* Escaped character. Computing the key name in this case is not + * implemented. We would need a temp buffer. */ + return 0; + } + } + return 1; +} + void keysCommand(client *c) { dictEntry *de; sds pattern = c->argv[1]->ptr; @@ -801,7 +816,27 @@ void keysCommand(client *c) { allkeys = (pattern[0] == '*' && plen == 1); if (server.cluster_enabled && !allkeys) { pslot = patternHashSlot(pattern, plen); + } else if (!server.cluster_enabled) { + pslot = 0; } + + /* Once the pattern can do an exact match, we can convert + * it to a kvstoreDictFind to avoid iterating over all data. */ + if (patternExactMatch(pattern, plen) && pslot != -1) { + de = kvstoreDictFind(c->db->keys, pslot, pattern); + if (de) { + robj keyobj; + sds key = dictGetKey(de); + initStaticStringObject(keyobj, key); + if (!keyIsExpired(c->db, &keyobj)) { + addReplyBulkCBuffer(c, key, sdslen(key)); + numkeys++; + } + } + setDeferredArrayLen(c, replylen, numkeys); + return; + } + kvstoreDictIterator *kvs_di = NULL; kvstoreIterator *kvs_it = NULL; if (pslot != -1) { diff --git a/tests/unit/keyspace.tcl b/tests/unit/keyspace.tcl index c8bdcac984..37b830e549 100644 --- a/tests/unit/keyspace.tcl +++ b/tests/unit/keyspace.tcl @@ -40,7 +40,20 @@ start_server {tags {"keyspace"}} { } assert_equal [lsort [r keys "{a}*"]] [list "{a}x" "{a}y" "{a}z"] assert_equal [lsort [r keys "*{b}*"]] [list "{b}a" "{b}b" "{b}c"] - } + } + + test {KEYS with no pattern} { + r debug populate 1000 + r set foo bar + r set foo{t} bar + r del non-exist + assert_equal [r keys foo] {foo} + assert_equal [r keys foo{t}] {foo{t}} + assert_equal [r keys non-exist] {} + + r set foo\\ bar + assert_equal [r keys foo\\] {foo\\} + } {} {needs:debug} test {DEL all keys} { foreach key [r keys *] {r del $key} @@ -548,3 +561,33 @@ foreach {type large} [array get largevalue] { r flushall } {OK} {singledb:skip} } + +start_cluster 1 0 {tags {"keyspace external:skip cluster"}} { + # SET keys for random slots, for random noise. + set num_keys 0 + while {$num_keys < 1000} { + set random_key [randomInt 163840] + r SET $random_key VALUE + incr num_keys 1 + } + + test {KEYS with hashtag in cluster mode} { + foreach key {"{a}x" "{a}y" "{a}z" "{b}a" "{b}b" "{b}c"} { + r set $key hello + } + assert_equal [lsort [r keys "{a}*"]] [list "{a}x" "{a}y" "{a}z"] + assert_equal [lsort [r keys "*{b}*"]] [list "{b}a" "{b}b" "{b}c"] + } + + test {KEYS with no pattern in cluster mode} { + r set foo bar + r set foo{t} bar + r del non-exist + assert_equal [r keys foo] {foo} + assert_equal [r keys foo{t}] {foo{t}} + assert_equal [r keys non-exist] {} + + r set foo\\ bar + assert_equal [r keys foo\\] {foo\\} + } +}