Nginx+Redis+Ehcache:大型高并发与高可用的三层缓存架构总结

01b86e8287f459e5f42b4205de43d2ab.jpeg

摘要: 对于高并发架构,毫无疑问缓存是最重要的一环,对于大量的高并发,能够采用三层缓存架构来实现,nginx+redis+ehcachenode


Nginx


02738b7fdf3a5550218ed07139d70ba4.jpeg


对于中间件nginx经常使用来作流量的分发,同时nginx自己也有本身的缓存(容量有限),咱们能够用来缓存热点数据,让用户的请求直接走缓存并返回,减小流向服务器的流量linux


1、模板引擎nginx


一般咱们能够配合使用freemaker/velocity等模板引擎来抗住大量的请求redis


小型系统可能直接在服务器端渲染出全部的页面并放入缓存,以后的相同页面请求就能够直接返回,不用去查询数据源或者作数据逻辑处理数据库

对于页面很是之多的系统,当模板有改变,上述方法就须要从新渲染全部的页面模板,毫无疑问是不可取的。所以配合nginx+lua(OpenResty),将模板单独保存在nginx缓存中,同时对于用来渲染的数据也存在nginx缓存中,可是须要设置一个缓存过时的时间,以尽量保证模板的实时性apache


2、双层nginx来提高缓存命中率后端


对于部署多个nginx而言,若是不加入一些数据的路由策略,那么可能致使每一个nginx的缓存命中率很低。所以能够部署双层nginx缓存


分发层nginx负责流量分发的逻辑和策略,根据本身定义的一些规则,好比根据productId进行hash,而后对后端nginx数量取模将某一个商品的访问请求固定路由到一个nginx后端服务器上去tomcat

后端nginx用来缓存一些热点数据到本身的缓存区(分发层只能配置1个吗)服务器


Redis


f148cda7b6c28cb52775058850c51ac6.jpeg


用户的请求,在nginx没有缓存相应的数据,那么会进入到redis缓存中,redis能够作到全量数据的缓存,经过水平扩展可以提高并发、高可用的能力


1、持久化机制


持久化机制:将redis内存中的数据持久化到磁盘中,而后能够按期将磁盘文件上传至S3(AWS)或者ODPS(阿里云)等一些云存储服务上去。


若是同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来从新构建数据,由于AOF中的数据更加完整,建议将两种持久化机制都开启,用AO F来保证数据不丢失,做为数据恢复的第一选择;用RDB来做不一样程度的冷备,在AOF文件都丢失或损坏不可用的时候来快速进行数据的恢复。


实战踩坑:对于想从RDB恢复数据,同时AOF开关也是打开的,一直没法正常恢复,由于每次都会优先从AOF获取数据(若是临时关闭AOF,就能够正常恢复)。此时首先中止redis,而后关闭AOF,拷贝RDB到相应目录,启动redis以后热修改配置参数redis config set appendonly yes,此时会自动生成一个当前内存数据的AOF文件,而后再次中止redis,打开AOF配置,再次启动数据就正常启动


RDB

对redis中的数据执行周期性的持久化,每一刻持久化的都是全量数据的一个快照。对redis性能影响较小,基于RDB可以快速异常恢复

AOF

以append-only的模式写入一个日志文件中,在redis重启的时候能够经过回放AOF日志中的写入指令来从新构建整个数据集。(实际上每次写的日志数据会先到linux os cache,而后redis每隔一秒调用操做系统fsync将os cache中的数据写入磁盘)。对redis有必定的性能影响,可以尽可能保证数据的完整性。redis经过rewrite机制来保障AOF文件不会太庞大,基于当前内存数据并能够作适当的指令重构。


2、redis集群


replication

一主多从架构,主节点负责写,而且将数据同步到其余salve节点(异步执行),从节点负责读,主要就是用来作读写分离的横向扩容架构。这种架构的master节点数据必定要作持久化,不然,当master宕机重启以后内存数据清空,那么就会将空数据复制到slave,致使全部数据消失

sentinal哨兵

哨兵是redis集群架构中很重要的一个组件,负责监控redis master和slave进程是否正常工做,当某个redis实例故障时,可以发送消息报警通知给管理员,当master node宕机可以自动转移到slave node上,若是故障转移发生来,会通知client客户端新的master地址。sentinal至少须要3个实例来保证本身的健壮性,而且可以更好地进行quorum投票以达到majority来执行故障转移。

前两种架构方式最大的特色是,每一个节点的数据是相同的,没法存取海量的数据。所以哨兵集群的方式使用与数据量不大的状况

redis cluster

redis cluster支撑多master node,每一个master node能够挂载多个slave node,若是mastre挂掉会自动将对应的某个slave切换成master。须要注意的是redis cluster架构下slave节点主要是用来作高可用、故障主备切换的,若是必定须要slave可以提供读的能力,修改配置也能够实现(同时也须要修改jedis源码来支持该状况下的读写分离操做)。redis cluster架构下,master就是能够任意扩展的,直接横向扩展master便可提升读写吞吐量。slave节点可以自动迁移(让master节点尽可能平均拥有slave节点),对整个架构过载冗余的slave就能够保障系统更高的可用性。


ehcache


3aca08f28333e7d8400712a055722a65.jpeg


tomcat jvm堆内存缓存,主要是抗redis出现大规模灾难。若是redis出现了大规模的宕机,致使nginx大量流量直接涌入数据生产服务,那么最后的tomcat堆内存缓存也能够处理部分请求,避免全部请求都直接流向DB


针对上面的技术我特地整理了一下,有不少技术不是靠几句话能讲清楚,因此干脆找朋友录制了一些视频,不少问题其实答案很简单,可是背后的思考和逻辑不简单,要作到知其然还要知其因此然。若是想学习Java工程化、高性能及分布式、深刻浅出。微服务、Spring,MyBatis,Netty源码分析的朋友能够加个人Java进阶群:694549689,群里有阿里大牛直播讲解技术,以及Java大型互联网技术的视频免费分享给你们。


缓存数据更新策略


对时效性要求高的缓存数据,当发生变动的时候,直接采起数据库和redis缓存双写的方案,让缓存时效性最高。

对时效性不高的数据,当发生变动以后,采起MQ异步通知的方式,经过数据生产服务来监听MQ消息,而后异步去拉取服务的数据更新tomcat jvm缓存和redis缓存,对于nginx本地缓存过时以后就能够从redis中拉取新的数据并更新到nginx本地。


经典的缓存+数据库读写的模式,cache aside pattern


读的时候,先读缓存,缓存没有的话,那么就读数据库,而后取出数据后放入缓存,同时返回响应

更新的时候,先删除缓存,而后再更新数据库

之因此更新的时候只是删除缓存,由于对于一些复杂有逻辑的缓存数据,每次数据变动都更新一次缓存会形成额外的负担,只是删除缓存,让该数据下一次被使用的时候再去执行读的操做来从新缓存,这里采用的是懒加载的策略。举个例子,一个缓存涉及的表的字段,在1分钟内就修改了20次,或者是100次,那么缓存跟新20次,100次;可是这个缓存在1分钟内就被读取了1次,所以每次更新缓存就会有大量的冷数据,对于缓存符合28黄金法则,20%的数据,占用了80%的访问量


数据库和redis缓存双写不一致的问题


最初级的缓存不一致问题以及解决方案

问题:若是先修改数据库再删除缓存,那么当缓存删除失败来,那么会致使数据库中是最新数据,缓存中依旧是旧数据,形成数据不一致。

解决方案:能够先删除缓存,再修改数据库,若是删除缓存成功可是数据库修改失败,那么数据库中是旧数据,缓存是空不会出现不一致

比较复杂的数据不一致问题分析

问题:对于数据发生来变动,先删除缓存,而后去修改数据库,此时数据库中的数据尚未修改为功,并发的读请求到来去读缓存发现是空,进而去数据库查询到此时的旧数据放到缓存中,而后以前对数据库数据的修改为功来,就会形成数据不一致

解决方案:将数据库与缓存更新与读取操做进行异步串行化。当更新数据的时候,根据数据的惟一标识,将更新数据操做路由到一个jvm内部的队列中,一个队列对应一个工做线程,线程串行拿到队列中的操做一条一条地执行。当执行队列中的更新数据操做,删除缓存,而后去更新数据库,此时尚未完成更新的时候过来一个读请求,读到了空的缓存那么能够先将缓存更新的请求发送至路由以后的队列中,此时会在队列积压,而后同步等待缓存更新完成,一个队列中多个相同数据缓存更新请求串在一块儿是没有意义的,所以能够作过滤处理。等待前面的更新数据操做完成数据库操做以后,才会去执行下一个缓存更新的操做,此时会从数据库中读取最新的数据,而后写入缓存中,若是请求还在等待时间范围内,不断轮询发现能够取到缓存中值就能够直接返回(此时可能会有对这个缓存数据的多个请求正在这样处理);若是请求等待事件超过必定时长,那么这一次的请求直接读取数据库中的旧值

对于这种处理方式须要注意一些问题:

读请求长时阻塞:因为读请求进行来很是轻度的异步化,因此对超时的问题须要格外注意,超过超时时间会直接查询DB,处理很差会对DB形成压力,所以须要测试系统高峰期QPS来调整机器数以及对应机器上的队列数最终决定合理的请求等待超时时间

多实例部署的请求路由:可能这个服务会部署多个实例,那么必须保证对应的请求都经过nginx服务器路由到相同的服务实例上

热点数据的路由导师请求的倾斜:由于只有在商品数据更新的时候才会清空缓存,而后才会致使读写并发,因此更新频率不是过高的话,这个问题的影响并非特别大,可是的确可能某些机器的负载会高一些


分布式缓存重建并发冲突解决方案


对于缓存生产服务,可能部署在多台机器,当redis和ehcache对应的缓存数据都过时不存在时,此时可能nginx过来的请求和kafka监听的请求同时到达,致使二者最终都去拉取数据而且存入redis中,所以可能产生并发冲突的问题,能够采用redis或者zookeeper相似的分布式锁来解决,让请求的被动缓存重建与监听主动的缓存重建操做避免并发的冲突,当存入缓存的时候经过对比时间字段废弃掉旧的数据,保存最新的数据到缓存


缓存冷启动以及缓存预热解决方案


当系统第一次启动,大量请求涌入,此时的缓存为空,可能会致使DB崩溃,进而让系统不可用,一样当redis全部缓存数据异常丢失,也会致使该问题。所以,能够提早放入数据到redis避免上述冷启动的问题,固然也不多是全量数据,能够根据相似于当天的具体访问状况,实时统计出访问频率较高的热数据,这里热数据也比较多,须要多个服务并行的分布式去读写到redis中(因此要基于zk分布式锁)


经过nginx+lua将访问流量上报至kafka中,storm从kafka中消费数据,实时统计处每一个商品的访问次数,访问次数基于LRU(apache commons collections LRUMap)内存数据结构的存储方案,使用LRUMap去存放是由于内存中的性能高,没有外部依赖,每一个storm task启动的时候基于zk分布式锁将本身的id写入zk同一个节点中,每一个storm task负责完成本身这里的热数据的统计,每隔一段时间就遍历一下这个map,而后维护一个前1000的数据list,而后去更新这个list,最后开启一个后台线程,每隔一段时间好比一分钟都将排名的前1000的热数据list同步到zk中去,存储到这个storm task对应的一个znode中去


部署多个实例的服务,每次启动的时候就会去拿到上述维护的storm task id列表的节点数据,而后根据taskid,一个一个去尝试获取taskid对应的znode的zk分布式锁,若是可以获取到分布式锁,再去获取taskid status的锁进而查询预热状态,若是没有被预热过,那么就将这个taskid对应的热数据list取出来,从而从DB中查询出来写入缓存中,若是taskid分布式锁获取失败,快速抛错进行下一次循环获取下一个taskid的分布式锁便可,此时就是多个服务实例基于zk分布式锁作协调并行的进行缓存的预热


缓存热点致使系统不可用解决方案


对于瞬间大量的相同数据的请求涌入,可能致使该数据通过hash策略以后对应的应用层nginx被压垮,若是请求继续就会影响至其余的nginx,最终致使全部nginx出现异常整个系统变得不可用。


基于nginx+lua+storm的热点缓存的流量分发策略自动降级来解决上述问题的出现,能够设定访问次数大于后95%平均值n倍的数据为热点,在storm中直接发送http请求到流量分发的nginx上去,使其存入本地缓存,而后storm还会将热点对应的完整缓存数据没发送到全部的应用nginx服务器上去,并直接存放到本地缓存。


对于流量分发nginx,访问对应的数据,若是发现是热点标识就当即作流量分发策略的降级,对同一个数据的访问从hash到一台应用层nginx降级成为分发至全部的应用层nginx。storm须要保存上一次识别出来的热点List,并同当前计算出来的热点list作对比,若是已经不是热点数据,则发送对应的http请求至流量分发nginx中来取消对应数据的热点标识


缓存雪崩解决方案


redis集群完全崩溃,缓存服务大量对redis的请求等待,占用资源,随后缓存服务大量的请求进入源头服务去查询DB,使DB压力过大崩溃,此时对源头服务的请求也大量等待占用资源,缓存服务大量的资源所有耗费在访问redis和源服务无果,最后使自身没法提供服务,最终会致使整个网站崩溃。


事前的解决方案,搭建一套高可用架构的redis cluster集群,主从架构、一主多从,一旦主节点宕机,从节点自动跟上,而且最好使用双机房部署集群。


事中的解决方案,部署一层ehcache缓存,在redis所有实现状况下可以抗住部分压力;对redis cluster的访问作资源隔离,避免全部资源都等待,对redis cluster的访问失败时的状况去部署对应的熔断策略,部署redis cluster的降级策略;对源服务访问的限流以及资源隔离


过后的解决方案:redis数据作了备份能够直接恢复,重启redis便可;redis数据完全失败来或者数据过旧,能够快速缓存预热,而后让redis从新启动。而后因为资源隔离的half-open策略发现redis已经可以正常访问,那么全部的请求将自动恢复


缓存穿透解决方案


对于在多级缓存中都没有对应的数据,而且DB也没有查询到数据,此时大量的请求都会直接到达DB,致使DB承载高并发的问题。解决缓存穿透的问题能够对DB也没有的数据返回一个空标识的数据,进而保存到各级缓存中,由于有对数据修改的异步监听,因此当数据有更新,新的数据会被更新到缓存汇中。


Nginx缓存失效致使redis压力倍增


能够在nginx本地,设置缓存数据的时候随机缓存的有效期,避免同一时刻缓存都失效而大量请求直接进入redis


这个过程值得咱们去深刻学习和思考。


本文转载自公众号「开源中国」

相关文章
相关标签/搜索