缓存层场景实战:你知道如何将十几秒的查询请求优化成毫秒级吗?

本文详细探讨了电商系统中数据库读取频繁的问题,通过案例分析,提出将商品详情数据缓存以优化性能。文章重点介绍了本地缓存(JVM内存缓存)和分布式缓存(Redis)的选择及其在缓存策略中的应用,包括缓存何时存储数据、缓存雪崩和穿透的解决方案以及预热策略。
摘要由CSDN通过智能技术生成

读缓存

前面已经讲解了数据持久化层相关的架构方案,本文开始正式进入第2部分——缓存层场景实战。这一章主要围绕数据库读取操作频繁的问题进行探讨。首先描述一下业务场景。

业务场景:如何将十几秒的查询请求优化成毫秒级

这次项目针对的系统是一个电商系统。每个电商系统都有个商品详情页。

一开始这个页面很简单,只包括商品的图片、介绍、规格、评价等。

刚开始,这个页面打开很快,系统运行平稳可靠。

后来,页面中加了商品推荐,即在商品详情页后面显示一些推荐商品的列表。

再后来,页面中加入了最近成交情况,即显示一下某人在什么时候下单了。

接着,页面中又加入了优惠活动,即显示这个商品都可以参加哪些优惠活动。

……

当时这个系统里面有5万多条商品数据,数据量并不大,但是每次用户浏览商品详情页时都需要几十条SQL语句,经常出现十几秒才能打开详情页的情况。

这样的用户体验当然不好。公司有个第三方监控工具,从国内各地监控系统几个关键路径的性能。其中一个关键路径是从首页到搜索再到商品详情页的时长,这个平均时长从刚开始的3.61秒逐渐变成后来的15.53秒,监控工具成为“摆设”,实际不再使用。之前也有人提过商品详情页需要优化,不过另一个人说,打开App进入首页前广告都要播放10秒,用户还在乎这个商品详情页打开慢吗?当然,没有马上优化的原因,是后面不断有其他业务需求。

后来这个优化项目提上了日程,项目组开始考虑要怎么优化。重构数据库基本不可能,最好不要改动表结构。大家想到的方案也很通用,就是把大部分商品的详情数据缓存起来,少部分的数据通过异步加载。比如,最近的成交数据通过异步加载,即用户打开商品详情页以后,再在后台加载最近的成交数据,并显示给用户。不过这一章主要讲缓存,所以异步加载的方案就不在此展开了。

关于缓存,最简单的实现方法就是使用本地缓存,即把商品详情数据放在JVM里面。在Google Guava中有一个内存缓存模块,它把所有商品的ID与商品详情信息一对一缓存至JVM内存中,用户获取商品详情数据时,系统会根据商品ID直接从缓存中读取数据,能大大提升用户页面的访问速度。

不过,通过简单换算后发现这个方法明显不合理。先来举个例子。

一条商品数据中往往包含品牌、分类、参数、规格、服务、描述等字段,仅存储这些商品数据就要占用500KB左右的内存,再将这些数据缓存到本地的话,就要占用500KB×50000≈25GB内存。此时,假设商品服务有30个服务器节点,仅缓存商品数据就需要额外准备750GB的内存空间,这种方法显然不可取。

为此,项目组决定使用另外一个解决办法——分布式缓存,先将所有的缓存数据集中存储在同一个地方,而非重复保存到各个服务器节点中,然后所有的服务器节点都从这个地方读取数据,如图4-1所示。

• 图4-1 分布式缓存示意图

那么这个统一存储缓存数据的地方需要使用什么技术呢?这就涉及接下来要讲的缓存中间件技术选型问题了。

缓存中间件技术选型(Memcached,MongoDB,Redis)

先将目前比较流行的缓存中间件Memcached、MongoDB、Redis进行简单对比,见表4-1。

表4-1 缓存中间件对比

使用MongoDB的公司最少,因为它只是一个数据库,由于它的读写速度与其他数据库相比更快,人们才把它当作类似缓存的存储。

所以接下来就是比较Redis和Memcached,并从中做出选择。

目前,Redis比Memcached更流行,这里总结一下原因,共3点。

(1)数据结构

举个例子,在使用Memcached保存List缓存对象的过程中,如果往List中增加一条数据,则首先需要读取整个List,再反序列化塞入数据,接着再序列化存储回Memcached。而对于Redis而言,这仅仅是一个Redis请求,它会直接帮助塞入数据并存储,简单快捷。

(2)持久化

对于Memcached来说,一旦系统宕机数据就会丢失。因为Memcached的设计初衷就是一个纯内存缓存。

通 过 Memcached 的 官 方 文 档 得 知 , 1.5.18 版 本 以 后 的 Memcached 支 持Restartable Cache(可重启缓存),其实现原理是重启时CLI先发信号给守护进程,然后守护进程将内存持久化至一个文件中,系统重启时再从那个文件中恢复数据。不过,这个设计仅在正常重启情况下使用,意外情况还是不处理。

而Redis是有持久化功能的。

(3)集群这点尤为重要。Memcached的集群设计非常简单,客户端根据Hash值直接判断存取的Memcached节点。而Redis的集群因在高可用、主从、冗余、Failover等方面都有所考虑,所以集群设计相对复杂些,属于较常规的分布式高可用架构。

因此,经过一番慎重的思考,项目组最终决定使用Redis作为缓存的中间件。

技术选型完成后,开始考虑缓存的一些具体问题,先从缓存何时存储数据入手。

缓存何时存储数据

使用缓存的逻辑如下。

1)先尝试从缓存中读取数据。

2)若缓存中没有数据或者数据过期,再从数据库中读取数据保存到缓存中。

3)最终把缓存数据返回给调用方。

这种逻辑唯一麻烦的地方是,当用户发来大量的并发请求时,它们会发现缓存中没有数据,那么所有请求会同时挤在第2)步,此时如果这些请求全部从数据库读取数据,就会让数据库崩溃。

数据库的崩溃可以分为3种情况。

1)单一数据过期或者不存在,这种情况称为缓存击穿。

解决方案:第一个线程如果发现Key不存在,就先给Key加锁,再从数据库读取数据保存到缓存中,最后释放锁。如果其他线程正在读取同一个Key值,那么必须等到锁释放后才行。关于锁的问题前面已经讲过,此处不再赘述。

2)数据大面积过期或者Redis宕机,这种情况称为缓存雪崩。

解决方案:设置缓存的过期时间为随机分布或设置永不过期即可。

3)一个恶意请求获取的Key不在数据库中,这种情况称为缓存穿透。

比如正常的商品ID是从100000到1000000(10万到100万之间的数值),那么恶意请求就可能会故意请求2000000以上的数据。这种情况如果不做处理,恶意请求每次进来时,肯定会发现缓存中没有值,那么每次都会查询数据库,虽然最终也没在数据库中找到商品,但是无疑给数据库增加了负担。这里给出两种解决办法。

①在业务逻辑中直接校验,在数据库不被访问的前提下过滤掉不存在的Key。

②针对恶意请求的Key存放一个空值在缓存中,防止恶意请求骚扰数据库。

最后说一下缓存预热。

上面这些逻辑都是在确保查询数据的请求已经过来后如何适当地处理,如果缓存数据找不到,再去数据库查询,最终是要占用服务器额外资源的。那么最理想的就是在用户请求过来之前把数据都缓存到Redis中。这就是缓存预热。

其具体做法就是在深夜无人访问或访问量小的时候,将预热的数据保存到缓存中,这样流量大的时候,用户查询就无须再从数据库读取数据了,将大大减小数据读取压力。

关于缓存何时存数据的问题就讨论完了,接下来开始讨论更新缓存的问题,这部分内容因涉及双写(缓存+数据库),所以会花费一些篇幅。

  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值