Redis作为LRU缓存竞争条件



想象一下以下场景:

  1. 客户端#1向Redis请求对象A
  2. Redis回答为null
  3. 客户端#1从数据库请求对象A
  4. 数据库返回对象A
  5. 客户端#1在Redis中存储对象A

一切都很好。让我们在4到5之间添加另一个客户端:

  1. 客户端#1向Redis请求对象A
  2. Redis回答为null
  3. 客户端#1从数据库请求对象A
  4. 数据库返回对象A

    • 客户端#2向Redis请求对象A
    • Redis回答为null
    • 客户端#2从dtabase请求对象A
    • 数据库返回对象A
    • 客户端#2将对象A存储在Redis中
    • 客户端#2更新数据库和Redis中的对象A
  5. 客户端#1在Redis中存储对象A

现在对象A在数据库中是最新的,但在Redis中已经过时了。有什么模式可以防止这种行为吗?显然,在等待Redis存储其副本的同时锁定数据库并不是一个解决方案。

我还应该注意到,我使用的是NodeJS,它维护了一个到Redis的固定大小的连接池,并且请求是异步处理的,所以我们不能假设来自不同客户端的查询顺序不会混合在一个连接中。

如果您的数据不经常更改(非易失性),如昨天或一个月前的历史数据,您可以安全地忽略竞争条件,因为无论谁最后一次提交,客户端之间的数据仍然相同。

对于时间敏感的数据,尤其是事务数据,可以使用用户id(客户端#1、客户端#2)作为附加密钥,基于用户id来分离缓存。这将消除客户端之间的竞争条件,而无需借助锁定和事务同步内存使用率较高的缺点

此方法可以与第一种方法组合,例如,如果数据是关于用户特定数据的,您可以使用用户id键,另一方面,如果数据包含共享/分组信息,如基于角色的菜单访问,您可以改为使用组id。

您正在寻找Redis事务。关键是WATCH命令。在初始读取之前对对象A调用WATCH,并将对象设置在MULTI中。现在你的两个场景看起来像:

  1. 客户端#1在Redis中监视对象A
  2. 客户端#1向Redis请求对象A
  3. Redis回答为null
  4. 客户端#1从数据库请求对象A
  5. 数据库返回对象A
  6. 客户端#1在MULTI/EXEC块中将对象A存储在Redis中

  1. 客户端#1在Redis中监视对象A
  2. 客户端#1向Redis请求对象A
  3. Redis回答为null
  4. 客户端#1从数据库请求对象A
  5. 数据库返回对象A

    • 客户端#2向Redis请求对象A
    • Redis回答为null
    • 客户端#2从dtabase请求对象A
    • 数据库返回对象A
    • 客户端#2将对象A存储在Redis中
    • 客户端#2更新数据库和Redis中的对象A
  6. 客户端#1将对象A存储在Redis的MULTI/EXEC块中,,但它失败了,因为对象A在WATCH之后发生了更改

失败后,客户端#1可以重试,但最好忽略失败,因为看起来您只是将其用作缓存。

当然,客户端#2也应该执行WATCH/MULTI/EXEC模式。

如果对象上没有更新操作(第五步),您应该使用SETNX来存储它。这样您就不会存储过时的对象。

最新更新