01 没缓存的日子
对于 web 来说, 是用户量和访问量支持项目技术的更迭和前进. 随着服务用户提升. 可能会出现一下的一些状况:
页面并发量和访问量并不多, MySQL 足以支撑自己逻辑业务的发展. 那么其实可以不加缓存. 最多对静态页面进行缓存即可.
页面的并发量显著增多, 数据库有些压力, 并且有些数据更新频率较低反复被查询或者查询速度较慢. 那么就可以考虑使用缓存技术优化. 对高命中的对象存到 key-value 形式的 Redis 中, 那么, 如果数据被命中, 那么可以省经效率很低的 db. 从高效的 Redis 中查找到数据.
当然, 可能还会遇到其他问题, 你可以需要静态页面本地缓存, cdn 加速, 甚至负载均衡这些方法提高系统并发量. 这里就不做介绍.
02 缓存思想无处不在
我们从一个算法问题开始了解缓存的意义.
问题 1: 输入一个数 n(n<20), 求 n!;
分析 1: 单单考虑算法, 不考虑数值越界问题. 当然我们知道 n!=n * (n-1) * (n-2) * ... * 1= n * (n-1)!; 那么我们可以用一个递归函数解决问题.
这样每输入求一次需要执行 n 次.
问题 2: 输入 t 组数据(可能成百上千), 每组一个 x(n<20), 求 x!;
分析 2:
时间复杂度才 O(n). 这里的思想就和缓存思想差不多. 先将数据在 jiecheng[21]数组中储存. 执行一次计算. 当后面继续访问的时候就相当于当问静态数组值. 为 O(1). 就能大大的减少查询, 执行成本啦!
03 缓存的应用场景
缓存适用于高并发的场景, 提升服务容量. 主要是将从经常被访问的数据或者查询成本较高从慢的介质中存到比较快的介质中, 比如从硬盘 ->内存. 我们知道大多数关系数据库是基于硬盘读写的, 其效率和资源有限, 而 Redis 等非关系型就是基于内存存储. 其效率差别很大. 当然, 缓存也分为本地缓存和服务端缓存, 这里只讲 Redis 的服务端缓存.
举个例子. 例如如果一个接口 sql 查询需要 2s. 你每次查询都会 2s 并且加载的时候都会等在, 这个长期等待给用户的体验是非常糟糕的. 而用户能够接受的往往是第一次的等待. 如果你用了缓存技术. 你第一次查询放到 Redis 里面. 然后数据再从 Redis 返回给你. 后面当你继续访问这个数据的时候. 查询到 Redis 中有备份, 那么不需要通过 db 直接能从 Redis 中获取数据. 那么, 你想想, 从一个 key value 的 Nosql 中取一个 value 能要多久呢!
所以对于像样的, 有点规模的网站, 缓存 isnecessary 的. Redis 也是必不可少的. 并且服务端的缓存设计也是要根据业务有所区别的. 也要防止占用内存过大, Redis 雪崩等问题.
04 需要注意的问题
缓存使用不当会带来很多问题. 所以需要对一些细节进行认真考量和设计. 笔者对于分布式的经验并不是很丰富, 就相对于笔者的眼中谈谈缓存设计不好会带来那些问题.
(1)是否用缓存
现在不少项目, 为了缓存而缓存, 然而缓存并不是适合所有场景, 比如如果对数据一致性要求极高, 又或者数据频繁更改而查询并不多. 有的可以不需要缓存. 因为如果使用 Redis 缓存多多少少可能会遇到数据一致性问题. 那你可以考虑使用 Redis 做成分布式锁去锁 sql 的数据. 同样如果频繁更新数据, 那么 Redis 能起到的作用就仅仅是多了一层中转站. 反而浪费资源. 使得传输过程臃肿.
(2)过期策略选择
大部分场景不适合缓存一致存在, 首先, 你的 sql 数据库的内容可能很多就不说了, 另外, 返回给你的对象如果是完整的 pojo 对象还好, 但是如果是使用不同参数各种关联查询出来的结果那么 Redis 中会储存太多冷数据. 占用资源而得不到销毁. 我们学过操作系统也知道在计算机的缓存实现中有)先进先出的算法 (FIFO); 最近最少使用算法(LRU); 最佳淘汰算法(OPT); 最少访问页面算法(LFR) 等磁盘调度算法. 对于 Web 开发也可以借鉴. 根据时间来的 FIFO 是最好实现的. 因为 Redis 在全局 key 支持过期策略.
而开发中可能还会遇到其他问题. 比如过期时间的选择上, 如果过久会导致数据聚集. 而过少可能导致频繁查询数据库甚至可能会导致缓存雪崩等问题.
所以, 过期策略一定要设置. 并且对于关键 key 一定要小心谨慎设计.
(3)数据一致性问题★
上面其实提到数据一致性问题. 如果对一致性要求极高那么不建议使用缓存. 下面稍微梳理一下缓存的数据.
在 Redis 缓存中经常会遇到数据一致性问题. 对于一个缓存. 下面罗列逼仄
1 读
read: 从 Redis 中读取, 如果 Redis 中没有, 那么就从 MySQL 中获取更新 Redis 缓存. 该流程图描述常规场景. 一般没啥争议.
2 写 1: 先更新数据库, 再更新缓存(普通低并发)
更新数据库信息, 再更新 Redis 缓存. 这是常规做法, 缓存基于数据库, 取自数据库. 但是其中可能遇到一些问题. 例如上述如果更新缓存失败(宕机等其他状况), 将会使得数据库和 Redis 数据不一致. 造成 DB 新数据, 缓存旧数据.
3 写 2: 先删除缓存, 再写入数据库(低并发优化)
解决的问题
这种情况能够有效避免写 1 中防止写入 Redis 失败的问题. 将缓存删除进行更新. 理想是让下次访问 Redis 为空去 MySQL 取得最新值到缓存中. 但是这种情况仅限于低并发的场景中而不适用高并发场景.
存在的问题
写 2 虽然能够看似写入 Redis 异常的问题. 看似较为好的解决方案但是在高并发的方案中其实还是有问题的. 我们在写 1 讨论过如果更新库成功, 缓存更新失败会导致脏数据. 我们理想是删除缓存让下一个线程访问适合更新缓存. 问题是: 如果这下一个线程来的太早, 太巧了呢?
因为多线程你也不知道谁先谁后, 谁快谁慢. 如上图所示情况, 将会出现 Redis 缓存数据和 MySQL 不一致. 当然你可以对 key 进行上锁. 但是锁这种重量级的东西对并发功能影响太大, 能不用锁就别用! 上述情况就高并发下依然会造成缓存是旧数据, DB 是新数据. 并且如果缓存没有过期这个问题会一致存在.
4 写 3: 延时双删策略
这个就是延时双删策略, 能过缓解在写 2 中在更新 MySQL 过程中有读的线程进入造成 Redis 缓存与 MySQL 数据不一致. 方法就是删除缓存 ->更新缓存 ->延时 (几百 ms)(可异步) 再次删除缓存. 即使在更新缓存途中发生写 2 的问题. 造成数据不一致, 但是延时 (具体实间根据业务来, 一般几百 ms) 再次删除也能很快的解决不一致.
但是就写的方案其实还是有漏洞的, 比如第二次删除错误, 多写多读高并发情况下对 MySQL 访问的压力等等. 当然你可以选择用 mq 等消息队列异步解决. 其实实际的解决很难顾及到万无一失, 所以不少大佬在设计这一环节可能会因为一些纰漏会被喷. 作为菜菜的笔者在这里就更不献丑了, 策略只是提供大纲, 具体设计还是需要自己团队实践和摸索. 并且也对一致性的要求级别有所区别.
5 写 4: 直接操作缓存, 定期写入 sql(适合高并发)
当有一堆并发 (写) 扔过来的后, 前面几个方案即使使用消息队列异步通信但也很难给用户一个舒适的体验. 并且对大规模操作 sql 对系统也会造成不小的压力. 所以还有一种方案就是直接操作缓存, 将缓存定期写入 sql. 因为 Redis 这种非关系数据库又基于内存操作 KV 相比传统关系型要快很多(找值最多多碰撞几次).
上面适用于高并发情况下业务设计, 这个时候以 Redis 数据为主, MySQL 数据为辅助. 定期插入(好像数据备份库一样). 当然, 这种高并发往往会因为业务对读, 写的顺序等等可能有不同要求, 可能还要借助消息队列以及锁完成针对业务上对数据和顺序可能会因为高并发, 多线程带来的不确定性和不稳定性. 提高业务可靠性.
总之, 越是高并发, 越是对数据一致性要求高的方案在数据一致性的设计方案需要考虑和顾及的越复杂, 越多. 上述也是笔者针对 Redis 数据一致性问题的学习和自我发散 (胡扯) 学习. 如果有解释理解不合理或者还请联系告知!
05 缓存穿透, 缓存雪崩和缓存击穿
如果不了解, 可能对这几个概念都不了解, 听着感觉太高大上, 至少笔者刚开始是这么觉得, 本文并不是详细介绍如何解决和完美解决, 更主要的是认识和认知吧.
(1)Redis 缓存穿透
1 理解
重在穿透吧, 也就是访问透过 Redis 直接经过 MySQL, 通常是一个不存在的 key, 在数据库查询为 null. 每次请求落在数据库, 并且高并发. 数据库扛不住会挂掉.
2 解决方案
可以将查到的 null 设成该 key 的缓存对象.
当然, 也可以根据明显错误的 key 在逻辑层就就行验证.
同时, 你也可以分析用户行为, 是否为故意请求或者爬虫, 攻击者. 针对用户访问做限制.
其他等等, 比如看到其他人用布隆过滤器 (超大型 hashmap) 过滤.
(2)Redis 缓存雪崩
1 理解
雪崩, 就是某东西蜂拥而至的意思, 像雪崩一样. 在这里, 就是 Redis 缓存集体大规模集体失效, 在高并发情况下突然使得 key 大规模访问 MySQL, 使得数据库崩掉. 可以想象下国家人口老年化. 以后那天人集中在 70-80 岁, 就没人干活了. 国家劳动力就造成压力.
2 解决方案
通常的解决方案是将 key 的过期时间后面加上一个随机数, 让 key 均匀的失效.
考虑用队列或者锁让程序执行在压力范围之内, 当然这种方案可能会影响并发量.
(3)Redis 缓存击穿
1 理解
击穿和穿透不同, 穿透的意思是想法绕过 Redis 去使得数据库崩掉. 而击穿你可以理解为正面刚击穿, 这种通常为大量并发对一个 key 进行大规模的读写操作. 这个 key 在缓存失效期间大量请求数据库, 对数据库造成太大压力使得数据库崩掉. 就比如在秒杀场景下 10000 块钱的 Mac 和 100 块的 Mac 这个 100 块的那个订单肯定会被抢到爆. 所以缓存击穿就是针对某个常用 key 大量请求导致数据库崩溃.
2 解决方案
能够达到这种场景的公司其实不多, 我也不清楚他们的具体处理方法, 但是一个锁拦截请求总是能防止数据库崩掉吧.
06 总结与感悟
其实缓存看起来, 理解起来看似简单然而实际上的设计方案非常有学问. 在细节设计上还会遇到消息队列, 布隆过滤器, 分布式锁, 服务降级, 熔断, 分流这些. 在缓存处理上甚至还有缓存预热 (提前缓存部分热点数据防止刚开始缓存全部命中导致服务崩掉) 等其他热门名词和问题这里就不做介绍了.
最后: 一波读者小福利~
读到这的朋友还可以免费领取一份收集的 Java 面试资料和 Java 核心知识体系文档及更多 Java 进阶知识笔记和视频资料.
来源: http://www.jianshu.com/p/1ae739d29d44