• 缓存与数据库一致性


    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

  • 相关阅读:
    《C# to IL》第一章 IL入门
    multiple users to one ec2 instance setup
    Route53 health check与 Cloudwatch alarm 没法绑定
    rsync aws ec2 pem
    通过jvm 查看死锁
    wait, notify 使用清晰讲解
    for aws associate exam
    docker 容器不能联网
    本地运行aws lambda credential 配置 (missing credential config error)
    Cannot connect to the Docker daemon. Is 'docker daemon' running on this host?
  • 原文地址:https://www.cnblogs.com/winner-0715/p/7451664.html
Copyright © 2020-2023  润新知