缓存与数据库一致性

十年热恋 提交于 2019-11-27 00:24:49

1.使用缓存的场景

缓存是提高系统读性能的常用技术,尤其对于读多写少的应用场景,使用缓存可以极大的提高系统的性能

例子:查询用户的存款: select money from user where uid = YYY;

为了优化该查询功能,我们可以在缓存中建立uid->money的键值对。

减少数据库的查询压力。        

2. 读操作流程

目前数据库和缓存中都有存储数据,当读取数据的时候,流程如下。

1)先读取缓存是否存在数据(uid->money)。如果缓存中有数据返回结果。

2)如果缓存中没有数据,则从数据库中读取数据。

介绍一个概念:

      缓存命中率:缓存命中数/总缓存访问数。

3. 写操作流程

在介绍写操作流程之前,先讨论两个问题

问题一:淘汰缓存还是更新缓存?

淘汰缓存:数据只会写入数据库,不会写入缓存,只会把数据淘汰掉。

更新缓存:数据不但写入数据库,还会写入缓存。

问题二:先写缓存还是先写数据库?

由于对缓存的更新和数据库的更新无法保证事务性操作。一定涉及到哪个先做,哪个后做的问题,我们的原则是采取对业务影响小的策略。下面是四种不同的组合策略

 

由此可见第四种策略的影响最小,只会造成一次查询缓存miss而已。那么当查询缓存miss的时候,我们该怎么办?很简单,查询数据库,然后将数据库的内容更新到缓存中。可能有人会问第四种策略,如果一上来淘汰缓存就失败了怎么办,当然是直接返回即可,通知用户本次操作失败。

我们的结论是:先淘汰缓存,再写数据库。

4. 分布式环境下如何保证一致性

下面我们再简单回顾下”先淘汰缓存,再写数据库 ”策略的读写流程。

写流程:

1)先淘汰缓存

2)再写数据库

读流程:

1)先读缓存,如果数据命中则返回

2)如果数据未命中则读取数据库

3)将数据库读出来的数据写入缓存

4.1 不一致性的例子

我们的这种策略在串行执行的情况,保证一致性是没有问题的。但是在分布式环境下,数据的读写都是并发的,可能有多个服务对同一个数据进行读写,也就是说后发出来的请求有可能先完成。我们来举个例子

 

 

1:    发送了写请求A,A的第一步淘汰了cache(如上图中的1)

2:    A的第二步写数据库,发出修改请求(如上图中的2)

3:    发送了读请求B,B的第一步读取cache, 发现cache中是空的(如上图中的3)

4:    B的第二步读取数据库,发出读取请求,此时A的第二步写数据还没完成,读出了脏数据,并放入了cache(如上图中的4)。即后发出的请求4比先发出的请求2先完成了,读出了脏数据,脏数据又入了缓存,造成缓存与数据库中的数据不一致。

4.2解决思路

我们来仔细看一下上面的例子,其实问题就出在对同一数据读取/写入请求不是串行的,而是并发的。那么如何能做到对同一数据的读取/写入请求是串行的?只需要让”同一数据的访问通过同一条DB连接执行 ”就行。如何做到这一点?可以修改获取DB连接的方法CPool.DBConnection(), 修改为CPool.DBConnection(uuid)[返回uuid取模相关联的连接]。

等等,”CPool.DBConnection(uuid)”这个代码是运行在每个service上面的,这样只能保证每个service上面是同一条DB连接。如何解决这个问题?聪明如你,可以在应用层根据uuid取模,来获取相关的service。这样就能保证同一数据的请求消息,都会路由到同一个service。

 

5. 主从DB与cache如何保证一致性

在只有主库时,通过我们上面讲的”串行化”的思路可以解决缓存与数据库不一致的问题。但是在”主从同步,读写分离的数据库架构下”,有可能出现脏数据入缓存的情况,此时串行化方案不再适用了,下面我们来讨论一下这个问题。                                                                

5.1不一致的例子

 

 

1)  请求A发起了一个写操作,第一步淘汰了cache(如上图中的1)

2)请求A继续写数据库,写的是主库,写入最新数据(如上图中的2)

3)请求B发起了一个读操作,读cache, 此时 cache中是空的(如上图中的3)

4)请求B继续读数据库,读的是从库,此时恰巧主从同步还没有完成,读出来一个脏数据,然后脏数据入cache(如上图中的4)

5)最后数据库的主从同步完成了(如上图的5)

 

这种情况下,其实就是主从同步的时延期间,有读请求读从库导致的不一致。这个问题怎么优化呢?

5.2解决思路

假设主从同步的时延<1s, 那么旧数据就是在那1s的间隙中入缓存的,是不是可以在写请求完成后,再休眠1s, 再次淘汰缓存,就能将这1s内写入的脏数据再次淘汰掉呢?

Bingo, 当然是可以。

写请求的步骤如下:

1)先淘汰缓存

2)再写数据库

3)休眠1s, 再次淘汰缓存

 

这样的话保证一致性是没有问题的,但是所有的写请求都阻塞了1s, 大大降低了写请求的吞吐量, 这是不可接受的。其实我们不需要休眠1s,而是直接将”淘汰缓存的任务”交给一个异步的timer来处理。

多说一句,从架构的角度来看,其实我们可以将对缓存,数据库的操作独立出来,提供一个统一的服务接口,这样上层的service就不需要关注先操作缓存,还是先操作数据库等问题,我们的架构可以是这样的:

 

 

参考:

https://my.oschina.net/u/818912/blog/655703

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!