缓存穿透、缓存击穿、缓存雪崩看这篇就够了,文末还送福利哦!
当我们进行架构设计时,缓存是提高高性能的最重要也是最常用的组件之一。数据库的瓶颈在于磁盘I/O,虽然现如今关系数据库的部分应用场景采用了NoSQL作为替代,但依然没能摆脱磁盘I/O的性能问题。缓存的妙处就是在提高性能的同时,也保护了下游数据库,避免I/O压力过大导致宕机。
常用的缓存有单机版的EhCache、分布式版的Memcache和Redis,都属于K-V类型的存储,Key与Value一一对应。当然如果较真的话他们也属于NoSQL范畴,比如Redis弥补了关系数据不能存储结构数据的缺憾。虽然这些缓存中间件已经非常成熟和稳定,也得到了广泛的应用,但设计一个良好的缓存系统还是会遇到很多问题需要解决、方案也需要取舍。
缓存的应用场景
每一个技术或中间件都有自己的应用场景,在这些场景中可以发挥它最大的优势。如果不熟悉他们的应用场景,愣是生搬硬套,只会事倍功半,南辕北辙。缓存的应用场景有如下几种:
- 高频访问的数据:限于磁盘I/O的瓶颈,对于高频访问的数据,需要缓存起来提高性能,降低下游数据库的压力冲击;
- 复杂运算的结果:对于需要耗费CPU、经过复杂运算才能获得的结果,需要缓存来,做到“一劳长时间逸",如count(id)统计论坛在线人数;
- 读多写少:每次读都需要select甚至join很多表,数据库压力大,由于写得少,容易做到数据的一致性,非常适合缓存的应用;
- 一致性要求低:由于缓存的数据来源于数据库,在高并发时数据不一致性就比较凸显,不一致的问题可以解决但代价不菲;
本篇主要来讲讲架构设计中的缓存穿透、缓存击穿、缓存雪崩产生的原因和缓解措施,技术来不得半点含糊,要知其然知其所以然。
缓存穿透 Cache Penetration
缓存穿透是指数据库中没有符合条件的数据,缓存服务器中也就没有缓存数据,导致业务系统每次都绕过缓存服务器查询下游的数据库,缓存服务器完全失去了其应用的作用。如果黑客试图发起针对该key的大量访问攻击,数据库将不堪重负,最终可能导致崩溃宕机。从上图可以看出直接穿过缓存到达下游数据库,大致业务流程如下
可采取的缓解措施
1.存储空值/默认值
虽然数据库中没有符合条件的数据,可以考虑缓存空值或适合业务的默认值,来缓解这种情况。为了降低数据的不一致需要注意两点:1. 缓存的过期时间需要设置的比较短;2. 当数据库数据更新时也需要及时更新缓存中对应的数据。
2.布隆过滤器(Bloom Filter)
布隆过滤器是一种比较巧妙的概率性数据结构,它可以告诉你数据一定不存在或可能存在,相比Map、Set、List等传统数据结构它占用内存少、结构更高效。比如有一个下面这样的数据结构,每个存储位存储的都是一个big,即0或1
当我们向缓存中插入key为name的缓存数据时,先使用N种不同的hash函数做N次hash得到N个哈希值,在上面的数据结构中找到对应哈希值的下标,并把存储数据设置为1。假如N=3,我们可以使用hash1、hash2、hash3分别计算出了哈希值为8,15和13,则将其对应下标的数据设置为1,如下图
此时你如果想判断一个缓存key是否存在,就采用同样的l流程:3次hash、根据哈希值寻找下标、取出存储的数据。如果存储的数据不全都是1,也就意味着缓存中不存在此key,但都是1也只表示可能存在。不过没关系,我们只需要否定的意图就能达到目标了(O ^ ~ ^ O)。
以上两种缓解措施在不同的应用场景可以做些适当的选择:如果访问量大可以使用第一种方案简单粗暴;如果访问量低但涉及的key比较多,则可采用第二种方案。
缓存击穿 Cache Breakdown
缓存击穿是指当某一key的缓存过期时大并发量的请求同时访问此key,瞬间击穿缓存服务器直接访问数据库,让数据库处于负载的情况。
缓存击穿一般发生在高并发的互联网应用场景,可采用的如下的缓解措施
1.锁更新
可以使用(分布式)锁,只让一个线程更新key,其他线程等待,直到缓存更新释放锁。比如简单粗暴的synchronized关键字
public synchronized String getCacheData() {
String cacheData = "";
//Read redis
cacheData = getDataFromRedis();
if (cacheData.isEmpty()) {
//Read database
cacheData = getDataFromDB();
//Write redis
setDataToCache(cacheData);
}
return cacheData;
}
但synchronized这个锁太宽泛,会造成大量的请求阻塞,性能极低,进一步优化缩小锁的范围
static Object lock = new Object();
public String getCacheData() {
String cacheData = "";
//Read redis
cacheData = getDataFromRedis();
if (cacheData.isEmpty()) {
synchronized (lock) {
//Read database
cacheData = getDataFromDB();
//Write redis
setDataToCache(cacheData);
}
}
return cacheData;
}
学无止境,还可以继续优化,使用互斥锁 (✧◡✧):得到锁的线程就读数据写缓存,没得到锁的线程可以不用阻塞,继续从缓存中读数据,如果没有读到数据就休息会再来试试。
public String getCacheData(){
String result = "";
//Read redis
result = getDataFromRedis();
if (result.isEmpty()) {
if (reenLock.tryLock()) {
try {
//Read database
result = getDataFromDB();
//Write redis
setDataToCache(result);
}catch(Exception e){
//...
}finally {
reenLock.unlock (); // release lock
}
} else {
//Note: this can be combined with the
// following double caching mechanism:
//If you can't grab the lock,
// query the secondary cache
//Read redis
result = getDataFromRedis();
if (result.isEmpty()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
//...
}
return getCacheData();
}
}
}
return result;
}
2.异步更新
还有个可行的方案就是把缓存设置为永久不过期,异步定时更新缓存。比如后台有个值守线程专门定时更新缓存,但一般还要定时频繁地去检测缓存,一旦发现被踢掉(比如被缓存的失效策略FIFO、LFU、LRU等)需要立刻更新缓存,但这个“定时”的度是比较难掌握的,实现简单但用户体验一般。
异步更新机制还比较适合缓存预热,缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统,避免在用户请求时才缓存数据,提高了性能。
缓存雪崩 Cache Avalanche
缓存雪崩是指当大量缓存同时过期或缓存服务宕机,所有请求的都直接访问数据库,造成数据库高负载,影响性能,甚至数据库宕机,它和缓存击穿的区别在于失效key的数量。
可以采取的缓解措施
1.集群
高可用方案的本质就是冗余,集群是其实现方式之一,使用集群可以避免服务单点故障,但集群也带来了复杂度,好在很多成熟的中间件都有稳妥的集群方案,比如Redis集群。
2.过期时间
为了避免大量的缓存在同一时间过期,可以把不同的key过期时间随机生成,但随机可能会对业务有影响,但可以根据业务特点进行设置,总之是让过期时间分散。也有是通过定时刷新过期时间,类似于refresh token机制。
3.服务降级或熔断
服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的fallback(退路)错误处理信息。
熔断和降级都可以间接保证了整个系统的稳定性和可用性。
End
版权归@码农神说所有,转载须经授权,翻版必究
转载可联系助手,微信号:codeceo-01
- 完善RecyclerView,添加首尾视图
- 初识Node.js
- Linux学习 - SED操作,awk的姊妹篇
- Android面试之高级篇
- 解密所有APP运行过程中的内部逻辑
- RecyclerView数据动态更新
- android PakageManagerService启动流程分析
- RFID入门:Mifare1智能水卡破解分析
- RecyclerView点击事件处理
- Python 实现一个火车票查询的工具
- HTTPS迎来春天:Chrome计划将所有HTTP标记为不安全
- View绘制流程
- Linux Rootkit系列一:LKM的基础编写及隐藏
- Swift基础语法(常量变量、数据类型、元组、可选、断言)
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 微信小程序开发实战(29):控制背景音乐
- 面试:如何从 100 亿 URL 中找出相同的 URL?
- Spring Boot+Gradle+ MyBatisPlus3.x搭建企业级的后台分离框架
- 不要再对类别变量进行独热编码了
- 面试Java基础问题汇总
- K8s集群上使用Helm部署2.4.6版本Rancher集群
- 一个工作三年的同事,居然还搞不清深拷贝、浅拷贝...
- 太有意思了,教你实现实现王者荣耀团战!
- 动画:什么是基数排序?
- 一个有意思的分钱模拟问题
- 如何快速的开发一个完整的直播购物源码,基础篇
- 「拥抱开源」Nacos 实战篇
- 仅2M!免费软件又一次干掉了付费版
- python爬虫学习 爬取幽默笑话网站
- 如何用Python快速优雅的批量修改Word文档样式?