拿来吧你,30W+ QPS的商品系统缓存一致性解决方案

1,263 阅读7分钟

缓存是个好东西,但在项目中引入它通常也会存在代价,这个世界上并没有只有好处的银弹,只有利弊之间的取舍。—— 秃我

本文是技术亮点第二篇,第一篇被公司截胡了,发表在了公司号上。。

第一篇:不可思议!亿级数据竟然如此轻松同步至ES! ,墙裂推荐!!!

一、这是背景

很显然,在电商业务中,商品服务的定位为高性能和高并发、高可用。

在秃我之前负责的商品系统中,平均 qps 在 3w+,大促的时候可以到 30w+,而耗时仅仅在 1.5ms 左右

商品服务QPS,商品服务性能图

见下图:

image.png

image.png

二、引入缓存抗并发

这么大的 qps与性能要求,数据库显然捉襟见肘,因此商品系统引入 Redis 集群来提高服务的并发能力与性能,但数据存储在数据库和 Redis 两个存储介质上会存在一致性的问题,本篇文章就是来阐述下解决该问题的方案

在介绍方案前,先了解下我们的商品系统是如何使用缓存的?

商品服务,缓存服务,读删命令,如下图

商品读:

image.png

商品写:

image.png

本文主要阐述缓存一致性问题

三、目标是什么

我们可以把视野放远些,以巨人的角度俯瞰整个优化的“战场”,我们到底需要什么?缓存一致这个说法过于太大,我们需要稍微细化些,总结如下:

和数据库不一致的缓存数据尽可能快的删除,不要影响线上业务,比如卖家提高了商品的价格,但是商详页展示的依然是原价,那么买家在购买该商品的时候,发现下单后涨价了,体验不好,且容易导致投诉

和数据库一致的缓存,尽可能长时间的保留,以提高服务性能、降低DB压力,因为我们引入缓存的目的就是为了提高系统性能及并发能力

总结一下:对于不一致的数据,尽早的删除。对于一致的数据,尽可能的保留。

四、缓存不一致的问题到底出在哪?

第一点: 网络抖动导致的缓存删除失败,商品服务和缓存服务之间通过网络进行交互,既然是网络,就存在抖动导致操作超时的可能性。

第二点: 多线程并发操作导致的缓存数据覆盖问题。

如下图,假设此时Redis中并没有A商品的缓存。(有可能是新创建的商品,也有可能是刚更新完删除了缓存)

  1. 首先来个查询请求,发现Redis没缓存,则去DB中读取数据
  2. 接着来个更新请求,更新A商品的DB后删除缓存
  3. 查询请求的塞缓存逻辑执行,把A商品更新前的数据塞到了缓存里面。。

至此出现了缓存不一致的现象

image.png

五、解决方案来了!

5.1 延时双删

延时就是过一会。

双删就是再删一次。

加起来就是:过一会再删一次缓存。。。

5.2 延时双删方案能否解决问题?

在动手之前先调研下路子能不能走通?

第一个问题:网络抖动导致缓存删除失败

网络抖动基本都是偶现的,第一次删除缓存失败,第二次删除基本就可以成功,当然,依赖概率的事情我们不做,所以本次双删方案是有重试策略的,所以问题一可以解决。

第二个问题:多线程并发操作导致缓存数据覆盖。

只要在不一致的数据塞到缓存之后再删除一次,就可以解决问题,所以双删的延迟时间的选择至关重要!

下面唠唠延时双删的一些关键逻辑

5.3 延时双删的逻辑写在哪?

延时双删的逻辑可以写在数据更新等业务逻辑中,但不建议这么做。

为什么? 业务逻辑是相当核心,它要纯粹。黄金要不要纯?要的!钻石要不要纯?要的!

而延时双删是个辅助逻辑,摘出来放到一个单独的服务中就挺好。

5.4 如何触发双删?

在商品更新的时候同步调用双删接口?这样操作同样和商品服务强耦合了。

因此,选择监听商品Binlog的MQ和商品核心服务解耦开是相当巴适的。

image.png

5.5 如何选择延时时间?

对于网络抖动导致的删除失败问题,只要保证能删除即可,对于延时时间,即越短越好,这样数据不一致的时间就越短。

而对于并发操作导致的缓存不一致问题,延时时间的选择就不能这么随意,如下图,最优删除时间如图,但这个时间我们没法评估,因此我们选择次优时间,即一个“读周期”。

何为读周期?即一次查询的全耗时

只要延时的时间大于“读周期”即可解决不一致的问题,“读周期”如何获取呢,在服务调用管理平台上,取max(读接口时间)即可,或TP999的耗时。

image.png

5.6 延时双删是必须要删吗?

嘎嘎一顿乱删,商品服务蹦了,这不合适,也不礼貌。

所以,非常有必要探讨下,延时双删,是不是一定要删?

延迟双删虽可解决缓存数据不一致的问题,但也引入了一个新的问题,就是较为频繁的删缓存可能带来的性能问题。因为商品服务的特点是读多写少,在删除缓存到重新设置到缓存期间这段时间,查询都是直接打到数据库的,对数据库造成了更大的压力。如何减少不必要的缓存删除操作?

商品有很多字段,只有部分字段需要保证较强的一致性,比如价格、状态等字段,因此在接收到MQ时,只对这些字段有变更的消息进行缓存一致性的处理

价格、状态这些字段变更就一定要删除缓存吗?其实也不一定,因为缓存不一致的情况还是少的,所以需要将数据从数据库查询出来和缓存中的数据进行对比,若一致,也就不需要删除了。

在下图给出的数据中,在450266次更新的操作中,只有79次出现了数据不一致,需要删除缓存。因此经过对比,可以极大的降低删除缓存的次数

image.png

但有的同学会问:每次更新都要从数据库读数据和缓存对比,这样成本不也高了吗?

是高了一些,但需要知道,商品服务的特点是读多写少,若直接删除缓存,大量并发的请求会直接打到数据库,此时成本会更高。

所以在特定字段数据更新的时候,再查一次数据库中的数据和缓存对比,成本会远远低于直接删缓存

5.7 延时组件的选择

延时可以选择延时MQ,但我们本次选择了时间轮做任务的延时调度,为什么?

  1. 因为延时MQ最小延迟时间为秒,而时间轮的精度随意,而且配合动态配置可以随时调整延迟时间,更灵活
  2. 本质上此操作属于非核心可降级操作,因此需要尽量少的影响线上的业务,时间轮可以限制提交的任务数,以此把负载维持在一个限度之内
  3. 时间轮的效率很高(o1的时间复杂度),采用比较少的空间来换时间,值得。

5.8 如何重试?

在双删逻辑执行失败时,可以将该商品ID重新扔进时间轮,延时一段时间后,继续重试

六、最终方案

image.png

七、参考资料

时间轮 :www.javadoop.com/post/Hashed…