石锅拌饭

memcached使用中的竞争条件

by Robin Lu on Oct.25, 2007, about , ,

在通过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,修改缓存更新策略吧。

:, ,

6 Comments for this entry

  • adou

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

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

    这样应该会简单些

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

  • Robin Lu

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

  • adou

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

  • adou

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

  • Robin Lu

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

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

2 Trackbacks / Pingbacks for this entry

Search

Archives

Browse by tags

agile apple blog book design ecto extension firefox git google hack ichm iphone keyword life mac madfox movie nonsense opensource plugin pm ruby rubyonrails sns software startup wordpress work 财帮子