您可能还记得第 6.2 节,锁定涉及生成一个 ID,有条件地
使用 SETNX 设置一个键,并在成功后设置键的过期时间。
虽然概念上很简单,但我们必须处理故障和重试,这导致
在下一个列表中显示的原始代码。
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 中。
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()。
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 显示了调用的次数
执行成功。
基准测试配置 |
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 进行锁定的尝试速度有多快,这主要是由于减少了
往返次数。
但即使比性能改进更好,我们的代码来获取和释放
锁更容易理解和验证是否正确。
我们构建同步原语的另一个例子是使用信号量;
让我们看看接下来构建它们。