先更新缓存还是先更新数据库
一、提前阅读
讨论这个问题之前可以先看下缓存模式(Cache Aside、Read Through、Write Through、Write Behind)这篇文章。
二、先更新缓存,再更新数据库
1、考虑并发操作:线程A写,线程B读
- 1、线程A发起一个写操作,第一步delete cache
- 2、此时线程B发起一个读操作,cache miss
- 3、线程B继续读数据库,读出来一个老数据
- 4、然后老数据入cache
- 5、线程A写入了最新的数据
这样以后每次从缓存中读到的都是老数据,造成数据不一致。
既然这种情况下先删除缓存会有数据不一致的情况,那我们来试试第一步不删除缓存而是直接更新缓存试试看。
2、考虑并发操作:线程A写,线程B写
- 1、线程A发起一个写操作,第一步set cache
- 2、线程B发起一个写操作,第一步set cache
- 3、线程B写入数据到数据库
- 4、线程A写入数据到数据库
这样以后每次从缓存中读到的都是线程B设置的数据,但数据库中存储的是线程A写入的数据,导致数据不一致。
3、小结
可看到先操作缓存不论是先删除缓存还是先更新缓存都会发生数据不一致的情况,所以不推荐这两种做法。
从理论上说,只要我们设置了缓存的过期时间,我们就能保证缓存和数据库的数据最终是一致的。因为只要缓存数据过期了,就会被删除。随后读的时候,因为缓存里没有,就得去查数据库的数据,然后将查出来的数据写入到缓存中。除了设置过期时间,我们还需要做更多的措施来尽量避免数据库与缓存处于不一致的情况发生。但是设置过期时间是基本操作,只要数据不是静态数据,就应该给缓存中的此类数据设置过期时间,它并不是解决“先更新缓存,再更新数据库”造成数据不一致问题的方法。
三、先更新数据库,再更新缓存
1、考虑并发操作:线程A写,线程B读
- 1、线程A发起一个写操作,第一步写入数据到数据库
- 2、线程A第二步delete cache
- 3、线程B发起一个读操作,cache miss
- 4、线程B从数据库获取最新数据
- 5、线程B同时set cache
一个是读操作,一个是写操作的并发,首先没有了文章开始删除cache数据的操作了,而是先更新了数据库中的数据,此时缓存依然有效,所以此时读操作查到的是没有更新的旧数据,但是更新操作马上让缓存失效了,后续的查询操作再把数据从数据库中查出来。而不会像文章开头的那个逻辑产生的问题,即后续的查询操作一直都在读旧的数据。
这是标准的Cache Aside Pattern
:
包括Facebook的论文《Scaling Memcache at Facebook》也使用了这个策略。为什么不是写完数据库后更新缓存?你可以看一下Quora上的这个问答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》,主要是怕两个并发的写操作导致脏数据,这个问题我们下面会说到。
特殊情况:
在高并发的场景下,可能会出现数据库与缓存数据不一致的的情况,考虑下面情形:
- 1、线程A发起一个写操作,还未操作数据库
- 2、缓存刚好失效
- 3、线程B查询数据库,得一个旧值
- 4、线程A将新值写入数据库
- 5、线程A删除缓存
- 6、线程B将查到的旧值写入缓存
但是这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必须在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率其实并不大。
2、考虑并发操作:线程A写,线程B写,线程C读
- 1、线程A发起一个写操作,第一步写入数据到数据库
- 2、线程B发起一个写操作,第一步写入数据到数据库
- 3、线程B第二步delete cache
- 4、线程C发起一个读操作,cache miss
- 5、线程C从数据库获取最新数据
- 6、线程C同时set cache
- 7、线程A第二步delete cache
该情况下由于线程A、B最初都把数据写入了数据库,接着都有delete cache,此时如果有线程C来读数据,你会发现不管线程C的动作做任意顺序穿插在A、B动作之间,最后查询数据最差也就是在线程A、B删除cache之前获取到了旧数据,其余都会获取到新数据,并不会影响后来的请求获取到新数据。
为什么最后是把缓存的数据删掉,而不是把更新的数据写到缓存里。这么做引发的问题是,如果A、B两个线程同时做数据更新,A先更新了数据库,B后更新数据库,则此时数据库里存的是B的数据。而更新缓存的时候,是B先更新了缓存,而A后更新了缓存,则缓存里是A的数据。这样缓存和数据库的数据会发生不一致。
3、小结
- 先操作数据库再删除缓存能有让人可接受的结果,所以最推荐这种做法。
- 先操作缓存再更新数据库可能造成数据不一致的场景,不推荐这种做法。
四、异常情况
上面的讨论与对比都是在更新缓存
和更新数据库
这两步操作都成功的情况下叙述的。当然系统正常运行时的操作基本上都是成功的,那么如果两步操作有其中一步操作失败了呢?(以先操作数据库再操作缓存举例)
- 第一步失败:这种情况很简单,不会影响第二步操作,也不会影响数据一致性,直接抛异常出去就好了。
- 第二步失败:
- 将需要删除的缓存key发送到消息队列中
- 另起终端消费队列消息,获得需要删除的缓存key
- 设置重试删除操作,超过最大重试次数(比如5次)后将消息转入死信队列并报警给运维人员