• 无论某个客户端是否是锁的持有者,只要它调用 方法,锁就会被释放。

    为了解决这个问题,我们需要修改锁实现,给它加上身份验证功能:

    • 客户端在尝试获取锁的时候,除了需要输入锁的最大使用时限之外,还需要输入一个代表身份的标识符,当客户端成功取得锁时,程序将把这个标识符储存在代表锁的字符串键里面。

    • 当客户端调用 release() 方法时,它需要将自己的标识符传给 release() 方法,而 release() 方法则需要验证客户端传入的标识符与锁键储存的标识符是否相同,以此来判断调用 release() 方法的客户端是否就是锁的持有者,从而决定是否释放锁。

    根据以上描述,我们可能会写出代码清单 13-5 所示的代码。



    这个锁实现在绝大部分情况下都能够正常运行,但它的 release() 方法包含了一个非常隐蔽的错误:在程序使用 GET 命令获取锁键的值以后,直到程序调用 DEL 命令删除锁键的这段时间里面,锁键的值有可能已经发生了变化,因此程序执行的 DEL 命令有可能会导致当前持有者的锁被错误地释放。

    举个例子,表 13-1 就展示了一个锁被错误释放的例子:客户端 A 是锁原来的持有者,它调用 release() 方法尝试释放自己的锁,但是当客户端 A 执行完 GET 命令并确认自己就是锁的持有者之后,锁键却因为过期而自动被移除了,紧接着客户端 B 又通过执行 acquire() 方法成功取得了锁,然而客户端 A 并未察觉这一变化,它以为自己还是锁的持有者,并调用 命令把属于客户端 B 的锁给释放了。


    表 13-1 一个错误地释放锁的例子


    为了正确地实现 release() 方法,我们需要一种机制,它可以保证如果锁键的值在 GET 命令执行之后发生了变化,那么 DEL 命令将不会被执行。在 Redis 里面,这种机制被称为乐观锁。

    本节接下来的内容将对 Redis 的乐观锁机制进行介绍,并在之后给出一个使用乐观锁实现的、正确的、具有身份验证功能的锁。

    客户端可以通过执行 WATCH 命令,要求服务器对一个或多个数据库键实施监视,如果在客户端尝试执行事务之前,这些键的值发生了变化,那么服务器将拒绝执行客户端发送的事务,并向它返回一个空值:

    通过同时使用 WATCH 命令和 Redis 事务,我们可以构建出一种针对被监视键的乐观锁机制,确保事务只会在被监视键没有发生任何变化的情况下执行,从而保证事务对被监视键的所有修改都是安全、正确和有效的。

    以下代码展示了一个因为乐观锁机制而导致事务执行失败的例子:


    表 13-2 展示了这个事务执行失败的具体原因:因为客户端 A 监视了 user_id_counter 键,而客户端 B 却在客户端 A 执行事务之前对该键进行了修改,所以服务器最终拒绝了客户端 A 的事务执行请求。


    表 13-2 事务被拒绝执行的完整过程


    其他信息

    UNWATCH:取消对键的监视

    客户端可以通过执行 UNWATCH 命令,取消对所有键的监视:

    服务器在接收到客户端发送的 UNWATCH 命令之后,将不会再对之前 WATCH 命令指定的键实施监视,这些键也不会再对客户端发送的事务造成任何影响。

    除了显式地执行 UNWATCH 命令之外,使用 EXEC 命令执行事务和使用 DISCARD 取消事务,同样会导致客户端撤销对所有键的监视,这是因为这两个命令在执行之后都会隐式地调用 UNWATCH 命令。

    其他信息