SET命令的EX选项和PX选项

在使用键过期功能时,组合使用 SET 命令和 EXPIRE/PEXIRE 命令的做法非常常见,比如上面展示的带有自动移除特性的缓存程序就是这样做的。

因为SET命令和EXPIRE/PEXPIRE命令组合使用的情况如此常见,所以为了方便用户使用这两组命令,Redis从2.6.12版本开始为SET命令提供EX选项和PX选项,用户可以通过使用这两个选项的其中一个来达到同时执行SET命令和EXPIRE/PEXPIRE命令的效果:

SET key value [EX seconds] [PX milliseconds]

也就是说,如果我们之前执行的是SET命令和EXPIRE命令:

SET key value
EXPIRE key seconds

那么现在只需要执行一条带有EX选项的SET命令就可以了:

SET key value EX seconds

与此类似,如果我们之前执行的是SET命令和PEXPIRE命令:

SET key value
PEXPIRE key milliseconds

那么现在只需要执行一条带有PX选项的SET命令就可以了:

SET key value PX milliseconds

组合命令的安全问题

使用带有EX选项或PX选项的SET命令除了可以减少命令的调用数量并提升程序的执行速度之外,更重要的是保证了操作的原子性,使得“为键设置值”和“为键设置生存时间”这两个操作可以一起执行。

比如,前面在实现带有自动移除特性的缓存程序时,我们首先使用了 SET命令设置缓存,然后又使用了EXPIRE命令为缓存设置生存时间,这相当于让程序依次向Redis服务器发送以下两条命令:

SET key value
EXPIRE key timeout

因为这两条命令是完全独立的,所以服务器在执行它们的时候,可能出现SET命令被执行了,但是EXPIRE命令却没有被执行的情况。比如,如果Redis服务器在成功执行SET命令之后因为故障下线,导致EXPIRE命令没有被执行,那么SET命令设置的缓存就会一直存在,而不会因为过期而自动被移除。

与此相反,使用带有EX选项或PX选项的SET命令就没有这个问题:当服务器成功执行了一条带有EX选项或PX选项的SET命令时,键的值和生存时间都会同时被设置好,因此程序就不会出现只设置了值但是却没有设置生存时间的情况。

基于上述原因,我们把前面展示的缓存程序实现称之为“不安全” (unsafe)实现。为了修复这个问题,我们可以使用带有EX选项的SET命令来重写缓存程序,重写之后的程序如代码清单12-2所示。

代码清单12-2 重写之后的缓存程序:/expire/volatile_cache.py
class VolatileCache:

    def __init__(self, client):
        self.client = client

    def set(self, key, value, timeout):
        """
        把数据缓存到键 key 里面,并为其设置过期时间。
        如果键 key 已经有值,那么使用新值去覆盖旧值。
        """
        self.client.set(key, value, ex=timeout)

    def get(self, key):
        """
        获取键 key 储存的缓存数据。
        如果键不存在,又或者缓存已经过期,那么返回 None 。
        """
        return self.client.get(key)

重写之后的缓存程序实现是 “安全的”:设置缓存和设置生存时间这两个操作要么一起成功,要么一起失败,“设置缓存成功了,但是设置生存时间却失败了” 这样的情况不会出现。后续的章节也会介绍如何通过 Redis 的事务功能来保证执行多条命令时的安全性。

其他信息

  • 复杂度:O(1)。

  • 版本要求:带有 EX 选项和 PX 选项的 SET 命令从 Redis 2.6.12 版本开始可用。

示例:带有自动释放特性的锁

在第 2 章,我们曾实现过一个锁程序,它的缺陷之一就是无法自行释放:如果锁的持有者因为故障下线,那么锁将一直处于持有状态,导致其他进程永远无法获得锁。

为了解决这个问题,我们可以在获取锁的同时,通过Redis的自动过期特性为锁设置一个最大加锁时限,这样,即使锁的持有者由于故障下线,锁也会在时限到达之后自动释放。

代码清单12-3展示了使用上述原理实现的锁程序。

代码清单12-3 带有自动释放特性的锁:/expire/timing_lock.py
VALUE_OF_LOCK = "locking"

class TimingLock:

    def __init__(self, client, key):
        self.client = client
        self.key = key

    def acquire(self, timeout):
        """
        尝试获取一个带有秒级最大使用时限的锁,
        成功时返回 True ,失败时返回 False 。
        """
        result = self.client.set(self.key, VALUE_OF_LOCK, ex=timeout, nx=True)
        return result is not None

    def release(self):
        """
        尝试释放锁。
        成功时返回 True ,失败时返回 False 。
        """
        return self.client.delete(self.key) == 1

以下代码演示了这个锁的自动释放特性:

>>> from redis import Redis
>>> from timing_lock import TimingLock
>>> client = Redis()
>>> lock = TimingLock(client, "test-lock")
>>> lock.acquire(5) # 获取一个在5s之后自动释放的锁
True
>>> lock.acquire(5) # 在5s之内尝试再次获取锁,但是由于锁未被释放而失败
False
>>> lock.acquire(5) # 在5s之后尝试再次获取锁
True # 因为之前获取的锁已经自动被释放,所以这次将成功取得新的锁