一.最经典的数据库加缓存的双写双删模式
1.1 Cache Aside Pattern概念以及读写逻辑
(1)读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应
(2)更新的时候,先删除缓存,然后再更新数据库
1.22、为什么是删除缓存,而不是更新缓存呢?
原因很简单,很多时候,复杂点的缓存的场景,因为缓存有的时候
,不简单
是数据库中直接取出来的值,可能需要比较复杂的计算
,甚至进行很多网络请求以及DB请求(比如我们有个缓存就是查微信的公共库以及我们自己的私有库联合组成一个缓存),这种更新缓存的代价很高
的,但是呢我们更新完了缓存这个缓存,这个缓存也不一定立马就有人用,可能我更新了很多次数据库更新了很多次
缓存都没人访问
,这就导致了我服务器做了很多无用的计算
二. 高并发场景下的缓存+数据库双写不一致问题分析与解决方案设计
这里围绕和结合实时性较高的库存服务
,把数据库与缓存双写不一致问题以及其解决方案,给大家讲解一下.
我们有两个操作顺序可以选择,其中都存在各种双鞋不一致情况,具体讨论讨论
- 更新数据先删除缓存,再更新数据库
- 更新数据先更新数据库,再删除缓存
2.1先删除缓存再更新数据库方式
2.1.1 上面说的
最经典的方式有什么缓存不一致的问题?解决方案是什么?
问题:如果我们的方案是先修改数据库库存,再删除缓存,那么如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致
解决思路:
先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致,因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中
注意这里无并发读写没问题,但是并发情况下依然会有问题
,我们继续往下看
2..22 上面第一个解决方案在并发下
还是有问题
如果先删除缓存再删除数据库可能存在这种情况
- A服务删除缓存成功
- B请求来了读旧数据库存
- A更新新的库存成功
这样依然是数据库和缓存的库存不一致了
2.3 如何允许短暂的不一致,我们可以用什么思路来做?
2.3.1 基于MQ的分布式事务实现最终一致性
2.3.2 基于binlog监听实现
2.3.3 延迟双删 (比上面稍微优点的一点在于这里不需要印入MQ)
延时双删
延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再 Sleep 一段时间,然后再次删除缓存。
Sleep 的时间要对业务读写缓存的时间做出评估,Sleep 时间大于读写缓存的时间即可。
流程如下:
线程1删除缓存,然后去更新数据库。
线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存。
线程1,根据估算的时间,Sleep,由于 Sleep 的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除。
三 高并发下又要求强一致性
的解决思路:将统一商品的请求进行串行化
2.3上面高并发的场景下,该解决方案要注意的问题
2.3.1读请求长时阻塞
由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回
该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面
,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库
务必通过一些模拟真实的测试,看看更新数据的频繁是怎样的
因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作
如果一个内存队列里居然会挤压100个商品的库存修改操作,每隔库存修改操作要耗费10ms区完成,那么最后一个商品的读请求,可能等待10 * 100 = 1000ms = 1s后,才能得到数据
这个时候就导致读请求的长时阻塞
一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会hang多少时间,如果读请求在200ms返回,如果你计算过后,哪怕是最繁忙的时候,积压10个更新操作,最多等待200ms,那还可以的
如果一个内存队列可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少
其实根据之前的项目经验,一般来说数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的
针对读高并发,读缓存架构的项目,一般写请求相对读来说,是非常非常少的,每秒的QPS能到几百就不错了
一秒,500的写操作,5份,每200ms,就100个写操作
单机器,20个内存队列,每个内存队列,可能就积压5个写操作,每个写操作性能测试后,一般在20ms左右就完成
那么针对每个内存队列中的数据的读请求,也就最多hang一会儿,200ms以内肯定能返回了
写QPS扩大10倍,但是经过刚才的测算,就知道,单机支撑写QPS几百没问题,那么就扩容机器,扩容10倍的机器,10台机器,每个机器20个队列,200个队列
大部分的情况下,应该是这样的,大量的读请求过来,都是直接走缓存取到数据的
少量情况下,可能遇到读跟数据更新冲突的情况,如上所述,那么此时更新操作如果先入队列,之后可能会瞬间来了对这个数据大量的读请求,但是因为做了去重的优化,所以也就一个更新缓存的操作跟在它后面
等数据更新完了,读请求触发的缓存更新操作也完成,然后临时等待的读请求全部可以读到缓存中的数据
3.2 读请求并发量过高
必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时hang在服务上,看服务能不能抗的住,需要多少机器才能抗住最大的极限情况的峰值
但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大
按99:1的比例计算读和写的请求,每秒5万的读QPS,可能只有500次更新操作
如果一秒有500的写QPS,那么要测算好,可能写操作影响的数据有500条,这500条数据在缓存中失效后,可能导致多少读请求,发送读请求到库存服务来,要求更新缓存,这些读请求每个会hang多长时间?
如果我们写读比例是1:20,每秒更新500条数据,这500秒数据对应的读请求,会有20 * 500 = 1万,1万个读请求全部hang在库存服务上,就死定了
3.3 多服务实例部署的请求路由一致性问题
可能这个服务部署了多个实例,那么必须保证,同一个商品id(我们路由到queue的规则),执行数据更新库存操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上(这个要改nginx的hash路由规则)
如果一个商品的库存更新操作在A服务器的queue里,他的读路由到另一个服务器的队列里去了,这他娘的还串行化个屁。
3.4 热点商品的路由问题,导致请求的倾斜
万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能造成某台机器的压力过大
其实只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些,需要注意。
3.5 .串行化缺点
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案:读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况。
但是呢:串行化之后,就会导致系统的吞吐量会大幅度的降低
,用比正常情况下多几倍的机器
去支撑线上的请求。