mybatis+redis实现分布式缓存+缓存面试题
一,mybatis缓存机制
mybatis提供了一级、二级缓存。
- 一级缓存:线程级别的缓存,也称为本地缓存或sqlSession级别的缓存,一级缓存是默认存在的,同一个会话中,查询两次相同的操作就会从缓存中取。
- 二级缓存:全局范围的缓存;除了当前sqlSession能用外,其他的也可以使用。二级缓存默认也是开启的,只需要在mapper文件中写一个<cache/>即可实现,二级缓存的实现需要pojo实现序列化的接口,否则会出错
二,不使用缓存情况
首先我们先试试没有开启缓存的情况:
查询sql的代码如下,我这里使用的service会调用mapper实现一个分页查询:
List<UserListVO> s1 = service.getUserListVOByPage(2, 5);
List<UserListVO> s2 = service.getUserListVOByPage(2, 5);
List<UserListVO> s3 = service.getUserListVOByPage(1, 3);
大致结果如下:
==> Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<== Columns: id, username, password, name, gender, role, balance, telephone
<== Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<== Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<== Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<== Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<== Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<== Total: 5==> Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<== Columns: id, username, password, name, gender, role, balance, telephone
<== Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<== Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<== Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<== Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<== Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<== Total: 5==> Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 0(Integer), 3(Integer)
<== Columns: id, username, password, name, gender, role, balance, telephone
<== Row: 1, root, root, 李显超, 0, 1, 98312, 17674574650
<== Row: 2, admin, admin, 舒芬, 1, 2, 1189, 18012113193
<== Row: 3, admin1, admin1, lxc, 1, 1, 1000, 11503995061
<== Total: 3
可以看到,执行了3次jdbc的查询操作,虽然前两次查询都是一模一样的,但是还是查询了多次数据库,这时候你可能会好奇不是有一级缓存吗?
因为我们这里使用的是service,而每一次service的调用都会重新创建一个新的数据库会话,当service方法调用结束后就会自动的提交事务、关闭会话,所有这种情况一级缓存是管不到的,只能使用二级缓存来减轻数据库的压力。
三,开启二级缓存
具体步骤:
1.最好在mybatis配置文件中显示的开启二级缓存
2.在mapper文件中加入使用二级缓存的标志
3.让要查询的pojo类实现序列化接口
这时候再来查询,结果如下:
Creating a new SqlSession
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.0
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@32fd5bc] will be managed by Spring
==> Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<== Columns: id, username, password, name, gender, role, balance, telephone
<== Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<== Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<== Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<== Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<== Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<== Total: 5
Releasing transactional SqlSession
Creating a new SqlSession
Registering transaction synchronization for SqlSession
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.5
Releasing transactional SqlSession
Creating a new SqlSessionCache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.3333333333333333
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@32fd5bc] will be managed by Spring
==> Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 0(Integer), 3(Integer)
<== Columns: id, username, password, name, gender, role, balance, telephone
<== Row: 1, root, root, 李显超, 0, 1, 98312, 17674574650
<== Row: 2, admin, admin, 舒芬, 1, 2, 1189, 18012113193
<== Row: 3, admin1, admin1, lxc, 1, 1, 1000, 11503995061
<== Total: 3
可以看到前两次相同的查询只查询了一次,第二次查询是在二级缓存中直接拿到的数据。简单的本地缓存也就实现了。
接下来是分布式缓存的实现
四,分布式缓存
上面提到的,实现二级缓存,需要在mapper中写上<cache/>标签,该标签实际对应着mybatis提供的Cache接口,该接口有多个实现类用于提供二级缓存的实现:
其中默认的使用PerpetualCache这个类:
它的实现就是将sql语句作为key,数据作为value存储在一个哈希表中,这是默认的情况,现在我们想要实现redis缓存,且这些缓存数据不放在服务器应用上,所有我们需要自己写一个实现了Cache接口的缓存类,把该缓存的内容存储到我们的redis服务器,拿缓存也是从redis服务器拿。
1.自定义redis cache
首先我们新建一个RedisCache类,实现Cache接口,实现它的几个方法即可,整体框架如下:
public class RedisCache implements Cache {@Overridepublic String getId() {return null;}@Overridepublic void putObject(Object o, Object o1) {}@Overridepublic Object getObject(Object o) {return null;}@Overridepublic Object removeObject(Object o) {return null;}@Overridepublic void clear() {}@Overridepublic int getSize() {return 0;}
}
实现了Cache的类使用时必须给一个构造方法,带一个String类型的id,可以参考上面的PerpetualCache
- getId():返回cache的唯一id,构造方法传过来的
- putObject(): 将数据放入缓存
- getObject():从缓存中取出数据
- removeObject():移除一个缓存
- clear():清空所有缓存
- getSize():取得缓存的个数
2.使用自定义的RedisCache
将cache标签的默认值改为我们新建的类
日后,该mapper中的二级缓存都会在redis中存或者取。
3.完善RedisCache
初始化的代码如下:
该类的对象必须是由mybatis创建的,mybatis会给每个mapper文件创建一个对象,每个对象的id都不一样。
private final String id;public RedisCache(String id) {this.id = id;
}@Override
public String getId() {return id;
}
到这一步,我们的RedisCache就能够启动了,但是并不能存取缓存数据,需要实现剩下的方法,但是有个问题,就是我们装缓存的容器需要时redis,所有需要操作Redis,我们可以使用上一篇讲到的redis整合springboot。但是RedisCache并不是IOC容器中的,不能够直接注入RedisTemplate,我们先自定义个一个获取IOC容器的工具类,用它来获取IOC容器中的redisTemplate:
@Component
//需要继承ApplicationContextAware
public class ApplicationContextUtils implements ApplicationContextAware {//获取到ioc容器private static ApplicationContext context;@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {context = applicationContext;}//获取ioc容器public static ApplicationContext getContext(){return context;}//直接获取bean对象public static Object getBean(String bean){return context.getBean(bean);}
}
现在我们就可以在RedisCache中获取IOC容器里面的对象了:
再实现所有方法,代码如下:
public class RedisCache implements Cache {private final String id;public RedisCache(String id) {this.id = id;}@Overridepublic String getId() {return id;}@Overridepublic void putObject(Object o, Object o1) {RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");redis.opsForHash().put(id, o, o1);}@Overridepublic Object getObject(Object o) {RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");return redis.opsForHash().get(id, o);}@Overridepublic Object removeObject(Object o) {RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");return redis.opsForHash().delete(id, o);}@Overridepublic void clear() {RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");redis.delete(id);}@Overridepublic int getSize() {RedisTemplate<Object, Object> redis = (RedisTemplate<Object, Object>) ApplicationContextUtils.getBean("objectRedisTemplate");return redis.opsForHash().size(id).intValue();}
}
把mybatis创建时提供的id作为我们redis中的key,创建一个hashmap的数据结构,hashmap的key就是查询的sql语句,value为对应的数据。
再说明一点,代码中的objectRedisTemplate是我自定义的redisTemplate,为了方便,把它的序列化方式全部改为了jackson的形式
4.测试
在启动我们的代码测试:
结果如下:
Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.0
==> Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 5(Integer), 5(Integer)
<== Columns: id, username, password, name, gender, role, balance, telephone
<== Row: 6, admin4, admin4, 33, 1, 2, 1000, 18586747625
<== Row: 7, admin7, admin5, 44, 1, 2, 1000, 18749888293
<== Row: 8, admin8, admin5, 44, 1, 2, 1000, 17488660630
<== Row: 9, admin9, admin5, 44, 1, 2, 1000, 17173666055
<== Row: 10, admin10, admin5, 44, 1, 2, 1000, 16749049960
<== Total: 5Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.5Cache Hit Ratio [com.lxc.sales.mapper.UserMapper]: 0.3333333333333333
==> Preparing: select u.id,u.username,u.password,u.name,u.gender,u.role,u.balance,u.telephone from user u where status=0 limit ?,?
==> Parameters: 0(Integer), 3(Integer)
<== Columns: id, username, password, name, gender, role, balance, telephone
<== Row: 1, root, root, 李显超, 0, 1, 98312, 17674574650
<== Row: 2, admin, admin, 舒芬, 1, 2, 1189, 18012113193
<== Row: 3, admin1, admin1, lxc, 1, 1, 1000, 11503995061
<== Total: 3
可以看到,使用到了二级缓存,重复的查找只查找了一次,第二次是在缓存中获取到的,redis中的数据如下:
正好两个对应有两个键值对,没有任何问题。这两个查询的值已经到我们的redis服务器中了,如果再启动一次测试代码,那么更简单,直接全部在redis服务器中取,一次数据库的查询都不需要。
五,出现的问题
再者,当我们执行一个增删改操作时
就会调用clear方法,清空redis中该key对应的所有数据,也就是以mapper文件为单位进行情况,根据id为标志。并不能影响到其他的表。
如果多张表不存在任何的关联查询,就不会出现问题,但如果其中有关联查询,如果修改了其中一个表,紧接着删除了该表的所有缓存,但另一张关联的表是不会情况缓存的,这时候就会出现查询到的数据与数据库不统一的情况。
这时候就需要修改一些,使得我们有关联关系的两张表增删改其中的一张表就清空所有的关联表的缓存,不仅仅删除自身的缓存。所有我们就不能在每张表都设置一个cache标签了,让两个有关联关系的表共用一个RedisCache对象,如下:
只需要一个cache标签即可,这样的话这两个mapper的缓存就在一起了,清空也是全清空。
六,优化
这一步主要是对缓存的键值对的一个优化措施,从上面的图片可以看到,key对应于一条sql语句加上其他的一些信息,看起来很冗长,这样会影响redis的性能,我们要尽可能设计的简洁一下。
我们的目的是让key变短,且必须是唯一的,不能够冲突,这一特点可以使用加密算法那一模板的报文摘要技术,把一个长的数据变为一个固定长度的数据,且能够唯一的区分。
最常用的报文摘要就是即MD5,我们就将key进行MD5加密,再存放到redis服务器中。
写为这种形式:
重新清空redis,并运行查询后结果如下:
键名的长度得到了明显的缩减,查找速度也会变快一点。
相应的也能给缓存设置一个超时时间。
六,面试题
缓存穿透
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在(如查找id为-1的数据),这样缓存永远不会生效,这些恶意请求都会到达数据库,让数据库承受巨大的压力。
有常用的两种解决方法:
- 缓存空对象:把查到为空的数据也缓存在缓存中,下次再有相同的形式查找就会从缓存中拿到空数据。
- 优点:实现简单,维护方便
- 缺点:1.额外的内存消耗(可能会恶意使用随机值查找),解决方法为设置超时时间;2.可能造成短期的不一致
- 布隆过滤:请求时先访问布隆过滤器,如果存在要查的数据就放行,如果不存在就拒绝。
- 优点:内存占用小,没有多余key
- 缺点:可能误判;实现复杂
- 实时监控:发现Redis的命中率变低了,就记性排查
另外还可以通过增强id的复杂度,避免被操作id的规律、做好基础校验等主动的解决方案。
缓存雪崩
缓存雪崩是指在某一时期,大量的缓存同时失效或者redis服务器宕机,导致大量的请求到达数据库,带来巨大的压力。
解决方案:
- 给不同的缓存添加不同的过期时间(常用的数据ttl更长,冷门的ttl更短)
- 利用rediis集群提高服务的可用性,防止宕机
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存,nginx缓存+redis缓存+其他缓存
缓存击穿
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
和雪崩的区别就是不是大量的key过期,redis还是正常状态,但数据库却崩了。
常用解决方案:
- 互斥锁:同时只让一个线程查询数据库,其他的都等待从缓存中取,实现简单,强一致性,但性能差,可用性变低了。
- 逻辑过期:永久存储该数据,但在数据中额外存储一个逻辑的过期时间。取到该数据时检测到过期了,那还是返回该数据,只不过会再新开一个线程去同步数据库的新数据到缓存。最终一致性,实现复杂。
- 预先设置热门数据:在redis高峰访问之前,把热门的数据提前存到redis里,并加大热门数据的ttl
- 实时调整:监控热门数据,调整key的ttl
mybatis+redis实现分布式缓存+缓存面试题相关推荐
- 【分布式缓存系列】Redis实现分布式锁的正确姿势
一.前言 在我们日常工作中,除了Spring和Mybatis外,用到最多无外乎分布式缓存框架--Redis.但是很多工作很多年的朋友对Redis还处于一个最基础的使用和认识.所以我就像把自己对分布式缓 ...
- Redis 5.0.8+常见面试题(单线程还是多线程、先更新缓存还是数据库、雪崩穿透击穿解决办法...)
Redis 6.0 保姆级教程(含微服务案例与完整面试题):https://www.yuque.com/yuxuandmbjz/redis Redis是单线程还是多线程 ?为什么这么设计 ? Redi ...
- Redis 实现分布式缓存
缓存 1. 什么是缓存? 缓存就是数据交换的缓冲区,用于临时存储数据(使用频繁的数据).当用户请求数据时,首先在缓存中寻找,如果找到了则直接返回.如果找不到,则去数据库中查找 缓存的本质就是用空间换时 ...
- 分布式缓存的面试题8
1.面试题 了解什么是redis的雪崩和穿透?redis崩溃之后会怎么样?系统该如何应对这种情况?如何处理redis的穿透? 2.面试官心里分析 其实这是问到缓存必问的,因为缓存雪崩和穿透,那是缓存最 ...
- Mybatis自定义分布式二级缓存实现与遇到的一些问题解决方案!
Mybatis自定义分布式二级缓存实现与遇到的一些问题解决方案! 参考文章: (1)Mybatis自定义分布式二级缓存实现与遇到的一些问题解决方案! (2)https://www.cnblogs.co ...
- redis 缓存 2023面试题总结
文章目录 1. 说说你对缓存的理解,什么是缓存 2. 哪些地方可以用到缓存,或者说缓存的分类 3. client端缓存具体有哪些,怎么实现 4. http缓存 Cache-Control 5. ngi ...
- 关于Spring Boot + Mybatis + Redis二级缓存整合详解
二级缓存是多个SqlSession共享的,其作用域是mapper的同一个namespace,不同的sqlSession两次执行相同namespace下的sql语句且向sql中传递参数也相同即最终执行相 ...
- 关于Redis缓存的面试题
关于Redis缓存的面试题 问题描述: 由于海量的用户的请求 如果这时redis服务器出现问题 则可能导致整个系统崩溃. 运行速度: tomcat服务器 150-250 之间 JVM调优 1000/秒 ...
- 分布式数据库缓存的基本概念?MemCache和redis的详细比较?
分布式数据库缓存指的是在高并发环境下,为了减轻数据库压力和提高系统响应时间,在数据库系统和应用系统之间增加的独立缓存系统. 目前市场上常见的数据库缓存系统是MemChace和Redis,他们的主要区别 ...
最新文章
- python 服务端渲染_客户端渲染和服务器渲染的区别
- Dropwizard入门及开发步骤
- Storyboard的简单使用
- shell脚本中if的相关参数
- python大作业爬虫_爬虫大作业
- bzoj4554 [HEOI2016]游戏 二分匹配
- ipad导入pdf_Ipad笔记法①日常笔记篇
- 常看网页表单数据_数据收集、整理低效繁琐?WPS表单帮你轻松解决
- MDK代码格式化工具
- 怎么将计算机的触摸鼠标锁定,怎么锁定笔记本触摸板_怎么锁定笔记本键盘
- Windows环境下用nmake编译libevent
- 联想拯救者Y7000P拆机清灰学习
- word表格删除空白行java_在Word中怎样批量删除空行,这些点主要注意
- python名片系统_初识python-名片管理系统v1.0
- 一别西湖,又是江南烟雨
- DOM4J解析XML文档
- VMware安装tools
- 源码大公开!Python爬取豆瓣电影Top250源代码,赶紧收藏!
- vscode 样式字体粗细颜色自定义
- [536]python将1张图片分割成9张