一、概括:在我们做项目的性能优化,攻关的时候,经常会需要用到二级,三级缓存组件。
1、实操里面涉及到的技术有Caffeine,Netty,HotKey架构,源码,Canal,Promethus,zset原理:缓存命中率的统计,Redis Cluster原理。
2、在系列里面可以学习到Caffeine的架构,源码,Hotkey架构,源码,Netty架构,源码,j2Cache架构,源码。需要在hotkey和j2Cache做定制和开发。
3、结合有赞的TMC,阿里的jetCache,码云的j2Cache,去哪儿的共享缓存组件。这些行业案例具有学习,吸取行业的架构价值,理论价值。
二、一道重要的面试题:假设10w人同时访问,如何保证系统不崩溃?
背景:很多人会想:我所做的系统总体的用户量,不到1万,怎么可能会有10w人同时访问?
对于toC外部的系统: 哪怕只有1w的用户量,也有可能10w人同时访问
爬虫:通过一个做反爬虫的哥们,发现一个极端的案例:某个页面12000次点击里边,98%的点击率,是爬虫贡献的。爬虫和用户的比例是19比1,那么1w用户,可能会对应到19w爬虫,那么1w用户,就有10w的同时访问的可能性。因为大量的爬虫的存在,当然是有的。
刷子羊毛党:有利可图;提前囤积账号,囤积老的账号和开启机器人批量注册新账号对流量的贡献是满满的。
首先做下数字化分析:从理论来预估来看:假设一个系统10w人同时访问,同一秒访问,也就是吞吐量/并发量为10 wqps。那么按照2-8原则:10w qps,对应到1个亿对的用户量。而我们大多数人所做的系统的总用户量不到1万,那么对应的吞吐量是10。
虽然我们的吞吐量是10,但是我们部署的通常是分布式微服务架构
这种架构,对于qps为10的小流量来说,可以说是小菜一碟。
因为首先先看下上面的架构,LVS是10w qps级别的吞吐量的负载均衡组件,在一些公司是没有的。但是一般公司的接入层有两个nginx,中间有VIP来保证高可用,nginx的吞吐量大概在5w qps左右。假设10w qps过来之后,一个lvs+一个nginx,如果还有点担心的话,一个lvs+两个nginx
然后流量就会进入到服务层,首先会进入springcloud gateway,一个5k。一般我们线上部署2-4个。如果我们想要扛住10w qps的话,我们只能去做扩容。部署额外的16个spring cloud gateway。我们使用自动扩容的机制。如果我们的基础设施好一点,在k8s来做硬件资源的管理,通过HPA自动的伸缩的控制器来实现服务的自动伸缩。比如说对cpu使用率进行监测,如果超过/低于了阈值,就会动态的创建和删除:
#hpa-nginx-deploy:这个服务具有多个容器实例的,为服务创建HPA控制器kubectl autoscale deployment hpa-nginx-deploy --cpu-percent=20 --min= 1 --max=10 -n test
--cpu-percent=20 HPA会通过Pod的伸缩保持平均CPU利用率在20%以内。
--min=1:HPA允许Pod的伸缩范围,最小的pod副本数为1
--min=10:HPA允许Pod的伸缩范围,最大的pod副本数为10
也可以通过脚本+监控进程的方式来实现容器的自动扩缩容
不管使用哪种方式都实现了微服务网关和服务的自动扩容和自动缩容
所以:接入层和服务层的抵抗10w流量的方式:
①、扩容:必须要有扎扎实实的硬件支撑。
②、限流:用户体验差点,会返回给用户系统忙之类的提示。限流是无奈之举,nginx可以进行限流,微服务网关限流,redis+lua:令牌桶限流,阿里的sentinel:流量哨兵组件来限流。限流到后端的微服务最大支撑1wqps,就限流到1wqps。限流简单粗暴的方式
如果10w qps都是有效流量,不能使用限流这种简单粗暴的方式,而是这个10wqps必须进入到服务层,进入到缓存redis。
一般情况下,我们的redis集群一般搭建的是3主3从。
一般来说,主节点提供服务,从节点是做冗余的,并不提供数据的写入服务。redis cluster模式官方默认主节点提供读写,从节点提供slot数据备份以及故障转移。默认情况下,从节点并不提供读写服务。
那么3个节点:单节点redis的吞吐量一般是2w左右。那么10w流量过来,能否扛得住?
分两个情况:
1、在三个节点流量分布均匀的话,这个时候勉强是可以够的,但是突发流量只有1s的时间就转瞬即逝,这时候是可以支撑的。
2、10w qps访问的是同一个key,我们没有做处理,这个时候命中到一个节点,于是就很容易出现redis cpu 为100%,请求会排队,没有响应,严重的情况下出现redis雪崩。用户请求就超时,这时就访问db,db就根本扛不了这么高的流量,db就会崩溃,导致整个服务雪崩。
解决方案:
①、redis扩容:redis的扩容是很麻烦的,慢慢去扩容
②、使用本地缓存:很好解决突发流量的问题,如果key刚好被本地缓存命中,就不用走到redis缓存里面来,刚好在服务层就可以把请求给消化掉。前提:我们所访问的数据需要提前在本地缓存存储,当然一旦第一次访问把数据写入到本地缓存,后续的访问也能减轻redis缓存的压力。这就是我们常说的二级/三级缓存架构。
二级缓存架构:
java本地缓存+redis分布式缓存,具体如下图:
先访问一级缓存caffeine,如果没有找到,再访问二级缓存redis集群。
三级缓存结构:
nginx使用的缓存是lua的字典结构:三级缓存
三、本地缓存的优缺点:
1、快但是量少:访问速度快,但无法进行大数据存储
本地缓存相对于分布式缓存的好处是:由于数据不需要跨网络传输,所以性能更好,但是由于占用了应用进程的内存空间,如java进程的jvm内存空间,所以不能进行大数据的数据存储。
2、需要解决数据一致性问题:本地缓存,分布式缓存,DB数据一致性问题
与此同时,本地缓存只被该应用进程访问,一般无法被其他应用进程访问,所以在应用进程的集群部署当中,如果对应的数据库数据,存在数据更新,则需要同步更新不同部署节点的缓存数据来保证数据一致性。
复杂度较高并且容易出错,如基于rocketmq的发布订阅机制来同步更新各个部署节点。
3、未持久化,容易丢失:数据随应用进程的重启而丢失
由于本地缓存的数据是存储在应用进程的内存空间的,所以当应用进程重启时,本地缓存的数据会丢失,所以对于需要更改然后持久化的数据,需要注意及时保存,否则可能会造成数据丢失。
4、需要尽量缓存热点key,而提升缓存的命中率
由于本地缓存太小,从而容易被淘汰。如果还没有来得及访问,本地缓存中的数据就淘汰了,那就失去了本地缓存的价值,当然,本地缓存的命中率也会很低。
四、如何提升缓存命中率?
1、采用更好的缓存淘汰策略
比如caffeine中,使用了w-tinylfu策略。这种策略的缓存命中率,比较简单的lfu.lru都高出很多,有测表明:caffeine比guava的命中率,不同场景,都会高出10%以上。
2、尽量识别和缓存热点数据
简单的说,把热点数据,加载到本地缓存。