文档参考:书名:《从程序员到架构师:大数据量、缓存、高并发、微服务、多团队协同等核心场景实战》-王伟杰
前文如下:
1.业务场景:如何将十几秒的查询请求优化成毫秒级
每个电商系统都有个商品详情页。一开始这个页面很简单,只包括商品的图片、介绍、规格、评价等。刚开始,这个页面打开很快,系统运行平稳可靠。
后来,页面中加了商品推荐,即在商品详情页后面显示一些推荐商品的列表。
再后来,页面中加入了最近成交情况,即显示一下某人在什么时候下单了。接着,页面中又加入了优惠活动,即显示这个商品都可以参加哪些优惠活动。
……当时这个系统里面有5万多条商品数据,数据量并不大,但是每次用户浏览商品详情页时都需要几十条SQL语句,经常出现十几秒才能打开详情页的情况。这样的用户体验当然不好。
一个解决办法——分布式缓存,先将所有的缓存数据集中存储在同一个地方,而非重复保存到各个服务器节点中,然后所有的服务器节点都从这个地方读取数据。
那么这个统一存储缓存数据的地方需要使用什么技术呢?这就涉及接下来要讲的缓存中间件技术选型问题了。
2. 缓存中间件技术选型(Memcached,MongoDB,Redis)
先将目前比较流行的缓存中间件Memcached、MongoDB、Redis进行简单对比,见表4-1。
比较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等方面都有所考虑,所以集群设计相对复杂些,属于较常规的分布式高可用架构。
3.缓存何时存储数据
使用缓存的逻辑如下。
1)先尝试从缓存中读取数据。
2)若缓存中没有数据或者数据过期,再从数据库中读取数据保存到缓存中。
3)最终把缓存数据返回给调用方。
这种逻辑唯一麻烦的地方是,当用户发来大量的并发请求时,它们会发现缓存中没有数据,那么所有请求会同时挤在第2)步,此时如果这些请求全部从数据库读取数据,就会让数据库崩溃。
数据库的崩溃可以分为3种情况。
1)单一数据过期或者不存在,这种情况称为缓存击穿。解决方案:第一个线程如果发现Key不存在,就先给Key加锁,再从数据库读取数据保存到缓存中,最后释放锁。如果其他线程正在读取同一个Key值,那么必须等到锁释放后才行。关于锁的问题前面已经讲过,此处不再赘述。
2)数据大面积过期或者Redis宕机,这种情况称为缓存雪崩。解决方案:设置缓存的过期时间为随机分布或设置永不过期即可。
3)一个恶意请求获取的Key不在数据库中,这种情况称为缓存穿透。比如正常的商品ID是从100000到1000000(10万到100万之间的数值),那么恶意请求就可能会故意请求2000000以上的数据。这种情况如果不做处理,恶意请求每次进来时,肯定会发现缓存中没有值,那么每次都会查询数据库,虽然最终也没在数据库中找到商品,但是无疑给数据库增加了负担。这里给出两种解决办法。
比如正常的商品ID是从100000到1000000(10万到100万之间的数值),那么恶意请求就可能会故意请求2000000以上的数据。这种情况如果不做处理,恶意请求每次进来时,肯定会发现缓存中没有值,那么每次都会查询数据库,虽然最终也没在数据库中找到商品,但是无疑给数据库增加了负担。这里给出两种解决办法。
①在业务逻辑中直接校验,在数据库不被访问的前提下过滤掉不存在的Key。
②针对恶意请求的Key存放一个空值在缓存中,防止恶意请求骚扰数据库。
4)最后说一下缓存预热。上面这些逻辑都是在确保查询数据的请求已经过来后如何适当地处理,如果缓存数据找不到,再去数据库查询,最终是要占用服务器额外资源的。那么最理想的就是在用户请求过来之前把数据都缓存到Redis中。这就是缓存预热。其具体做法就是在深夜无人访问或访问量小的时候,将预热的数据保存到缓存中,这样流量大的时候,用户查询就无须再从数据库读取数据了,将大大减小数据读取压力。