• 缓存与数据库一致性


    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

  • 相关阅读:
    windows下面Nginx日志切割
    C#通过DocX创建word
    leetcode 189 Rotate Array
    leetcode 172 Factorial Trailing Zeroes
    leetcode 169 Majority Element 冰山查询
    leetcode 165 Compare Version Numbers
    leetcode 160 Intersection of Two Linked Lists
    【windows-》linux】SCP
    【设计】B端和C端区别
    【Flask】部署
  • 原文地址:https://www.cnblogs.com/winner-0715/p/7451664.html
Copyright © 2020-2023  润新知