摄影:产品经理
黄金蘑菇
我们知道,用 Redis 的 Hash 可以实现一对多的映射,就像是 Python 的字典一样,例如:
但如果我们需要实现多对多怎么办?我举一个例子,我们要用 Redis 实现一个英语词典。有10000个单词,每个单词对应1-3个中文意思。例如:
- resume: 重新开始;简历
- close: 靠近;关闭
- hello: 你好
- address: 地址;致辞
如果这些词和中文是已经对应好的,那么显然我们直接用 Hash 就可以了,如下图所示:
代码语言:javascript复制eng_dict = {'resume': '重新开始;简历', 'close': '靠近;关闭', 'hello': '你好', 'address': '地址;致辞'}
client.hmset('eng_dict', eng_dict)
但是现在问题来了,如果我们要实现实时添加怎么办?例如一开始,close
这个词只有一个意思:靠近
,现在我要添加另一个意思,你觉得是否可以这样写:
new_chinese = '关闭'
chinese = client.hget('eng_dict', 'close')
if chinese:
chinese = f'{chinese.decode()};{new_chinese}'
else:
chinese = new_chinese
client.hmset('eng_dict', {'close': chinese})
这样写,在你一个人操作的时候,确实没有问题。但是,假如有两个人同时要修改这个词的中文意思怎么办?close
这个词还有吝啬的
的意思。如果两个人要添加这个词的中文意思,并且两个人的代码几乎同时运行到chinese = client.hget('eng_dict', 'close')
这一行代码。此时,他们获取到的中文意思,都只有靠近
这一个。但是甲先更新了关闭
的意思,然后乙再更新了吝啬的
的意思。此时就会导致甲的修改被覆盖。
为了解决这个问题,使用锁是一个思路。但今天我们不用锁,而是使用另一个方案。
在使用 Redis 的字符串时,我们可以使用 append 命令,原子性地在字符串末尾追加新的字符串,如下图所示:
但是,Hash 没有这个命令。如果你翻看redis-py
这个库的官方文档,也许你会惊喜地发现,似乎使用Pipeline Watch[1]可以实现你的需求:
先别高兴地太早,你仔细看一下watch
命令监控的对象是什么。watch
监控的是一个key
,而不是 Hash 里面的field
。但同一时间,可能会有其他人修改其他field
。这就会导致 watch
总是失败。
在这种情况下,是时候使用 Redis 的内置 Lua 脚本了。你可以把一段 Lua 脚本发送到 Redis 中,它会被原子性地执行。
那么,如果使用redis-py
这个库来执行 Lua 脚本呢?在官方文档上也给出了一个示例[2],如下图所示:
于是,我们可以仿照它的写法,来实现一个 Lua 版本的 Hash Append 命令:
代码语言:javascript复制import redis
client = redis.Redis()
def register_redis_lua():
lua = '''
local key = KEYS[1]
local field = ARGV[1]
local new_chinese = ARGV[2]
local chinese_to_update = ""
if redis.call('HEXISTS', key, field) == 1 then
local chinese = redis.call('HGET', key, field)
chinese_to_update = chinese .. ';' .. new_chinese
else
chinese_to_update = new_chinese
end
redis.call('HSET', key, field, chinese_to_update)
'''
lua_instance = client.register_script(lua)
return lua_instance
automic_hash_append = register_redis_lua()
def hash_append(key, field, new_chinese):
automic_hash_append(keys=[key], args=[field, new_chinese])
其中,我们调用register_lua
方法,返回一个脚本实例,这个实例接收两个参数,keys
和args
,他们都是列表。这个脚本对象只需要注册一次,就可以在整个运行时持续使用。
我们来测试一下,首先,在 key 不存在的时候,它会把当前的值添加到 Hash 中:
现在已经close
已经有一个中文意思了,我们再添加一个:
这样,就实现了 Hash 版本的 append 命令。
最后,我们简单讲讲涉及到的 Lua 命令。大部分命令大家看字面意思就能懂。只有一个chinese .. ';' .. new_chinese
可能会让大家困惑一下。实际上,..
在 Lua 里面就是用来连接两个字符串的符号,相当于 Python 中的
。
参考资料
[1]Pipeline Watch: https://github.com/andymccurdy/redis-py#pipelines [2]示例: https://github.com/andymccurdy/redis-py#lua-scripting