示例:带有身份验证功能的锁

在了解了乐观锁机制的使用方法之后,现在是时候使用它来实现一个正确的带身份验证功能的锁了。

之前展示的锁实现的问题在于,在 GET 命令执行之后直到 DEL 命令执行之前的这段时间里,锁键的值有可能会发生变化,并出现误删锁键的情况。为了解决这个问题,我们需要使用乐观锁去保证 DEL 命令只会在锁键的值没有发生任何变化的情况下执行,代码清单 13-6 展示了修改之后的锁实现。


代码清单 13-6 带有身份验证功能的锁实现:/pipeline-and-transaction/identity_lock.py

  1. from redis import WatchError
  2.  
  3. class IdentityLock:
  4.  
  5. def __init__(self, client, key):
  6. self.client = client
  7. self.key = key
  8.  
  9. def acquire(self, identity, timeout):
  10. """
  11. 尝试获取一个带有身份标识符和最大使用时限的锁,
  12. 成功时返回 True ,失败时返回 False 。
  13. """
  14. result = self.client.set(self.key, identity, ex=timeout, nx=True)
  15. return result is not None
  16.  
  17. def release(self, input_identity):
  18. """
  19. 根据给定的标识符,尝试释放锁。
  20. 返回 True 表示释放成功;
  21. 返回 False 则表示给定的标识符与锁持有者的标识符并不相同,释放请求被拒绝。
  22. """
  23. # 开启流水线
  24. pipe = self.client.pipeline()
  25. try:
  26. # 监视锁键
  27. pipe.watch(self.key)
  28. # 获取锁键储存的标识符
  29. lock_identity = pipe.get(self.key)
  30. if lock_identity is None:
  31. # 如果锁键的标识符为空,那么说明锁已经被释放
  32. return True
  33. elif input_identity == lock_identity:
  34. # 如果给定的标识符与锁键储存的标识符相同,那么释放这个锁
  35. # 为了确保 DEL 命令在执行时的安全性,我们需要使用事务去包裹它
  36. pipe.multi()
  37. pipe.delete(self.key)
  38. pipe.execute()
  39. return True
  40. else:
  41. # 如果给定的标识符与锁键储存的标识符并不相同
  42. # 那么说明当前客户端不是锁的持有者
  43. # 拒绝本次释放请求
  44. return False
  45. except WatchError:
  46. # 抛出异常说明在 DEL 命令执行之前,已经有其他客户端修改了锁键
  47. return False
  48. finally:
  49. # 取消对键的监视
  50. pipe.unwatch()
  51. # 因为 redis-py 在执行 WATCH 命令期间,会将流水线与单个连接进行绑定
  52. # 所以在执行完 WATCH 命令之后,必须调用 reset() 方法将连接归还给连接池
  53. pipe.reset()

注意,因为乐观锁的效果只会在同时使用 WATCH 命令以及事务的情况下产生,所以程序除了需要使用 WATCH 命令对锁键实施监视之外,还需要将 DEL 命令包裹在事务里面,这样才能确保 DEL 命令只会在锁键的值没有发生任何变化的情况下执行。

以下代码展示了这个锁实现的使用方法:

  1. >>> from redis import Redis
  2. >>> from identity_lock import IdentityLock
  3. >>> client = Redis(decode_responses=True)
  4. >>> lock = IdentityLock(client, "test-lock")
  5. >>> lock.acquire("peter", 3600) # 使用 "peter" 作为标识符,获取一个使用时限为 3600 秒的锁
  6. True
  7. >>> lock.release("tom") # 尝试使用错误的标识符去释放锁,失败
  8. False
  9. >>> lock.release("peter") # 使用正确的标识符去释放锁,成功
  10. True