一日一技:这个东西能给 Redis 插上火箭

2020-05-14 17:31:56 浏览数 (1)

摄影:产品经理

黄金蘑菇

我们知道,用 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这个词只有一个意思:靠近,现在我要添加另一个意思,你觉得是否可以这样写:

代码语言:javascript复制
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方法,返回一个脚本实例,这个实例接收两个参数,keysargs,他们都是列表。这个脚本对象只需要注册一次,就可以在整个运行时持续使用。

我们来测试一下,首先,在 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

0 人点赞