FIX: Don't use `Redis#keys` in production.

As per the documentation for KEYS

```
Warning: consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases. This command is intended for debugging and special operations, such as changing your keyspace layout.
```

Instead SCAN

```
Since these commands allow for incremental iteration, returning only a small number of elements per call, they can be used in production without the downside of commands like KEYS or SMEMBERS that may block the server for a long time (even several seconds) when called against big collections of keys or elements.
```
This commit is contained in:
Guo Xiang Tan 2018-12-15 08:53:52 +08:00 committed by Sam
parent 61dcd7c52c
commit 2cbb513c98
3 changed files with 39 additions and 1 deletions

View File

@ -22,7 +22,7 @@ class Cache < ActiveSupport::Cache::Store
end end
def keys(pattern = "*") def keys(pattern = "*")
redis.keys("#{@namespace}:#{pattern}") redis.scan_each(match: "#{@namespace}:#{pattern}").to_a
end end
def clear def clear

View File

@ -201,6 +201,31 @@ class DiscourseRedis
end end
end end
def scan_each(options = {}, &block)
DiscourseRedis.ignore_readonly do
match = options[:match].presence || '*'
options[:match] =
if @namespace
"#{namespace}:#{match}"
else
match
end
if block
@redis.scan_each(options) do |key|
key = remove_namespace(key) if @namespace
block.call(key)
end
else
@redis.scan_each(options).map do |key|
key = remove_namespace(key) if @namespace
key
end
end
end
end
def keys(pattern = nil) def keys(pattern = nil)
DiscourseRedis.ignore_readonly do DiscourseRedis.ignore_readonly do
pattern = pattern || '*' pattern = pattern || '*'
@ -253,4 +278,10 @@ class DiscourseRedis
Cache.new Cache.new
end end
private
def remove_namespace(key)
key[(namespace.length + 1)..-1]
end
end end

View File

@ -35,15 +35,22 @@ describe DiscourseRedis do
expect(redis.keys).to include('key') expect(redis.keys).to include('key')
expect(redis.keys).to_not include('key2') expect(redis.keys).to_not include('key2')
expect(redis.scan_each.to_a).to eq(['key'])
redis.scan_each.each do |key|
expect(key).to eq('key')
end
redis.del('key') redis.del('key')
expect(raw_redis.get('default:key')).to eq(nil) expect(raw_redis.get('default:key')).to eq(nil)
expect(redis.scan_each.to_a).to eq([])
raw_redis.set('default:key1', '1') raw_redis.set('default:key1', '1')
raw_redis.set('default:key2', '2') raw_redis.set('default:key2', '2')
expect(redis.mget('key1', 'key2')).to eq(['1', '2']) expect(redis.mget('key1', 'key2')).to eq(['1', '2'])
expect(redis.scan_each.to_a).to contain_exactly('key1', 'key2')
end end
end end