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