无论某个客户端是否是锁的持有者,只要它调用 方法,锁就会被释放。
为了解决这个问题,我们需要修改锁实现,给它加上身份验证功能:
客户端在尝试获取锁的时候,除了需要输入锁的最大使用时限之外,还需要输入一个代表身份的标识符,当客户端成功取得锁时,程序将把这个标识符储存在代表锁的字符串键里面。
当客户端调用
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
命令。