缓存是个好东西,但在项目中引入它通常也会存在代价,这个世界上并没有只有好处的银弹,只有利弊之间的取舍。—— 秃我
本文是技术亮点第二篇,第一篇被公司截胡了,发表在了公司号上。。
第一篇:不可思议!亿级数据竟然如此轻松同步至ES! ,墙裂推荐!!!
一、这是背景
很显然,在电商业务中,商品服务的定位为高性能和高并发、高可用。
在秃我之前负责的商品系统中,平均 qps 在 3w+,大促的时候可以到 30w+,而耗时仅仅在 1.5ms 左右
商品服务QPS,商品服务性能图
见下图:
二、引入缓存抗并发
这么大的 qps与性能要求,数据库显然捉襟见肘,因此商品系统引入 Redis 集群来提高服务的并发能力与性能,但数据存储在数据库和 Redis 两个存储介质上会存在一致性的问题,本篇文章就是来阐述下解决该问题的方案
在介绍方案前,先了解下我们的商品系统是如何使用缓存的?
商品服务,缓存服务,读删命令,如下图
商品读:
商品写:
本文主要阐述缓存一致性问题
三、目标是什么
我们可以把视野放远些,以巨人的角度俯瞰整个优化的“战场”,我们到底需要什么?缓存一致这个说法过于太大,我们需要稍微细化些,总结如下:
和数据库不一致的缓存数据尽可能快的删除,不要影响线上业务,比如卖家提高了商品的价格,但是商详页展示的依然是原价,那么买家在购买该商品的时候,发现下单后涨价了,体验不好,且容易导致投诉
和数据库一致的缓存,尽可能长时间的保留,以提高服务性能、降低DB压力,因为我们引入缓存的目的就是为了提高系统性能及并发能力
总结一下:对于不一致的数据,尽早的删除。对于一致的数据,尽可能的保留。
四、缓存不一致的问题到底出在哪?
第一点: 网络抖动导致的缓存删除失败,商品服务和缓存服务之间通过网络进行交互,既然是网络,就存在抖动导致操作超时的可能性。
第二点: 多线程并发操作导致的缓存数据覆盖问题。
如下图,假设此时Redis中并没有A商品的缓存。(有可能是新创建的商品,也有可能是刚更新完删除了缓存)
- 首先来个查询请求,发现Redis没缓存,则去DB中读取数据
- 接着来个更新请求,更新A商品的DB后删除缓存
- 查询请求的塞缓存逻辑执行,把A商品更新前的数据塞到了缓存里面。。
至此出现了缓存不一致的现象
五、解决方案来了!
5.1 延时双删
延时就是过一会。
双删就是再删一次。
加起来就是:过一会再删一次缓存。。。
5.2 延时双删方案能否解决问题?
在动手之前先调研下路子能不能走通?
第一个问题:网络抖动导致缓存删除失败
网络抖动基本都是偶现的,第一次删除缓存失败,第二次删除基本就可以成功,当然,依赖概率的事情我们不做,所以本次双删方案是有重试策略的,所以问题一可以解决。
第二个问题:多线程并发操作导致缓存数据覆盖。
只要在不一致的数据塞到缓存之后再删除一次,就可以解决问题,所以双删的延迟时间的选择至关重要!
下面唠唠延时双删的一些关键逻辑
5.3 延时双删的逻辑写在哪?
延时双删的逻辑可以写在数据更新等业务逻辑中,但不建议这么做。
为什么? 业务逻辑是相当核心,它要纯粹。黄金要不要纯?要的!钻石要不要纯?要的!
而延时双删是个辅助逻辑,摘出来放到一个单独的服务中就挺好。
5.4 如何触发双删?
在商品更新的时候同步调用双删接口?这样操作同样和商品服务强耦合了。
因此,选择监听商品Binlog的MQ和商品核心服务解耦开是相当巴适的。
5.5 如何选择延时时间?
对于网络抖动导致的删除失败问题,只要保证能删除即可,对于延时时间,即越短越好,这样数据不一致的时间就越短。
而对于并发操作导致的缓存不一致问题,延时时间的选择就不能这么随意,如下图,最优删除时间如图,但这个时间我们没法评估,因此我们选择次优时间,即一个“读周期”。
何为读周期?即一次查询的全耗时
只要延时的时间大于“读周期”即可解决不一致的问题,“读周期”如何获取呢,在服务调用管理平台上,取max(读接口时间)即可,或TP999的耗时。
5.6 延时双删是必须要删吗?
嘎嘎一顿乱删,商品服务蹦了,这不合适,也不礼貌。
所以,非常有必要探讨下,延时双删,是不是一定要删?
延迟双删虽可解决缓存数据不一致的问题,但也引入了一个新的问题,就是较为频繁的删缓存可能带来的性能问题。因为商品服务的特点是读多写少,在删除缓存到重新设置到缓存期间这段时间,查询都是直接打到数据库的,对数据库造成了更大的压力。如何减少不必要的缓存删除操作?
商品有很多字段,只有部分字段需要保证较强的一致性,比如价格、状态等字段,因此在接收到MQ时,只对这些字段有变更的消息进行缓存一致性的处理
价格、状态这些字段变更就一定要删除缓存吗?其实也不一定,因为缓存不一致的情况还是少的,所以需要将数据从数据库查询出来和缓存中的数据进行对比,若一致,也就不需要删除了。
在下图给出的数据中,在450266次更新的操作中,只有79次出现了数据不一致,需要删除缓存。因此经过对比,可以极大的降低删除缓存的次数
但有的同学会问:每次更新都要从数据库读数据和缓存对比,这样成本不也高了吗?
是高了一些,但需要知道,商品服务的特点是读多写少,若直接删除缓存,大量并发的请求会直接打到数据库,此时成本会更高。
所以在特定字段数据更新的时候,再查一次数据库中的数据和缓存对比,成本会远远低于直接删缓存
5.7 延时组件的选择
延时可以选择延时MQ,但我们本次选择了时间轮做任务的延时调度,为什么?
- 因为延时MQ最小延迟时间为秒,而时间轮的精度随意,而且配合动态配置可以随时调整延迟时间,更灵活
- 本质上此操作属于非核心可降级操作,因此需要尽量少的影响线上的业务,时间轮可以限制提交的任务数,以此把负载维持在一个限度之内
- 时间轮的效率很高(o1的时间复杂度),采用比较少的空间来换时间,值得。
5.8 如何重试?
在双删逻辑执行失败时,可以将该商品ID重新扔进时间轮,延时一段时间后,继续重试