memcached使用中的竞争条件

在通过ruby的memcached-client使用memcached的过程中,遇到一些问题,数据更新时清除了缓存,缓存重建的时候却仍然是老数据,在并发密集的情况下更容易出现。研究了一下,类似这样典型的memcache使用方法:

Controller里:

...
def foo_action
  ...
  unless d = Cache.get("key")
    d = Data.find(...)
    Cache.put("key", d)
  end
  ...
end
...

Model里

...
def after_save
   ...
   Cache.delete("key")
   ...
end
...

存在下面的竞争条件(race condition):

存在两个rails应用实例(比如两个并发的mongrel)A和B,

  1. memcached清除过期缓存c。
  2. 实例A运行foo_action,发现缓存c不存在(Cache.get失败),读取数据d。
  3. 实例B更新数据d’,清除过期缓存c(Cache.delete)。
  4. 实例A保存缓存c(Cache.put),其中的数据是老数据d。

这时,再有数据访问缓存c的时候,c已经存在,到下一次缓存c被清除前,这个缓存都是存在问题的过期数据。不难看出,即使将第三步中的清除过期缓存c改成更新缓存c,c仍然会被实例A在第四步覆盖。

其实,memcached为了避免这种竞争条件,提供了一些便利的原子操作(参看memcached protocol):

“add” means “store this data, but only if the server *doesn’t* already
hold data for this key”.

“cas” is a check and set operation which means “store this data but
only if no one else has updated since I last fetched it.”

将第三步中的“清除过期缓存c”变成“用memcached set方法(也就是memcached-client中的Cache.put)来更新缓存c”,然后在第四步中始终用add方法来更新缓存c,就可以解决问题。也就是说,在能够确认数据是最新的地方,比如after save中,不采用Cache.delete,而直接用Cache.put来更新缓存,在不能确认是否是最新数据的其它地方,只使用Cache.add,就能保证过期数据不会在race condition下覆盖新数据。(更新无论是Cache.delete还是Cache.put,放在after_save中仍然有问题,会因为activerecord的built-in transaction而破坏了数据的完整性,具体参见再谈rails缓存机制的问题)

memcached-client从1.4.0起才开始支持add方法,目前还不支持cas方法。不过add方法已经能够解决不少竞争条件了。如果你也有类似的问题,升级memcached-client,修改缓存更新策略吧。

8 thoughts on “memcached使用中的竞争条件

  • 大多数情况把取数据时候生成缓存的set改成add应该就够了

    修改或删除数据时 如果需要set的话 又得去获取一次完整的缓存数据 直接delete 等取数据的时候再生成好了

    这样应该会简单些

    当然如果能很方便得到完整的需要缓存的数据 在修改后直接set更好 那样可以直接让下次被访问时就直接从缓存取数据

  • 楼上,请仔细阅读我的原文,你就能理解为什么不能在取数据的时候再生成。
    直接把set改成add是不能解决问题的,除非你在add的时候能够确认你使用的是新数据,否则可能会把事情搞得更糟糕。

  • 嗯 有竞争的时候确实是会有问题 但是可能被查询到的数据有上亿条 并且有各种组合 不可能把它们全部放进缓存 现在只是当个别被访问到的时候才进行缓存 然后设置一个过期时间 如果不在取数据的时候生成缓存 有什么好方法吗 🙂 不吝赐教

  • 设置缓存的时候 数据是从master而不是slave数据库取 基本能保证数据是新的 在取出数据到设置缓存这一瞬间如果有并发的写 确实会有问题

  • 对于你上面提到的情况,如果数据更新相对频繁,对展示准确性要求不是十分严格的情况下,可以通过缩短缓存过期时间来缓解这个问题。如果对数据一致性要求非常严格,可以配合cas方法来解决。cas方法为数据添加了“版本”信息,能解决更多的竞争问题。

    目前ruby的memcached-client没有实现cas方法,需要自己实现。

Comments are closed.