Redis键不会自动过期 - Ably


Ably 是一个发布/订阅消息传递的平台。发布是在命名频道上进行的,订阅给定频道的客户端会将该频道上的所有消息传递给他们。我们使用Redis,一个用于基于密钥存储的分布式内存数据库,来存储各种实体,例如身份验证令牌和临时通道状态。它非常适合在我们处理消息时临时存储它们。
在任何给定时间,我们都有数十亿个活动的 Redis 密钥,这些密钥分散在众多 Redis 实例中。分片策略将相关键放在同一个分片中,以便我们可以执行原子更新相关键的操作。我们广泛使用 Lua Redis 脚本来查询和更新键,并依靠脚本执行的原子性来保持相关键值的完整性。也就是说,脚本中的所有命令都运行,或者根本不运行,并且没有其他命令同时执行。
我们还广泛使用过期密钥;Ably 服务的本质是频道的大部分状态都是短暂的,并且仅保留有限的时间段(通常为 2 分钟)。我们将密钥设置为具有 TTL,以便它们自动过期。
 
问题
一组相关键key的完整性要求所有键都存在或不存在。我们曾假设脚本执行的原子性质也适用于脚本调用的过期操作,但实际上,在同一个脚本中实现失效过期多个键也会保持这种完整性?这并不是真的。
虽然过期操作在同一个脚本中自动执行(没有机会发生干预操作),但与每个过期操作相关的时间戳不一定相同。
运行TIME显示两个不同的值:

-- time.lua       

local a = redis.call('time')       
local b = redis.call('time')       
return {a, b}       
$ ./redis-cli --eval /app/time.lua      

1) 1) "1638280442"     
   2)
"996960"     
2) 1)
"1638280442"     
   2)
"996966"      

检查实际到期时间:

-- expire_check.lua     

redis.call('set', 'foo', '1')     
redis.call('expire', 'foo', 1)     

-- slow calls...

redis.call('set', 'bar', '2')     
redis.call('expire', 'bar', 1)     

local fooExpiry = redis.call('PEXPIRETIME', 'foo')     
local barExpiry = redis.call('PEXPIRETIME', 'bar')     
return {fooExpiry, barExpiry}     
$ ./redis-cli --eval /app/expire_check.lua     

1) (integer) 1638280843717     
2) (integer) 1638280843730     

过期可能不是精确的,它可能在 0 到 1 毫秒之间。
这意味着有时某些密钥已过期,但其他相关密钥尚未过期,这可能导致状态不一致。
 
我们的解决方案
解决方案是使用EXPIREAT为所有相关键key设置绝对到期时间,而不是依赖于通过 TTL 的相对到期时间。
EXPIREAT如果键具有相同的设置,是否保证同时发生多个键到期,Redis 文档不清楚。为谨慎起见,我们重新排序了键key到期时间,以确保无论如何我们都避免不一致。

-- expire_new.lua     

-- Unix time     

local now = redis.call('time')[1]     
local expiry = now + 1     
redis.call('set', 'foo', '1')     
redis.call('expireat', 'foo', expiry)     

-- slow calls...     

redis.call('set', 'bar', '2')     
redis.call('expireat', 'bar', expiry)     
local fooExpiry = redis.call('PEXPIRETIME', 'foo')     
local barExpiry = redis.call('PEXPIRETIME', 'bar')     
return {now, fooExpiry, barExpiry}     
$ ./redis-cli --eval /app/expire_new.lua

2) (integer) 1638281266000     
3) (integer) 1638281266000