多级缓存
多级缓存在秒杀系统中是非常重要的一个技术点,是应对秒杀场景瞬时高并发读请求的一种有效手段。通过在数据库前面加入多个缓存层,达到过滤掉大多数读请求的效果,有效的对数据库起到保护作用,防止数据库受到大流量的冲击。
浏览器缓存、CDN、反向代理服务器缓存都在本系列前面的文章中描述过了。
- 浏览器缓存、CDN: 【图解秒杀系列】秒杀技术点——浏览器缓存、CDN
- 反向代理服务器缓存:【图解秒杀系列】秒杀技术点——静态化
我们下面说一说本地缓存(一级缓存)和分布式缓存(二级缓存)。
本地缓存
本地缓存就是我们应用程序内部的缓存,可以是JVM(堆内)缓存,也可以是堆外缓存。比如我们可用用一个HashMap缓存商品id与商品详情信息的映射关系。
Map<Long, ProducetDetailInfo> localCache = new HashMap<>();
这样当服务接收到一个商品详情的查询请求时,假如本地缓存命中,就可以直接返回;如果本地缓存不命中,再去分布式缓存中获取并回写本地缓存;如果分布式缓存也不命中,才去查询DB,然后回写分布式缓存和本地缓存。
但是,直接用HashMap做本地缓存有一个缺点,就是对于一些冷数据的删除是不方便的,如果不做删除的话该HashMap占用的空间会越来越大,因此我们还要自己维护数据的访问频率或者最近的访问时间,以便于冷数据的删除。
因此,直接使用HashMap做本地缓存是不太推荐的,更推荐的是使用Guava Cache。使用了Guava Cache,只要通过CacheBuilder.maximumSize(long)设置容量大小即可,当超出容量时会使用LRU算法进行回收。
分布式缓存
分布式缓存是独立部署的缓存服务,是运行在应用程序之外的,需要经过网络通信进行缓存的读写。常用的分布式缓存就是Redis。当本地缓存不命中时,就尝试访问分布式缓存,如果分布式缓存命中了,就回写本地缓存。
一般而言,我们会把本地缓存的过期时间设置的短一点,而分布式缓存的过期时间设置的较长一点。
分层过滤
分层过滤是秒杀系统中高效处理高并发请求的策略,分层过滤采用“漏斗”式设计,通过在不同层次上逐步过滤无效请求,确保只有有效请求能够到达系统后端,从而减轻系统压力,提高系统的处理能力和响应速度。
秒杀系统分层过滤中的分层大致可分为以下几层:
浏览器缓存和CDN作为静态资源的缓存,可以过滤掉大多数静态资源请求,减轻服务器压力,这个在前面的文章已经说过。
反向代理服务器同样也起到静态资源缓存的效果,如果使用OpenResty,还可以通过lua_shared_dict缓存后端返回的商品详情数据,然后通过lua-resty-template进行模板渲染得到商品详情页返回给客户端。这样后续对商品详情页的请求都直接在这一层就可以处理掉,进一步减轻了服务器压力。
网关作为所有流量的统一入口,负责管理和控制进出网络的流量。它可以提供路由、安全、协议转换等多种功能,确保网络之间的有效通信和数据安全。
在网关这一层,我们可以加入以下过滤功能:
- 限流:比如限制每秒只放进来1000个请求,多的就抛弃,前端显示“系统繁忙、请稍后再试”。
- Token校验:在网关层添加秒杀的Token检验,只有携带了Token并检验通过的才可以下单,否则请认定为无效请求。
- 黑名单防刷:在一段时间内请求超过一定次数的用户,我们就认定它为刷子并记录进黑名单,下次再次请求时则直接拒绝。
经过了网关之后,请求就会到达我们的服务,此时我们还可以在这里添加一层限流,比如使用Hystrix或者Sentinel之类的流控组件进行限流。
我们在前面的文章提到过,可以添加验证码或者答题机制进行削峰。那么我们的服务检验到验证码不通过,或者答题错误的,就可以拒绝处理该请求,这样就进一步的过滤掉一些无效请求。
再往下就是我们上面说过的多级缓存:
- 我们可以在一二级缓存中缓存商品详情数据。
- 我们可以在缓存中添加一个售罄标志,当该标志位true时,表示秒杀商品已经售空,请求不再往下走。
- 除此以外,秒杀常常伴随着限购,比如限制每个用户只能买一件或两件。我们可用通过缓存记录用户购买的数量,当超过限购规则限制时,就拒绝用户再下单。
经过层层过滤,最终剩下的就是有效请求,此时落到数据库的请求数已经很少了。