互联网一致性架构设计 -- DB和Cache一致性

互联网一致性架构设计 -- DB和Cache一致性

需求分析

下面两种情况会出现脏数据:

    单库情况下

    服务层的并发读写,缓存与数据库的操作交叉进行,这种情况虽然少见,但理论上是存在的,后发起的请求B在先发起的请求A中间完成了。


    1. 请求A发起一个写操作,第一步淘汰了cache,然后这个请求因为各种原因在服务层卡住了(进行大量的业务逻辑计算,例如计算了1秒钟),如上图步骤1

    2. 请求B发起一个读操作,读cache,cache miss,如上图步骤2

    3. 请求B继续读DB,读出来一个脏数据,然后脏数据入cache,如上图步骤3

    4. 请求A卡了很久后终于写数据库了,写入了最新的数据,如上图步骤4

 

    主从同步

    读写分离的情况下,读从库读到旧数据,这种情况请求A和请求B的时序是完全没有问题的,是主动同步的时延(假设延时1秒钟)中间有读请求读从库读到脏数据导致的不一致。



 

    1. 请求A发起一个写操作,第一步淘汰了cache,如上图步骤1

    2. 请求A写数据库了,写入了最新的数据,如上图步骤2

    3. 请求B发起一个读操作,读cache,cache miss,如上图步骤3

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

    5. 最后数据库的主从同步完成了,如上图步骤5

    不一致的原因

    1. 单库情况下,服务层在进行1s的逻辑计算过程中,可能读到旧数据入缓存

    2. 主从库+读写分离情况下,在1s钟主从同步延时过程中,可能读到旧数据入缓存

    建议:先淘汰缓存,再更新数据库

单库情况的优化

       自己重写数据库连接池,例如根据userId取模,得到唯一的数据库连接,如何做到“让同一个数据的访问串行化”,只需要“让同一个数据的访问通过同一条DB连接执行”就行。

       如何做到“让同一个数据的访问通过同一条DB连接执行”,只需要“在DB连接池层面稍微修改,按数据取连接即可”?

    获取DB连接的

    CPool.GetDBConnection()【返回任何一个可用DB连接】

    改为

    CPool.GetDBConnection(longid)【返回id取模相关联的DB连接】


        
 

    1. service的上游是多个业务应用,上游发起请求对同一个数据并发的进行读写操作,上例中并发进行了一个uid=1的余额修改(写)操作与uid=1的余额查询(读)操作。

    2. service的下游是数据库DB,假设只读写一个DB。

    3. 中间是服务层service,它又分为了这么几个部分

        (1) 最上层是任务队列

        (2) 中间是工作线程,每个工作线程完成实际的工作任务,典型的工作任务是通过数据库连接池读写数据库

        (3) 最下层是数据库连接池,所有的SQL语句都是通过数据库连接池发往数据库去执行的

    4. 当用uid=1写数据库时,正在使用数据库连接池中的连接1。

    5. 此时用uid度数据库,同样要使用连接1,这样读操作就会进行等待,要前面写数据完毕,释放数据库连接后才能读数据。

拓展

能否做到同一个数据的访问落在同一个服务上?

重新修改结构如下:

    获取Service连接的

    CPool.GetServiceConnection()【返回任何一个可用Service连接】

    改为

    CPool.GetServiceConnection(longid)【返回id取模相关联的Service连接】


    
 

    1. 业务应用的上游不确定是啥,可能是直接是http请求,可能也是一个服务的上游调用

   

    2. 业务应用的下游是多个服务service

    3. 中间是业务应用,它又分为了这么几个部分

        (1)最上层是任务队列【或许web-server例如tomcat帮你干了这个事情了】

        (2)中间是工作线程【或许web-server的工作线程或者cgi工作线程帮你干了线程分派这个事情了】,每个工作线程完成实际的业务任务,典型的工作任务是通过服务连接池进行RPC调用

        (3)最下层是服务连接池,所有的RPC调用都是通过服务连接池往下游服务去发包执行的

    4. 当请求Service层时,根据uid来取模,决定使用哪个Service的连接

主从同步情况下的优化

       既然旧数据就是在那1s的间隙中入缓存的,是不是可以在写请求完成后,再休眠1s,再次淘汰缓存,就能将这1s内写入的脏数据再次淘汰掉呢?

方法一

    1. 先淘汰缓存

    2. 再写数据库(这两步和原来一样)

    3. 休眠1秒,再次淘汰缓存

缺点:所有的写请求都阻塞了1秒,大大降低了写请求的吞吐量,增长了处理时间,业务上是接受不了的。

方法二

    1. 先淘汰缓存

    2. 再写数据库(这两步和原来一样)

    3. 不再休眠1s,而是往消息总线esb发送一个消息,发送完成之后马上就能返回


    
 

缺点:需要业务线的写操作增加一个步骤,这就是我们所谓的代码入侵

方法三

       业务线的代码就不需要动了,新增一个线下的读binlog的异步淘汰模块,读取到binlog中的数据,异步的淘汰缓存。


     
 

总结

单库

       由于数据库层面的读写并发,引发的数据库与缓存数据不一致的问题(本质是后发生的读请求先返回了),可能通过两个小的改动解决:

    1. 修改服务Service连接池,id取模选取服务连接,能够保证同一个数据的读写都落在同一个后端服务上

    2. 修改数据库DB连接池,id取模选取DB连接,能够保证同一个数据的读写在数据库层面是串行的

主从数据库

       在“异常时序”或者“读从库”导致脏数据入缓存时,可以用二次异步淘汰的“缓存双淘汰”法来解决缓存与数据库中数据不一致的问题,具体实施至少有三种方案:

    1. timer异步淘汰(本文没有细讲,本质就是起个线程专门异步二次淘汰缓存)

    2. 总线异步淘汰

    3. 读binlog异步淘汰

猜你喜欢

转载自youyu4.iteye.com/blog/2393588