缓存及数据库数据不一致
cache aside pattern
最经典的缓存+数据库读写的模式
- 读的时候先读缓存,缓存没有的话,那么就读数据库,然后取出数据放入缓存中
- 更新的时候,先更新数据库,然后删除缓存
为什么删除缓存,而不是更新缓存
原因很简单,因为缓存有的时候,不简单是数据库中直接取出来的值
比如可能更新了某个标的一个字段,然后其对应的缓存,是需要查询另外两个表的数据,并进行运算,才能计算出缓存最新的值
更新缓存的代价是很高的
如果频繁地修改一个缓存设计的多个表,那么这个缓存会被频繁的更新
但是问题就在于,这个缓存到底会不会被频繁的访问到?
举个例子,一个缓存涉及的标的字段,在1分钟内就修改了20次,那么缓存更新20次,但是这个缓存在1分钟内就被读取了1次
实际上如果只是删除缓存的话,一分钟内,这个缓存不过就被重新计算一次而已,开销大幅度降低
其实删除缓存,而不是更新缓存,就是一个lazy计算的思想,不要每次都重新做复杂的计算,不管他会不会用到,而是让他到需要被使用的时候再重新计算
具体情况的Redis数据不一致操作
来看一个简单的库存场景
在业务开发中,库存服务是一种常见的实时性比较高的数据缓存
库存数据可能会被修改,每次是修改都要去更新这个缓存数据,每次库存的数量,再缓存中一旦过期,或者被清理掉了,前端的nginx服务都会发送请求给库存服务,去获取相应的数据库存这一块,写数据的时候,直接更新redis 缓存
实际上这里没有那么简单,里面涉及到一个问题,数据库与缓存双写,数据不一致的情况
最初级的缓存不一致的问题以及解决方案
问题:先修改数据库,再删除缓存,如果删除缓存失败了,那么导致数据库中的数据是新数据,然后缓存中的数据是旧数据,数据出现不一致
解决思路:先删除缓存,再修改数据库,如果修改数据库失败了,那么数据库中的数据仍然是旧数据,缓存中是空的,那么数据不会不一致,因为读的时候缓存没有,则读数据库中旧数据,然后更新到数据库中
比较复杂的数据不一致问题分析
数据发生了变更,然后先删除缓存,然后就要去修改数据库,此时还没修改,一个请求过来,发电缓存空了,去查数据库,查到了修改前的旧数据,放到了缓存中
数据变更的程序完成了数据库的修改
这个情况下缓存数据与数据库数据就不一致了
所以上述问题只有可能在高并发场景下才会出现这种读写不一致的问题
解决方案:
数据库与缓存更新与读取操作进行异步串行化
更新数据的时候,根据数据的唯一标识,
将操作路由之后,发送到了一个jvm内部的队列中
读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个Jvm内部的队列中
一个队列对应一个工作线程
每个工作线程串行拿到对应的操作,然后一条一条的执行
这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新
此时如果一个读请求过来,读到了空的缓存,那么可以现将缓存更新的请求发送到队列里,此时,会在队列中积压,然后同步等待缓存更新完成
这里有一个优化点,一个队列中, 其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新操作进去了,直接等待前面的更新操作请求完成即可
待那个队列对应的工作线程完成了上一个操作的数据库修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中
如果请求还在等待时间范围内,不断轮训发现可以取到值了,那么就直接返回,如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值
该解决方案要注意的问题
- 读请求长时间阻塞
由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须要在超时时间范围内返回
该解决方案最大的风险在于,可能数据更新的很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,然后导致大量的请求直接走数据库
务必通过一些模拟真实的测试,查看更新数据的频率是怎样的
另外一点,因为一个队列中,可能会挤压针对多个数据项的更新操作因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作
大部分的情况下,应该是这样,大量的读请求过来,都是直接走的缓存取到数据的
少量情况下,可能遇到读跟数据更新冲突的情况,如上所述,那么此时更新操作如果先进入队列,之后可能会瞬间来了对这个数据大量的读请求,但是因为做了去重的优化所以也就一个更新缓存的操作跟在它后面
等数据更新完了,读请求出发的缓存更新操作也完成了,然后临时等待的读请求全部可以读到缓存中的数据 - 读请求并发量很高
这里也需要做好压力测试,确保恰巧碰上上述情况的时候,还有个风险,就是突然间大量读请求会在几十毫秒的延时hang再服务上,看服务能不能扛得住,需要多少机器才能抗住最大的极限情况的峰值
但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了然后那些数据对应的读请求过来,并发两应该也不会特别大
按照1:99的比例计算读和写的请求,每秒5万的qps,可能只有500次更新操作 - 多服务实例部署的请求路由
可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的时候,都通过nginx服务器路由到相同的服务实例上