背景
使用Reids作为缓存的原因:
在高并发场景下,传统关系型数据库的并发能力相对比较薄弱(QPS不能太大);
使用Redis做一个缓存。让用户请求先打到Redis上而不是直接打到数据库上。
但是如果出现数据更新操作:数据库与缓存更新,就会出现缓存(Redis)和数据库(MySQL)之间的数据一致性问题。
非读写分离架构:延时双删
先更新数据库,再更新缓存,为什么不可行?
线程安全角度
同时有请求A、B进行更新操作
执行顺序如下:
线程A更新了数据库
线程B更新了数据库
线程B更新了缓存
线程A更新了缓存
缓存和数据库出现了不一致
业务角度
如果某个业务场景是写多读少,就会导致缓存并未被读取就会被频繁的更新,极大的浪费了服务器的性能。
因为数据库的值,并不是直接刷入缓存,有的业务需要经过一系列复杂的计算再写入缓存。
先删除缓存,再更新数据库,为什么不可行?
线程安全角度
请求A进行写操作,先删除缓存
请求B查询发现缓存不存在
请求B去数据库查询得到旧值
请求B将旧值写入缓存
请求A更新数据库
此时数据库中的值是新值,缓存的值是旧值,就发生了数据不一致问题
延时双删
线程 A:
当应用程序需要更新数据时,首先将数据更新到数据库
A 线程向 Redis 发送删除缓存的指令,将缓存标记为过期
A 线程等待一定的时间窗口(通常是几十ms~几百ms),让 B 线程有足够的时间去访问缓存
线程 B:
在时间窗口内,当有请求访问过期的缓存数据时,B 线程发现缓存已过期,并触发缓存更新的操作
B 线程从数据库中获取到最新数据,并将其存储到缓存中
B 线程返回更新后的缓存数据
线程 A(续):
在时间窗口结束后,A 线程再次向 Redis 发送删除缓存的指令,彻底删除缓存数据
如果在时间窗口内没有请求访问到过期的缓存数据,A 线程会删除已标记为过期的缓存数据
通过上述流程
A 线程负责标记缓存过期并等待一段时间,给 B 线程足够的时间去访问缓存并更新
B 线程则负责处理实际的缓存更新操作
这样即使在缓存更新期间有请求访问过期的缓存数据,也能获取到最新的数据,避免脏读
注意
具体的时间窗口大小和线程的实现方式可以根据实际需求和系统性能进行调整。
同时,对于高并发环境,还需要考虑线程安全和并发控制的实现,以确保操作的正确性和性能。
读写分离架构(有专门的读服务、专门的写服务,写主,读从主)
可以采用先更新数据库,再删除缓存,配合上重试机制
问题
两个请求:请求A进行更新操作,请求B进行查询操作
请求A进行写操作,删除缓存
请求A将数据写入数据库,
请求B查询缓存发现,缓存没有值
请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
请求B将旧值写入缓存
数据库完成主从同步,从库变为新值
仍然会出现缓存与数据库数据不一致问题
延时双删的问题
延时时间需要在主从同步的延时时间基础上,加几百ms
双删失败
如果第二次删除缓存失败,仍然会出现缓存与数据库数据不一致的问题
同样还是有两个请求请求A进行更新操作,请求B进行查询操作(单库)
请求A进行写操作,删除缓存
请求B查询发现缓存不存在
请求B去数据库查询得到旧值
请求B将旧值写入缓存
请求A将新值写入数据库
请求A试图去删除请求B写入的缓存值,结果失败了
删除失败的重试补偿机制
先更新数据库,再删除缓存
同样存在并发问题,但是发生几率很低
两个请求:请求A进行更新操作,请求B进行查询操作(单库)
缓存刚好失效
请求A查询数据库,得一个旧值
请求B将新值写入数据库
请求B删除缓存
请求A将查到的旧值写入缓存
该情况发生的必要条件就是请求B写数据库的操作比请求A读数据库的操作耗时更短,才能使请求B先删除缓存
但是通常来说数据库的读操作是远远快于写操作的,所以这种并发问题很难发生。
如果在极端情况下,这种并发问题仍然发生了
给缓存设置一定的有效时间
异步延时双删策略
另起一个线程,异步删除,保证读请求完成以后,再进行删除操作
重试机制
与先删除缓存,再更新数据一样,如果删除缓存失败,那么仍然会出现数据不一致问题
选择靠谱的重试机制,比如利用消息队列进行删除的补偿
方案一:
更新数据库数据;
缓存因为种种问题删除失败
将需要删除的key发送至消息队列
自己消费消息,获得需要删除的key
继续重试删除操作,直到成功
缺点
对业务线代码造成大量的侵入,需要在业务代码中额外添加生成消息和消费消息的功能
业务代码变得不再专注于业务需求。
改进:
启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据
在应用程序中,另起一段程序(避免业务侵入),获得这个订阅程序传来的信息,进行删除缓存操作
方案二
更新数据库数据
数据库会将操作信息写入binlog日志当中
订阅程序提取出所需要的数据以及key
另起一段非业务代码,获得该信息
尝试删除缓存操作,发现删除失败
将这些信息发送至消息队列
重新从消息队列中获得该数据,重试操作订阅binlog程序在mysql中有现成的中间件叫canal
可以完成订阅binlog日志的功能
附录
MySQL的查询QPS主要取决于硬件性能以及应用的查询优化,通常在千到万的范围
但在特定的配置下可以达到几万到十几万。
通常来说,对于大多数应用场景而言,MySQL的QPS在2000-3000就已经比较高了。
过高的QPS可能会对服务器性能产生负面影响,如CPU和I/O压力过大。
因此,最佳的QPS应根据实际的硬件配置和应用需求来定。
通过对查询的优化,如合理的索引设计、合理的查询设计,也能够有效提高QPS
通过使用缓存、读写分离、分库分表等方式,也能显著提高系统的并发处理能力,从而提高QPS
词汇
写入失败重试直到成功,称之为:删除补偿
学习文档
https://mp.weixin.qq.com/s?__biz=Mzg5MjE0MjE3Mw==&mid=2247488095&idx=1&sn=c4f50e3dbfd381c3f9c6948b973f8063&chksm=cfc3c56df8b44c7be1a7284a0f6b9255274fb50e3cefcf98e7e6897b1df0f8080223012a312b#rd