对于高并发架构, 毫无疑问缓存是最重要的一环, 对于大量的高并发, 可以采用三层缓存架构来实现, nginx+redis+ehcache
nginx
对于中间件 nginx 常用来做流量的分发, 同时 nginx 本身也有自己的缓存 (容量有限), 我们可以用来缓存热点数据, 让用户的请求直接走缓存并返回, 减少流向服务器的流量
一. 模板引擎
通常我们可以配合使用 freemaker/velocity 等模板引擎来抗住大量的请求
1. 小型系统可能直接在服务器端渲染出所有的页面并放入缓存, 之后的相同页面请求就可以直接返回, 不用去查询数据源或者做数据逻辑处理
2. 对于页面非常之多的系统, 当模板有改变, 上述方法就需要重新渲染所有的页面模板, 毫无疑问是不可取的因此配合 nginx+lua(OpenResty), 将模板单独保存在 nginx 缓存中, 同时对于用来渲染的数据也存在 nginx 缓存中, 但是需要设置一个缓存过期的时间, 以尽可能保证模板的实时性
二. 双层 nginx 来提升缓存命中率
对于部署多个 nginx 而言, 如果不加入一些数据的路由策略, 那么可能导致每个 nginx 的缓存命中率很低因此可以部署双层 nginx
1. 分发层 nginx 负责流量分发的逻辑和策略, 根据自己定义的一些规则, 比如根据 productId 进行 hash, 然后对后端 nginx 数量取模将某一个商品的访问请求固定路由到一个 nginx 后端服务器上去
2. 后端 nginx 用来缓存一些热点数据到自己的缓存区 (分发层只能配置 1 个吗)
redis
用户的请求, 在 nginx 没有缓存相应的数据, 那么会进入到 redis 缓存中, redis 可以做到全量数据的缓存, 通过水平扩展能够提升并发高可用的能力
一. 持久化机制: 将 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 配置, 再次启动数据就正常启动
1.RDB
对 redis 中的数据执行周期性的持久化, 每一刻持久化的都是全量数据的一个快照对 redis 性能影响较小, 基于 RDB 能够快速异常恢复
2.AOF
以 append-only 的模式写入一个日志文件中, 在 redis 重启的时候可以通过回放 AOF 日志中的写入指令来重新构建整个数据集 (实际上每次写的日志数据会先到 linux os cache, 然后 redis 每隔一秒调用操作系统 fsync 将 os cache 中的数据写入磁盘) 对 redis 有一定的性能影响, 能够尽量保证数据的完整性 redis 通过 rewrite 机制来保障 AOF 文件不会太庞大, 基于当前内存数据并可以做适当的指令重构
二. redis 集群
1.replication
一主多从架构, 主节点负责写, 并且将数据同步到其他 salve 节点 (异步执行), 从节点负责读, 主要就是用来做读写分离的横向扩容架构这种架构的 master 节点数据一定要做持久化, 否则, 当 master 宕机重启之后内存数据清空, 那么就会将空数据复制到 slave, 导致所有数据消失
2.sentinal 哨兵
哨兵是 redis 集群架构中很重要的一个组件, 负责监控 redis master 和 slave 进程是否正常工作, 当某个 redis 实例故障时, 能够发送消息报警通知给管理员, 当 master node 宕机能够自动转移到 slave node 上, 如果故障转移发生来, 会通知 client 客户端新的 master 地址 sentinal 至少需要 3 个实例来保证自己的健壮性, 并且能够更好地进行 quorum 投票以达到 majority 来执行故障转移
前两种架构方式最大的特点是, 每个节点的数据是相同的, 无法存取海量的数据因此哨兵集群的方式使用与数据量不大的情况
3.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
tomcat jvm 堆内存缓存, 主要是抗 redis 出现大规模灾难如果 redis 出现了大规模的宕机, 导致 nginx 大量流量直接涌入数据生产服务, 那么最后的 tomcat 堆内存缓存也可以处理部分请求, 避免所有请求都直接流向 DB
针对上面的技术我特意整理了一下, 有很多技术不是靠几句话能讲清楚, 所以干脆找朋友录制了一些视频, 很多问题其实答案很简单, 但是背后的思考和逻辑不简单, 要做到知其然还要知其所以然如果想学习 Java 工程化高性能及分布式深入浅出微服务 Spring,MyBatis,Netty 源码分析的朋友可以加我的 Java 进阶群: 694549689, 群里有阿里大牛直播讲解技术, 以及 Java 大型互联网技术的视频免费分享给大家
缓存数据更新策略
1. 对时效性要求高的缓存数据, 当发生变更的时候, 直接采取数据库和 redis 缓存双写的方案, 让缓存时效性最高
2. 对时效性不高的数据, 当发生变更之后, 采取 MQ 异步通知的方式, 通过数据生产服务来监听 MQ 消息, 然后异步去拉取服务的数据更新 tomcat jvm 缓存和 redis 缓存, 对于 nginx 本地缓存过期之后就可以从 redis 中拉取新的数据并更新到 nginx 本地
经典的缓存 + 数据库读写的模式, cache aside pattern
1. 读的时候, 先读缓存, 缓存没有的话, 那么就读数据库, 然后取出数据后放入缓存, 同时返回响应
2. 更新的时候, 先删除缓存, 然后再更新数据库, 之所以更新的时候只是删除缓存, 因为对于一些复杂有逻辑的缓存数据, 每次数据变更都更新一次缓存会造成额外的负担, 只是删除缓存, 让该数据下一次被使用的时候再去执行读的操作来重新缓存, 这里采用的是懒加载的策略举个例子, 一个缓存涉及的表的字段, 在 1 分钟内就修改了 20 次, 或者是 100 次, 那么缓存跟新 20 次, 100 次; 但是这个缓存在 1 分钟内就被读取了 1 次, 因此每次更新缓存就会有大量的冷数据, 对于缓存符合 28 黄金法则, 20% 的数据, 占用了 80% 的访问量
数据库和 redis 缓存双写不一致的问题
1. 最初级的缓存不一致问题以及解决方案
问题: 如果先修改数据库再删除缓存, 那么当缓存删除失败来, 那么会导致数据库中是最新数据, 缓存中依旧是旧数据, 造成数据不一致
解决方案: 可以先删除缓存, 再修改数据库, 如果删除缓存成功但是数据库修改失败, 那么数据库中是旧数据, 缓存是空不会出现不一致
2. 比较复杂的数据不一致问题分析
问题: 对于数据发生来变更, 先删除缓存, 然后去修改数据库, 此时数据库中的数据还没有修改成功, 并发的读请求到来去读缓存发现是空, 进而去数据库查询到此时的旧数据放到缓存中, 然后之前对数据库数据的修改成功来, 就会造成数据不一致
解决方案: 将数据库与缓存更新与读取操作进行异步串行化当更新数据的时候, 根据数据的唯一标识, 将更新数据操作路由到一个 jvm 内部的队列中, 一个队列对应一个工作线程, 线程串行拿到队列中的操作一条一条地执行当执行队列中的更新数据操作, 删除缓存, 然后去更新数据库, 此时还没有完成更新的时候过来一个读请求, 读到了空的缓存那么可以先将缓存更新的请求发送至路由之后的队列中, 此时会在队列积压, 然后同步等待缓存更新完成, 一个队列中多个相同数据缓存更新请求串在一起是没有意义的, 因此可以做过滤处理等待前面的更新数据操作完成数据库操作之后, 才会去执行下一个缓存更新的操作, 此时会从数据库中读取最新的数据, 然后写入缓存中, 如果请求还在等待时间范围内, 不断轮询发现可以取到缓存中值就可以直接返回 (此时可能会有对这个缓存数据的多个请求正在这样处理); 如果请求等待事件超过一定时长, 那么这一次的请求直接读取数据库中的旧值
对于这种处理方式需要注意一些问题:
1. 读请求长时阻塞: 由于读请求进行来非常轻度的异步化, 所以对超时的问题需要格外注意, 超过超时时间会直接查询 DB, 处理不好会对 DB 造成压力, 因此需要测试系统高峰期 QPS 来调整机器数以及对应机器上的队列数最终决定合理的请求等待超时时间
2. 多实例部署的请求路由: 可能这个服务会部署多个实例, 那么必须保证对应的请求都通过 nginx 服务器路由到相同的服务实例上
3. 热点数据的路由导师请求的倾斜: 因为只有在商品数据更新的时候才会清空缓存, 然后才会导致读写并发, 所以更新频率不是太高的话, 这个问题的影响并不是特别大, 但是的确可能某些机器的负载会高一些
分布式缓存重建并发冲突解决方案
对于缓存生产服务, 可能部署在多台机器, 当 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 降级成为分发至所有的应用层 nginxstorm 需要保存上一次识别出来的热点 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 也没有的数据返回一个空标识的数据, 进而保存到各级缓存中, 因为有对数据修改的异步监听, 所以当数据有更新, 新的数据会被更新到缓存汇中
在这里给大家提供一个学习交流的平台, java 架构师群: 694549689
1. 具有 1-5 工作经验的, 面对目前流行的技术不知从何下手, 需要突破技术瓶颈的可以加群
2. 在公司待久了, 过得很安逸, 但跳槽时面试碰壁需要在短时间内进修跳槽拿高薪的可以加群
3. 如果没有工作经验, 但基础非常扎实, 对 java 工作机制, 常用设计思想, 常用 java 开发框架掌握熟练的可以加群
nginx 缓存失效导致 redis 压力倍增
可以在 nginx 本地, 设置缓存数据的时候随机缓存的有效期, 避免同一时刻缓存都失效而大量请求直接进入 redis
这个过程值得我们去深入学习和思考如果对你有帮助请动动小手关注下吧!
来源: http://www.jianshu.com/p/98ae99f92eec