前言

之前我们探讨了Mybatis中缓存模块的基本实现,对其中CacheKey和缓存的基本实现类PerpetualCache的核心代码进行了探索。传送门:Mybatis中的缓存模块实现

在上篇文章结尾的时候,留下了一个问题,就是Mybatis默认的实现类存在高并发下的缓存击穿的问题。这篇文章将解释缓存击穿以及对应的解决方案。

缓存击穿是对于高并发场景下二级缓存出现的一种现象。

前面的文章介绍了Mybatis进行一次查询是以SqlSession为单位,所以换句话说,缓存击穿就是发生在大量SqlSession未能从二级缓存中获取数据进而导致大量数据库IO操作的时候。

缓存击穿一款访问量巨大的软件产品,在使用Mybatis作为ORM框架时必须考虑的一点。

在了解缓存击穿之前,我们先来看一种Mybatis提供的缓存装饰器FifoCache,它内部在缓存实现类的基础之上,自己又添加了缓存淘汰的功能。

FifoCache

为什么说FifoCache是基于普通缓存装饰器的装饰器?

熟悉设计模式的开发者肯定都知道,有时候默认的系统模块可能只提供了基础的功能,但是由于场景问题,在某一个场景我们需要一个工具,除了提供原先的基础功能之外,还需要提供额外的特定的功能。

为了解决这样一个场景问题,又为了遵守开闭原则,诞生了一种设计模式叫做装饰器模式。装饰器模式往往要求装饰器中的功能开发者需要自己实现,同时持有被装饰对象的引用以保证基础功能的调用。

在JDK中,java.io下的二进制流模块就是装饰器模式的完美体现:比如基础的输入流为InputStream,那么BufferedInputStream就是InputStream的装饰器,在保证基本输入流操作的同时自身还为InputStream添加了缓冲的功能publicBufferedInputStream(InputStreamin,intsize){

super(in);

if(size<=0){

thrownewIllegalArgumentException("Buffer size <= 0");

}

buf=newbyte[size];

}

又比如DataInputStream,它也可以作为InputStream的装饰器publicDataInputStream(InputStreamin){

super(in);

}

前面的文章我们知道Cache的基本实现类为PerpetualCache,那么同样的,FifoCache作为它的装饰器,肯定要在被创建的时候拿到被装饰对象的引用:publicFifoCache(Cachedelegate){

this.delegate=delegate;//缓存基本实现类进行成员变量赋值

this.keyList=newLinkedList<>();

this.size=1024;

}

拿到基本的缓存实现类就能确保缓存的基本操作,然后我们主要来看一下它自身为缓存定制的淘汰机制,篇幅原因,这里贴出核心部分代码:publicclassFifoCacheimplementsCache{

privatefinalCachedelegate;//缓存基本实现类

privatefinalDequekeyList;//维护放入缓存的数据对应的key的队列

privateintsize;//允许缓存节点最大的数量

publicFifoCache(Cachedelegate){

this.delegate=delegate;

this.keyList=newLinkedList<>();//用链表来维护key

this.size=1024;//链表最大允许存储1024个key

}

@Override

publicvoidputObject(Objectkey,Objectvalue){

cycleKeyList(key);

delegate.putObject(key,value);

}

privatevoidcycleKeyList(Objectkey){

keyList.addLast(key);//从尾部添加新的key

if(keyList.size()>size){

//超出链表最大维护数量

ObjectoldestKey=keyList.removeFirst();//将最早加入的key清除

delegate.removeObject(oldestKey);//通过key将最早加入缓存的数据清除

}

}

}

通过上面的代码和注释,不难看出FIFOCache的缓存淘汰机制是通过一个固定大小的链表来实现的,链表维护被放入缓存中的数据对应的key,当到达链表最大长度的时候,最早加入缓存的数据以及最早加入链表的key都将被清除。

缓存击穿

那么问题来了,试想一下这样一个时间点,存在很多的sqlSession对同一个数据进行查询操作,恰巧此时二级缓存中的该数据作为最早被加入的数据,由于当下链表已超出了最大的size,所以该数据被移除了,那么将会导致大量的线程去对数据库进行查询操作,使得数据库IO一瞬间激增,磁盘和数据库的压力巨大,存在数据库宕机的风险。

上述的场景中,线程通过sqlSession进行查询,在缓存中无法获取数据的情况下,最终将去数据库中获取数据,这种情况我们称之为缓存击穿。当一个系统一瞬间发生大量缓存击穿现象的时候,容易引起系统宕机。

在Mybatis使用FIFOCache或者ScheduledCache(有超时丢弃机制的缓存,源码实现也较为简单,这里不再赘述),容易出现缓存击穿的问题。

BlockingCache

缓存击穿的关键是会在短时间内使我们的数据库压力激增,原因是Mybatis提供的普通的缓存工具类或者是装饰器没有做任何的并发控制。那么是不是控制线程顺序访问我们的缓存,就可以缓解数据库的压力了呢?没错,BlockingCache就是基于这么一个原理,让我们的系统即使发生了缓存击穿也不会导致宕机。

我们来看看内部的代码实现:publicclassBlockingCacheimplementsCache{

privatelongtimeout;//尝试锁住缓存记录的时间

privatefinalCachedelegate;//缓存实现类

privatefinalConcurrentHashMaplocks;//维护每个缓存的key对应的锁的集合

publicBlockingCache(Cachedelegate){

this.delegate=delegate;

this.locks=newConcurrentHashMap<>();

}

@Override

publicStringgetId(){

returndelegate.getId();

}

@Override

publicintgetSize(){

returndelegate.getSize();

}

@Override

publicvoidputObject(Objectkey,Objectvalue){

try{

delegate.putObject(key,value);//数据放入缓存

}finally{

releaseLock(key);//释放当前CacheKey所对应的锁

}

}

@Override

publicObjectgetObject(Objectkey){

acquireLock(key);//获取当前CacheKey所对应的锁,并尝试上锁

Objectvalue=delegate.getObject(key);

if(value!=null){

releaseLock(key);//如果缓存中存在数据,则释放锁直接返回

}

//如果缓存中不存在,则继续持有锁

//并且通过当前线程去数据库获取数据,直到当前线程调用putObject将数据库中的数据放入缓存才释放锁

returnvalue;

}

@Override

publicObjectremoveObject(Objectkey){

// despite of its name, this method is called only to release locks

releaseLock(key);//删除缓存中某个数据的时候,也释放该数据对应的锁

returnnull;

}

@Override

publicvoidclear(){

delegate.clear();

}

privateReentrantLockgetLockForKey(Objectkey){

returnlocks.computeIfAbsent(key,k->newReentrantLock());

}

privatevoidacquireLock(Objectkey){

Locklock=getLockForKey(key);//根据当前的CacheKey实例生成一把锁

if(timeout>0){

//如果开发者设置了等待时间,则允许线程在等待时间内自旋尝试获取锁

try{

booleanacquired=lock.tryLock(timeout,TimeUnit.MILLISECONDS);

if(!acquired){

thrownewCacheException("Couldn't get a lock in "+timeout+" for the key "+key+" at the cache "+delegate.getId());

}

}catch(InterruptedExceptione){

thrownewCacheException("Got interrupted while trying to acquire lock for key "+key,e);

}

}else{

lock.lock();//没有设置等待时间则只尝试一次

}

}

privatevoidreleaseLock(Objectkey){

ReentrantLocklock=locks.get(key);

if(lock.isHeldByCurrentThread()){

//如果当前线程拥有该可冲入锁的执行权,释放锁

lock.unlock();

}

}

publiclonggetTimeout(){

returntimeout;

}

publicvoidsetTimeout(longtimeout){

this.timeout=timeout;

}

}

BlockingCache通过内部的一个维护可重入锁的集合来达到防止缓存击穿引起数据库压力激增的问题。其中CacheKey对应的唯一数据在缓存中拥有一把唯一的锁。

比如,当一个线程对一个数据发起查询,在BlockingCache机制下,它会先获取这条数据的key对应的锁,获取成功后,获取key对应的value,如果value存在,直接释放锁并返回结果;如果value不存在,那么依旧将结果返回,结果为null,同时继续持有该锁,这时当前线程就会去数据库获取数据,直到数据获取成功并放入缓存才释放锁。后续其他线程就可以直接从缓存中获取了。

一句话总结,BlockingCache在发生缓存击穿时,控制同一时间只能一个线程访问数据库,并确保同一时间向缓存读取和写入的是同一个线程。

这就是它解决缓存击穿的原理,其实并不是防止击穿,而是在击穿发生的时刻通过锁来解决数据库压力问题。

关于缓存击穿和BlockingCache我们就了解到这里,Mybatis的源码在实现上其实挺精妙,研究的时候也比较轻松。如有不恰当的地方,欢迎指正!

mysql缓存击穿_Mybatis中的缓存击穿相关推荐

  1. 360浏览器清除缓存_手机中的缓存是什么?

    手机中的缓存是什么? 手机缓存就是数据交换的缓冲区(称作Cache).缓存是CPU的一部分,它存在于CPU中,而CPU存取数据的速度则非常的快,一秒钟能够存取.处理十亿条指令和数据(术语:CPU主频1 ...

  2. 缓存:网络中的缓存。

    网络中的缓存位于客户端和服务端之间,代理或响应客户端的网络请求,从而对重复的请求返回缓存中的数据资源.同时,接受服务端的请求,更新缓存中的内容. Web代理缓存 Web代理几乎是伴随着互联网诞生的,常 ...

  3. php中怎么让图片没有缓存,关于项目中图片缓存的问题

    之前用的是iis所以可能没有这些问题,后来换了nginx之后发现图片缓存问题很严重,本项目用的是thinkphp5框架: 浏览器.runtime.session.cookie.加参数,后台,所有缓存都 ...

  4. django中的缓存以及跨域

    django中的缓存 先来了解以下问题?(面试会问) 如何提高网站的并发量: QPS:Queries Per Second意思是"每秒查询率",是一台服务器每秒能够相应的查询次数, ...

  5. Django框架深入了解_05 (Django中的缓存、Django解决跨域流程(非简单请求,简单请求)、自动生成接口文档)(一)

    阅读目录 一.Django中的缓存: 前戏: Django中的几种缓存方式: Django中的缓存应用: 二.跨域: 跨域知识介绍: CORS请求分类(简单请求和非简单请求) 示例: 三.自动生成接口 ...

  6. okhttp配置缓存策略_一网打尽OkHttp中的缓存问题

    看到很多小伙伴对OkHttp的缓存问题并不是十分了解,于是打算来说说这个问题.用好OkHttp中提供的缓存,可以帮助我们更好的使用Retrofit.Picasso等配合OkHttp使用的框架.OK,废 ...

  7. 一网打尽OkHttp中的缓存问题

    看到很多小伙伴对OkHttp的缓存问题并不是十分了解,于是打算来说说这个问题.用好OkHttp中提供的缓存,可以帮助我们更好的使用Retrofit.Picasso等配合OkHttp使用的框架.OK,废 ...

  8. 缓存更新的Design Pattern -- 缓存专题(2)

    黄粱一梦终须醒, 无根无极本归尘. 金龙飞天归何处, 不如凡间做真人! 上个周忙活了一周上线工作,因为银行的行业性质,与国家安全.国家政策强相关.所以对上线版本的质量要求都很高,因而每一次上线前担惊害 ...

  9. java map缓存6_Java内存缓存-通过Map定制简单缓存

    缓存 在程序中,缓存是一个高速数据存储层,其中存储了数据子集,且通常是短暂性存储,这样日后再次请求此数据时,速度要比访问数据的主存储位置快.通过缓存,可以高效地重用之前检索或计算的数据. 为什么要用缓 ...

最新文章

  1. Android安全加密:消息摘要Message Digest
  2. robo3t设置密码链接
  3. BZOJ 3362 Navigation Nightmare 带权并查集
  4. 易语言html实现报表打印,易语言报表统计功能例程可打印
  5. composer安装
  6. Java面试题!5年经验Java程序员面试27天,看看这篇文章吧!
  7. 38张史上最全的IT架构师技能图谱(高清版下载)
  8. Xcode调试技巧总结
  9. html网页制作代码大全:庆余年——电影网站7页,不包含js 有登陆注册,表格 table布局 ,有的登录注册页面,内嵌 css
  10. 一位资深程序员大牛给予Java初学者的学习路线建议
  11. 课后习题7.11 医院内科有A,B,C,D,E,F,G共7位医生,每人在一周内要值一次夜班,排班的要求是: (1)A医生值班日比C医生晚1天; (2)D医生值班日比E医生晚2天; (3)B医生值班日比
  12. 安装虚拟计算机有什么用途,为什么要使用虚拟机软件?——VMware的介绍与安装...
  13. 数电实验三 数据选择器及其应用 任务一:用74151芯片采用降维的方法实现F=ABC+ABD+ACD+BCD; 任务二:用74151芯片采用降维方式实现F=BCD反+BC反+A反D;
  14. USB Type-C引脚解析 CC、DFP、UFP、DRP用途解析【转】
  15. Docker 镜像管理,显示本地镜像,查找镜像,删除镜像,镜像拉取,查看镜像的具体信息,镜像的导入和导出,将配置后的镜像commit成自己的镜像,docker history,等
  16. Python格式化字符串的4种方式
  17. [弱校联萌2016]2016弱校联盟十一专场10.5
  18. 【服务器数据恢复】华为OceanStor T系列存储数据恢复案例
  19. DVWA-master通关教程
  20. 二分法求函数的零点c++

热门文章

  1. 跑得快群谁有?运营经验分享
  2. TensorFlow之一—参数初始化
  3. 求字符串中出现最多次数的字符和次数
  4. vue element的日期选择器 ,选择日期时间范围的限制
  5. python神经网络:女生颜值打分器(一)
  6. webstorm 破解方法
  7. 浅析Javascript匿名函数与自执行函数
  8. 《转载+完善》java实现中文大写金额转小写数字
  9. suse linux 如何修改主机名,就这样轻松在Suse修改主机名
  10. oneinStack配置无域名网站