Memcache mutex设计模式

周六的S2 Web 2.0技术沙龙上介绍了memcache中使用mutex场景(文后要演讲稿),有网友对详情感兴趣,简单介绍如下。

场景

Mutex主要用于有大量并发访问并存在cache过期的场合,如

  • 首页top 10, 由数据库加载到memcache缓存n分钟
  • 微博中名人的content cache, 一旦不存在会大量请求不能命中并加载数据库
  • 需要执行多个IO操作生成的数据存在cache中, 比如查询db多次

问题

在大并发的场合,当cache失效时,大量并发同时取不到cache,会同一瞬间去访问db并回设cache,可能会给系统带来潜在的超负荷风险。我们曾经在线上系统出现过类似故障

解决方法

方法一
在load db之前先add一个mutex key, mutex key add成功之后再去做加载db, 如果add失败则sleep之后重试读取原cache数据。为了防止死锁,mutex key也需要设置过期时间。伪代码如下
(注:下文伪代码仅供了解思路,可能存在bug,欢迎随时指出。)

if (memcache.get(key) == null) {
    // 3 min timeout to avoid mutex holder crash
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
        value = db.get(key);
        memcache.set(key, value);
        memcache.delete(key_mutex);
    } else {
        sleep(50);
        retry();
    }
}

方法二
在value内部设置1个超时值(timeout1), timeout1比实际的memcache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中。伪代码如下

v = memcache.get(key);
if (v == null) {
    if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
        value = db.get(key);
        memcache.set(key, value);
        memcache.delete(key_mutex);
    } else {
        sleep(50);
        retry();
    }
} else {
    if (v.timeout <= now()) {
        if (memcache.add(key_mutex, 3 * 60 * 1000) == true) {
            // extend the timeout for other threads
            v.timeout += 3 * 60 * 1000;
            memcache.set(key, v, KEY_TIMEOUT * 2);

            // load the latest value from db
            v = db.get(key);
            v.timeout = KEY_TIMEOUT;
            memcache.set(key, value, KEY_TIMEOUT * 2);
            memcache.delete(key_mutex);
        } else {
            sleep(50);
            retry();
        }
    }
}

相对于方案一
优点:避免cache失效时刻大量请求获取不到mutex并进行sleep
缺点:代码复杂性增大,因此一般场合用方案一也已经足够。

方案二在Memcached FAQ中也有详细介绍 How to prevent clobbering updates, stampeding requests,并且Brad还介绍了用他另外一个得意的工具 Gearman 来实现单实例设置cache的方法,见 Cache miss stampedes,不过用Gearman来解决就感觉就有点奇技淫巧了。

附:本次Web2.0技术沙龙演讲主题:微博Cache设计谈,需下载请点击演讲稿下menu/download (需登录slideshare)。

如想及时阅读 Tim Yang 的文章,可通过页面右上方扫码订阅最新更新。

« | »

Comments

59 Comments

  1. bright

    确实,方案2控制的思路更加精细,增加mutex key失败后retry是合理的,因为tim要实现的是”要取到最新数据且避免数据库访问压力”,楼上有人提出直接返回cache中timeout1已经过期的数据也是合理的,但超出了tim的前提场景了。

  2. […] 前几天看了Timyang的一篇博文,提到他们的一个系统,memcached成为了系统的瓶颈,考虑到我们的系统对memcached的依赖度也非常高,有必要由别的缓存来分担一些memcached的压力,避免过多的网络IO。本地缓存由于只存在于本机内存中,调用效率应该是最高的,故决定在memcached上面加一层本地缓存。 […]

  3. 有两个问题:
    1.
    // load the latest value from db
    v = db.get(key);
    v.timeout = KEY_TIMEOUT;
    memcache.set(key, value, KEY_TIMEOUT * 2);

    这里的value是指什么?上下文也没出现过,是不是写错了。

    2.
    if( v.timeout now()时直接返回。

  4. 回复的内容怎么丢掉了了。
    第二个问题是:
    每次
    if (v.timeout now()) 时就直接返回吧。

  5. 第二个问题是:
    每次 if (v.timeout now()) 就直接返回了

  6. 如果捕捉不到内容,则把key扔进队列,然后定期把队列里的内容再此扔进cache中,前端只是做个友好界面提醒。。。

  7. 队列和数据库交互,把数据扔进cache中。

  8. […] mutex问题导致大量请求穿透到db。后他又写了一篇博客《Memcache mutex设计模式》阐述这个问题。关于cache的key […]

  9. […] FROM:http://timyang.net/programming/memcache-mutex/comment-page-1/ cache中的key mutex问题解决及延伸应用 […]

  10. 呃。。。这个不是很了解。。。

  11. xfly

    项目中就是使用的这种分布式缓存+mutex的方式控制并发,当时还跟架构小pk了下,现在终于有新的依据了,hoho~~:D

  12. bingobird

    第一种方案有点危险,如果批量请求是一个DB中不存在的值,则会引发线程堵塞(因为retry也不会有数据)。可以将retry改成判断锁是否存在。
    第二种方案里,如果没拿到锁,sleep跟retry也可以不做了,因为毕竟没有过期。
    (有些同学说为了拿到最新数据,但阻塞在此为了拿新数据实在不划算)

  13. 第二种方法不错

  14. […] 原文地址:http://timyang.net/programming/memcache-mutex/ 此条目是由 Mojay 发表在 趣 分类目录的。将固定链接加入收藏夹。 […]

  15. zgl

    1 无止境的retry确实不太合适,我的做法是retry3次之后,则放弃并做合适的后续动作;此外可以考虑一些trick,比如每次sleep的时间逐渐增加。
    2 extend timeout没有必要,不如直接保证timeout1-timeout2足够大(至少大于一次DB查询的时间)
    3 拿不到更新锁的线程完全可以返回当前的cache值:a 此时不算脏数据,b 否则就失去这个提前预判cache失效思路的最大价值了

    if ((retVal = cache.getAsExpireableCacheObjectWrapper(key))!=null) {

    if (!retVal.isAboutToExpire(expireThreshold)) {
    return retVal;
    }
    //need pre-refresh the cache.
    return preRefreshCache();

    } else {
    return whenCacheMissed();
    }

    preRefreshCache() {
    //set flag
    if (!getCacheUpdateLock(…,10s)) {
    //Failed: Some other guy is refreshing -> return v
    return retVal;
    }
    //Success: I should do this refresh
    Object object = DB.query(…);
    Object put = cache.put(key, new ExpireableCacheObjectWrapper(object,expireOffsetSec,System.currentTimeMillis()), expireOffsetSec);
    //reset refresh flag.
    releaseCacheUpdateLock(…);
    return object;
    }

    whenCacheMissed() {
    if (!getCacheUpdateLock(…,10s)) {
    // some other guy is doing the DB job.
    //sleep and then re-check the cache
    Thread.sleep(1000);
    ExpireableCacheObjectWrapper retVal;
    int i=0;
    while ((retVal = cache.getAsExpireableCacheObjectWrapper(key))==null&&i<3) {
    i++;
    Thread.sleep(1000);
    }
    if (retVal==null) {
    throw new IllegalStateException("cache[key:" +key+
    "] waited for the DB guy for … seconds, and still no luck. fail this action, throw Exception.");
    }
    //cache hit on subsequent check(s)
    return retVal;
    } else {
    //query DB and then feed cache.
    //and then release flag
    Object obj = DB.query(…);
    Object put = cache.put(key, new ExpireableCacheObjectWrapper(obj,expireOffsetSec,System.currentTimeMillis()),expireOffsetSec);
    releaseCacheUpdateLock(…);
    return obj;
    }
    }

  16. […] Memcache mutex设计模式http://timyang.net/programming/memcache-mutex/ […]

  17. 无论是第一和第二种方法,不停的retry肯定不行,当某个读DB的操作非常耗费时间的时候,很多请求都会阻塞。但是如果设置一个retry次数,那么就意味着,如果DB读取超时,返回的数据就会是空。不知道各位有何高见?

  18. stevencjh

    第二个 解决方案的第二个判断应该是直接返回cache值吧,否则跟第一种没有任何区别啊

  19. […] 减少缓存雪崩,防止 DB 连接数暴涨、响应变慢,连累前端应用连接数持续高涨、最后宕机。 图2 缓存控制体系(图出自 http://www.alidata.org/archives/1789 )   2.6.缓存重建   既然有缓存过期,自然有缓存重建。   热点数据的缓存重建,无论是本地缓存还是远端缓存,都有必要加锁来确保进程内同一时刻只有一个 Worker 负责重建,甚至利用分布式锁保证集群环境下只有一个重建者,避免缓存雪崩时的 Race Condition。TimYang 早在2010年在《Memcache mutex设计模式》中描述过如下风险:”在大并发的场合,当cache失效时,大量并发同时取不到cache,会同一瞬间去访问db并回设cache,可能会给系统带来潜在的超负荷风险。我们曾经在线上系统出现过类似故障。“孙立将这种场景称为 cache key mutex 问题[注7]。 图3 cache key mutex 问题的解决(图出自 http://www.cnblogs.com/sunli/archive/2010/07/27/cache_key_mutex.html)   简而言之,缓存重建时,当多个 Client 对同一个缓存数据发起请求时,会在客户端采用加锁等待的方式,对同一个 CacheKey 的重建需要获取到相应的排他锁才行,只有拿到锁的 Client 才能访问数据库重建缓存,其他的 Client 都需要等待这个拿到锁的 Client 重建好缓存后直接读缓存。这样,对同一个缓存数据,只有一次数据库重建访问。但是如果访问分散比较严重,还是会瞬间对数据库造成非常大的压力。     当然也可以不加(悲观)锁,那么多线程并发读写同一个 cache key 可能会带来“ABA问题”。   解决方法很简单:memcached 1.2.5以及更高版本提供了 gets 和 cas 命令。如果使用 gets 命令查询某个键值,memcached 会返回该键值的唯一标识 casUnique。如果覆写了这个键值并想把它写回到 memcached 中,可以通过 cas 命令把那个 casUnique 一起发送给 memcached。如果该键值存放在 memcached 中的 casUnique 与提供的一致,写操作将会成功。如果另一个进程在这期间也修改了这个键值,那么该键值存放在 memcached 中的 casUnique 将会改变,写操作就会失败。   2.7.缓存永不过期   因为担心缓存失效带来的系统抖动,所以有些业务场景会让缓存永不过期,数据变化时,由后端负责维护缓存数据一致性。   2.8.电商场景里的缓存计数器:秒杀和超卖   我们在秒杀和防超卖场景里的实现逻辑类似于淘宝这篇博客[注3]所提及的”分布式缓存计数器“,所以我就直接照搬过来了: […]

  20. […] Memcache mutex设计模式 […]

  21. daydayup

    但是这种方式应该还是不能解决应用分布在多台机器上的时候可能会出现的thundering herd的情况,好想只能用来解决单机上的多进程/多线程之间访问出现cache失效的时候可能出现的情况。

  22. […] [Tim]Memcache mutex设计模式 […]

  23. qingchao

    memcache.add(key_mutex, 3 * 60 * 1000) == true
    如果不加value的话,过期时间不生效~

  24. “文后要演讲稿”链接在我这里失效了,代理到教育网也不行,可不可以更新下?

  25. […] 本文章转自http://timyang.net/programming/memcache-mutex/ 周六的S2 Web 2.0技术沙龙上介绍了memcache中使用mutex场景(文后要演讲稿),有网友对详情感兴趣,简单介绍如下。 […]

  26. vision57

    在不大改现有代码架构的情况下,算是个相当不错的trick。
    上面很多人提到定时任务+异步队列的方案主动批量刷新缓存,这样性能确实会更好,但是可能会引入分布式定时任务调度、分布式异步队列等额外的复杂性,而且异步方案需要较大的修改现有代码架构。

  27. Thanks and keep up the good work!

  28. Your post is great and engaging, the content is very practical, and gets people’s attention. Thank you for sharing.

  29. I really appreciate this great post that you have provided us. I guarantee this will benefit most people and myself. thank you very much!

1 2

Leave a Comment

Your email address will not be published. Required fields are marked *