mybatis的二级缓存 作者:马育民 • 2020-11-08 22:28 • 阅读:10104 # 真实场景:商城【商品分类】 电商网站 / APP: 首页永远要加载:手机、电脑、服饰、零食…分类列表 - 数据**几个月都不改一次** - 几百万用户,每个人打开首页都要查这张表 - 不加二级缓存:每次请求都查数据库,**数据库压力爆炸** ### 加 MyBatis 二级缓存后 1. 第一个用户访问:查数据库,把分类数据存到**全局共享缓存** 2. 后面所有用户、所有接口请求:**不查数据库**,直接读缓存 3. 只有运营后台**修改/新增/删除分类**时:自动清空缓存,自动刷新新数据 # 二级缓存介绍 ### 1. 定义 MyBatis**二级缓存**是**Mapper命名空间(namespace)级别**的全局缓存,**跨 SqlSession 共享**。 多个数据库会话、多个请求,访问同一个 Mapper 下的相同查询,可命中缓存,减少数据库查询。 ### 2. 范围与特性 1. 层级:介于**一级缓存(SqlSession 本地)** 与 第三方分布式缓存中间; 2. 作用域:绑定 Mapper.xml 的 namespace,不同namespace缓存相互隔离; 3. 生效时机: - 查询数据先存一级缓存; - 当前 `SqlSession` **关闭/提交**后,数据才会刷新到二级缓存; 4. 触发清空:当前 namespace 执行 `insert/update/delete`,自动清空该命名空间全部二级缓存; 5. 强制要求:缓存实体类必须实现 `Serializable` 序列化接口; 6. 开关:全局总开关 + Mapper 单独开启,默认整体关闭。 ### 3. 查询执行顺序 **二级缓存 → 一级缓存 → 数据库** 优先走缓存,极大降低DB压力。 ### 4. 和一级缓存关键区别 - 一级缓存:单会话私有,会话关闭即销毁,无法跨请求; - 二级缓存:全局共享,跨会话、跨请求,生命周期更长。 --- # 应用场景 ### ✅ 适合使用 1. **读多写少业务** 基础数据、静态数据、变动极少的数据,频繁查询、极少修改。 例:字典表、地区数据、商品分类、系统配置、角色权限、常量参数。 2. **数据实时性要求低** 允许短时间缓存延迟(秒/分钟级不一致),不影响业务逻辑。 3. **单应用单体项目** 非集群、非分布式部署,无多实例缓存不同步问题,使用原生二级缓存无脏数据风险。 4. **高频重复查询** 首页展示、列表页、基础信息下拉选项,大量重复SQL查询,需要减轻数据库负载。 --- ### 不适合使用(禁用场景) 1. **强数据一致性业务** 订单、库存、支付、金融、账务、秒杀等,不能容忍缓存延迟、脏数据。 2. **分布式/集群部署** 多台服务实例,原生二级缓存是**单机内存缓存**,各节点缓存独立: A服务修改数据,B服务缓存不刷新,必然出现脏数据。 3. **高频更新表** 频繁增删改的业务表,缓存会频繁失效、清空,缓存命中率极低,反而增加性能开销。 4. **多表关联跨namespace查询** 多Mapper联查,单表更新只清空自身缓存,关联数据缓存残留,引发数据错乱。 # 使用 指 SqlSessionFactory对象中的缓存 由同一个 SqlSessionFactory 对象创建的 SqlSession,共享缓存,即:都可以使用 [](https://www.malaoshi.top/upload/pic/mybatis/1254583-20171029185910164-1823278112.png) ### 特点 - 默认不开启。需要额外配置才能使用 - 存放的是序列化后的数据,而不是对象(所以从缓存中提取的对象,并不是原对象) - 实体类必须实现 `Serializable` 接口 # 开启二级缓存 ### 修改 mybatis-config.xml 增加: ``` ``` 完整配置: ``` ``` ### 修改 userMapper.xml 加入以下配置: ``` ``` 详细配置在下面 ### 修改实体类 实体类必须实现 `Serializable` 接口 ``` package org.malaoshi.entity; import java.io.Serializable; import java.util.Date; public class User implements Serializable { private String id; private String username; private String password; private Integer age; private String sex; private String status; public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getSex() { return sex; } public void setSex(String sex) { this.sex = sex; } public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } ``` ### 测试 **注意:** 要 **关闭1个** `sqlsession`,另一个sqlsession才会用二级缓存 ``` @Test public void testSelectById() throws IOException { InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); IUserMapper mapper = sqlSession.getMapper(IUserMapper.class); User u=mapper.selectById("10001"); String info = String.format("id:%s,用户名:%s,密码:%s", u.getId(), u.getUsername(), u.getPassword()); System.out.println(info); sqlSession.close(); //重新创建SqlSession sqlSession = sqlSessionFactory.openSession(); mapper = sqlSession.getMapper(IUserMapper.class); u=mapper.selectById("10001"); info = String.format("id:%s,用户名:%s,密码:%s", u.getId(), u.getUsername(), u.getPassword()); System.out.println(info); sqlSession.close(); inputStream.close(); } ``` 在控制台中打印1条查询sql,说明重新创建 `SqlSession` 后,也没有查询数据库 # ``配置说明 ``` ``` ### 缓存淘汰算法 `eviction` 是缓存的淘汰算法,可选值有: - "LRU":最近最少使用的:移除最长时间不被使用的对象,默认 见:https://www.malaoshi.top/show_1EF5nwVYkx07.html - "FIFO":先进先出:按对象进入缓存的顺序来移除它们 见:https://www.malaoshi.top/show_1EF5nwVYkx07.html - "SOFT":软引用:移除基于垃圾回收器状态和软引用规则的对象 - "WEAK": 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象 ### 缓存过期时间 `flashInterval`指缓存过期时间,单位为毫秒,60000即为60秒,缺省值为空,即只要容量足够,永不过期 ### 缓存数量 `size`指缓存多少个对象,默认值为1024 ### 是否只读 `readOnly`是否只读,如果为true,则所有相同的sql语句返回的是同一个对象(有助于提高性能,但并发操作同一条数据时,可能不安全),如果设置为false,则相同的sql,后面访问的是cache的clone副本。 ### 注意 需要注意的是global caching的作用域是针对Mapper的Namespace而言的,也就是说只在有在这个Namespace内的查询才能共享这个cache # `` 属性说明 增加 `useCache="false"` 属性 ``` ``` `useCache`属性,默认为true,即开启二级缓存。通过该属性可以局部控制,比如设置userCache="false"禁用二级缓存 `useCache="false"`表示该select语句不使用缓存(即使xml最开头的全局cache启用) # `` 属性说明 默认情况下,如果开启二级缓存,`insert`、`update`、`delete` 成功后,会自动刷新相关的缓存 - 当为select语句时: flushCache默认为false,表示任何时候语句被调用,都不会去清空本地缓存和二级缓存。 - 当为insert、update、delete语句时: flushCache默认为true,表示任何时候语句被调用,都会导致本地缓存和二级缓存被清空。 一般执行inser操作时,需要刷新缓存,否则会新插入的数据查询不出来。 ### 测试 ``` @Test public void test() throws IOException { InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); IUserMapper mapper = sqlSession.getMapper(IUserMapper.class); User u=mapper.selectById("10001"); String info = String.format("id:%s,用户名:%s,密码:%s", u.getId(), u.getUsername(), u.getPassword()); System.out.println(info); User user=new User(); user.setId("1000"); user.setUsername("admin2"); user.setPassword("12345678"); int num=mapper.updateById(user); System.out.println("更新 "+num+" 条记录"); sqlSession.commit(); u=mapper.selectById("10001"); info = String.format("id:%s,用户名:%s,密码:%s", u.getId(), u.getUsername(), u.getPassword()); System.out.println(info); sqlSession.close(); inputStream.close(); } ``` 由于执行了 update 语句,所以再次查询相同sql时,会查询数据库: [](https://www.malaoshi.top/upload/pic/mybatis/QQ20201108231810.png) # 二级缓存的危害 参看以下文章: http://blog.csdn.net/isea533/article/details/44566257 参考文章,非常感谢: http://www.cnblogs.com/QQParadise/articles/5109633.html http://blog.csdn.net/lycemail/article/details/50923245 http://www.360doc.com/content/15/1205/07/29475794_518018352.shtml 原文出处:http://malaoshi.top/show_1IXPEuxjXC.html