GETSET:获取旧值并设置新值

GETSET 命令就像 GET 命令和 SET 命令的组合版本,GETSET 首先获取字符串键目前已有的值,接着为键设置新值,最后把之前获取到的旧值返回给用户:

GETSET key new_value

以下代码展示了如何使用 GETSET 命令去获取 number 键的旧值并为它设置新值:

redis> GET number -- number键现在的值为"10086"
"10086"

redis> GETSET number "12345"
"10086" -- 返回旧值

redis> GET number -- number键的值已被更新为"12345"
"12345"

如果被设置的键并不存在于数据库,那么 GETSET 命令将返回空值作为键的旧值:

redis> GET counter
(nil) -- 键不存在

redis> GETSET counter 50
(nil) -- 返回空值作为旧值

redis> GET counter
"50"

其它信息

  • 复杂度:O(1)。

  • 版本要求:GETSET 命令从 Redis 1.0.0 开始可用

示例:缓存

对数据进行缓存是 Redis 最常见的用法之一,因为缓存操作是指把数据存储在内存而不是硬盘上,而访问内存远比访问硬盘的速度要快得多,所以用户可以通过把需要快速访问的数据存储在 Redis 中来提升应用程序的速度。

代码清单 2-1 展示了一个使用 Redis 实现的缓存程序代码,这个程序使用 SET 命令将需要缓存的数据存储到指定的字符串键中,并使用 GET 命令来从指定的字符串键中获取被缓存的数据。

代码清单 2-1 使用字符串键实现的缓存程序:/string/cache.py
class Cache:

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

    def set(self, key, value):
        """
        把需要被缓存的数据储存到键 key 里面,
        如果键 key 已经有值,那么使用新值去覆盖旧值。
        """
        self.client.set(key, value)

    def get(self, key):
        """
        获取储存在键 key 里面的缓存数据,
        如果数据不存在,那么返回 None 。
        """
        return self.client.get(key)

    def update(self, key, new_value):
        """
        对键 key 储存的缓存数据进行更新,
        并返回键 key 在被更新之前储存的缓存数据。
        如果键 key 之前并没有储存数据,
        那么返回 None 。
        """
        return self.client.getset(key, new_value)

除了用于设置缓存的 set() 方法以及用于获取缓存的 get() 方法之外,缓存程序还提供了由 GETSET 命令实现的 update() 方法,这个方法可以让用户在对缓存进行设置的同时,获得之前被缓存的旧值。用户可以根据自己的需要决定是使用 set() 方法还是 update() 方法对缓存进行设置。

以下代码展示了如何使用这个程序来缓存一个 HTML 页面,并在需要时获取它:

>>> from redis import Redis
>>> from cache import Cache
>>> client = Redis(decode_responses=True) # 使用文本编码方式打开客户端
>>> cache = Cache(client)
>>> cache.set("greeting-page", "<html><p>hello world</p></html>")
>>> cache.get("greeting-page")
'<html><p>hello world</p></html>'
>>> cache.update("greeting-page", "<html><p>good morning</p></html>")
'<html><p>hello world</p></html>'
>>> cache.get("greeting-page")
'<html><p>good morning</p></html>'

因为 Redis 的字符串键不仅可以存储文本数据,还可以存储二进制数据,所以这个缓存程序不仅可以用来缓存网页等文本数据,还可以用来缓存图片和视频等二进制数据。比如,如果你正在运营一个图片网站,那么你同样可以使用这个缓存程序来缓存网站上的热门图片,从而提高用户访问这些热门图片的速度。

作为例子,以下代码展示了将 Redis 的 Logo 图片缓存到键 redis-logo.jpg 中的方法:

>>> from redis import Redis
>>> from cache import Cache
>>> client = Redis # 使用二进制编码方式打开客户端
>>> cache = Cache(client)
>>> image = open("redis-logo.jpg", "rb") # 以二进制只读方式打开图片文件
>>> data = image.read() # 读取文件内容
>>> image.close() # 关闭文件
>>> cache.set("redis-logo.jpg", data) # 将内存缓存到键redis-logo.jpg中
>>> cache.get("redis-logo.jpg")[:20] # 读取二进制数据的前20个字节
b'\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00H\x00H\x00\x00'

在测试以上两段代码的时候,请务必以正确的编码方式打开客户端(第一段代码采用文本方式,第二段代码采用二进制方式),否则测试代码将会出现编码错误。

示例:锁

锁是一种同步机制,用于保证一项资源在任何时候只能被一个进程使用,如果有其他进程想要使用相同的资源,那么就必须等待,直到正在使用资源的进程放弃使用权为止。

一个锁的实现通常会有获取(acquire)和释放(release)这两种操作:

  • 获取操作用于取得资源的独占使用权。在任何时候,最多只能有一个进程取得锁,我们把成功取得锁的这个进程称为锁的持有者。在锁已经被持有的情况下,所有尝试再次获取锁的操作都会失败。

  • 释放操作用于放弃资源的独占使用权,一般由锁的持有者调用。在锁被释放之后,其他进程就可以再次尝试获取这个锁了。

代码清单 2-2 展示了一个使用字符串键实现的锁程序,这个程序会根据给定的字符串键是否有值来判断锁是否已经被获取,而针对锁的获取操作和释放操作则是分别通过设置字符串键和删除字符串键来完成的。

代码清单 2-2 使用字符串键实现的锁程序:/string/lock.py
VALUE_OF_LOCK = "locking"

class Lock:

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

    def acquire(self):
        """
        尝试获取锁。
        成功时返回 True ,失败时返回 False 。
        """
        result = self.client.set(self.key, VALUE_OF_LOCK, nx=True)
        return result is True

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

获取操作 acquire() 方法是通过执行带有 NX 选项的 SET 命令来实现的:

result = self.client.set(self.key, VALUE_OF_LOCK, nx=True)

NX 选项的值确保了代表锁的字符串键只会在没有值的情况下被设置:

  • 如果给定的字符串键没有值,那么说明锁尚未被获取,SET 命令将执行设置操作,并将 result 变量的值设置为 True。

  • 如果给定的字符串键已经有值了,那么说明锁已经被获取,SET 命令将放弃执行设置操作,并将 result 变量的值设置为 None。

acquire() 方法最后会通过检查 result 变量的值是否为 True 来判断自己是否成功取得了锁。

释放操作 release() 方法使用了之前没有介绍过的 DEL 命令,这个命令接受一个或多个数据库键作为参数,尝试删除这些键以及与之相关联的值,并返回被成功删除的键的数量作为结果:

DEL key [key ...]

因为 Redis 的 DEL 命令和 Python 的 del 关键字重名,所以在 redis-py 客户端中,执行 DEL 命令实际上是通过调用 delete() 方法来完成的:

self.client.delete(self.key) == 1

release() 方法通过检查 delete() 方法的返回值是否为 1 来判断删除操作是否执行成功:如果用户尝试对一个尚未被获取的锁执行 release() 方法,那么方法将返回 false,表示没有锁被释放。

在使用 DEL 命令删除代表锁的字符串键之后,字符串键将重新回到没有值的状态,这时用户就可以再次调用 acquire() 方法去获取锁了。

以下代码演示了这个锁的使用方法:

>>> from redis import Redis
>>> from lock import Lock
>>> client = Redis(decode_responses=True)
>>> lock = Lock(client, 'test-lock')
>>> lock.acquire() # 成功获取锁
True
>>> lock.acquire() # 锁已被获取,无法再次获取
False
>>> lock.release() # 释放锁
True
>>> lock.acquire() # 锁释放之后可以再次被获取
True

虽然代码清单 2-2 中展示的锁实现了基本的获取和释放功能,但它并不完美:

  • 因为这个锁的释放操作无法验证进程的身份,所以无论执行释放操作的进程是否为锁的持有者,锁都会被释放。如果锁被持有者以外的其他进程释放,那么系统中可能会同时出现多个锁,导致锁的唯一性被破坏。

  • 这个锁的获取操作不能设置最大加锁时间,因而无法让锁在超过给定的时限之后自动释放。因此,如果持有锁的进程因为故障或者编程错误而没有在退出之前主动释放锁,那么锁就会一直处于已被获取的状态,导致其他进程永远无法取得锁。

本书后续将继续改进这个锁的实现,使得它可以解决这两个问题。