dot Redis 8 来了——而且是开源的

了解更多

11.2.2 重写我们的锁

返回主页

11.2.2 重写我们的锁

您可能还记得第 6.2 节,锁定涉及生成一个 ID,有条件地
使用 SETNX 设置一个键,并在成功后设置键的过期时间。
虽然概念上很简单,但我们必须处理故障和重试,这导致
在下一个列表中显示的原始代码。

列表 11.4 我们在第 6.2.5 节中的最终 acquire_lock_with_timeout() 函数
def acquire_lock_with_timeout(
    conn, lockname, acquire_timeout=10, lock_timeout=10):
    identifier = str(uuid.uuid4())

一个 128 位的随机标识符。

    lockname = 'lock:' + lockname
    lock_timeout = int(math.ceil(lock_timeout))

只将整数传递给我们的 EXPIRE 调用。

    end = time.time() + acquire_timeout
    while time.time() < end:
        if conn.setnx(lockname, identifier):
            conn.expire(lockname, lock_timeout)

获取锁并设置过期时间。

            return identifier
        elif not conn.ttl(lockname):
            conn.expire(lockname, lock_timeout)

根据需要检查和更新过期时间。

        time.sleep(.001)

    return False

如果您记得我们在第 6.2 节中是如何构建这个锁的,那么这里没有什么太令人惊讶的。
让我们继续提供相同的功能,但将核心锁定
放入 Lua 中。

列表 11.5 使用 Lua 重写的 acquire_lock_with_timeout()
def acquire_lock_with_timeout(
    conn, lockname, acquire_timeout=10, lock_timeout=10):
    identifier = str(uuid.uuid4())
    lockname = 'lock:' + lockname
    lock_timeout = int(math.ceil(lock_timeout))
    acquired = False
    end = time.time() + acquire_timeout
    while time.time() < end and not acquired:
        acquired = acquire_lock_with_timeout_lua(
            conn, [lockname], [lock_timeout, identifier]) == 'OK'

实际获取锁,检查以验证 Lua 调用是否成功完成。

        time.sleep(.001 * (not acquired))

    return acquired and identifier

acquire_lock_with_timeout_lua = script_load('''
if redis.call('exists', KEYS[1]) == 0 then

如果锁不存在,再次记住表使用基于 1 的索引。

    return redis.call('setex', KEYS[1], unpack(ARGV))

使用提供的过期时间和标识符设置键。

end
''')

代码中没有任何重大更改,除了我们更改了命令
我们使用的方法,以便在获得锁时,它始终具有超时。 让我们也继续
并重写释放锁代码以使用 Lua。

以前,我们监视锁键,然后验证锁是否仍然具有
相同的值。 如果它具有相同的值,我们将删除锁;否则我们会说
锁丢失了。 下一个显示的是我们的 Lua 版本的 release_lock()

列表 11.6 使用 Lua 重写的 release_lock()
def release_lock(conn, lockname, identifier):
    lockname = 'lock:' + lockname
    return release_lock_lua(conn, [lockname], [identifier])'

调用释放锁的 Lua 函数。

release_lock_lua = script_load(''
if redis.call('get', KEYS[1]) == ARGV[1] then

确保锁匹配。

    return redis.call('del', KEYS[1]) or true

删除锁并确保我们返回 true。

end
''')

与获取锁不同,释放锁变得更短,因为我们不再需要
执行所有典型的 WATCH/MULTI/EXEC 步骤。

减少代码是好的,但如果我们没有实际改进
锁本身的性能。 我们在锁定中添加了一些仪器
代码以及一些基准测试代码,这些代码执行 1、2、5 和 10 个并行进程
重复获取和释放锁。 我们计算尝试获取的次数
锁以及在 10 秒内获取锁的次数,以及我们的原始
以及基于 Lua 的获取和释放锁功能。 表 11.2 显示了调用的次数
执行成功。

表 11.2 我们的原始锁与基于 Lua 的锁在 10 秒内的性能

基准测试配置

10 秒内的尝试次数

10 秒内的获取次数

原始锁,1 个客户端

31,359

31,359

原始锁,2 个客户端

30,085

22,507

原始锁,5 个客户端

47,694

19,695

原始锁,10 个客户端

71,917

14,361

Lua 锁,1 个客户端

44,494

44,494

Lua 锁,2 个客户端

50,404

42,199

Lua 锁,5 个客户端

70,807

40,826

Lua 锁,10 个客户端

96,871

33,990

查看我们基准测试中的数据(注意右侧的列),一个
值得注意的是,基于 Lua 的锁在获取和释放锁方面取得了成功
周期比我们之前的锁显着更多 - 单个锁多出 40% 以上
客户端,2 个客户端时为 87%,5 个或 10 个客户端尝试获取时超过 100%
并释放相同的锁。 比较中间和右侧的列,我们还可以看到
使用 Lua 进行锁定的尝试速度有多快,这主要是由于减少了
往返次数。

但即使比性能改进更好,我们的代码来获取和释放
锁更容易理解和验证是否正确。

我们构建同步原语的另一个例子是使用信号量;
让我们看看接下来构建它们。