mybatis的缓存机制
一、缓存:缓存存储
1、现有的查询策略:
现有查询策略:每次查询都会链接访问数据库 存在的问题:每次都需要获取链接,释放连接资源,降低程序运行效率,解决方案使用缓存
2、使用缓存:
缓存:内存中的一块存储空间,用于存放多个用户反复查询数据,有了缓存之后 后续的查询数据都会直接从缓存中获取
3、使用缓存的好处:
减少每次使用链接的占用,提升查询效率, 提升程序的运行效率
4、使用缓存的缺点:
占用大量的内存资源(成本高)
5、缓存的机制:
以空间换时间(效率)
二、mybatis缓存
1、mybatis的缓存机制
(1)一级缓存 基于sqlSession的缓存【默认开启】
一级缓存:也称为本地缓存,用于保存用户在一次会话过程中查询的结果,用户一次会话中只能使用一个sqlSession,一级缓存是自动开启的,不允许关闭。
注意:查询语句最后都是关闭资源,如果不关闭资源数据会出现闪动
不关闭资源,第一次查询四条,第二次5条,SqlSession.close()关闭资源就是为了关闭sqlSession的缓存
一级缓存的特点:
1、查询第一次时,获取到数据写入一级缓存,再次查询时从缓存获取,不再执行sql语句 2、若当前SqlSession发生修改、增加、删除动作时,就会立即把当前缓存的所有数据清空 3、对于查询操作,只要SqlSession没有调用flush或者close方法,它就一直存在
注意:一次缓存造成查询数据结果闪动
(2)二级缓存:基于mapper的缓存(基于指定实体类的缓存)(末默认不开启)
二级缓存是mapper映射级别的缓存,多个SqlSession去操作同一个mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。(也可以说是,在同一namespace下,共享一块缓存空间,如果多个mapper (dao.xml)共享同一namesapce 则也共享一块缓存,二级缓存是跨sqlsession,多个sqlsession可以去二级缓存获取数据。即可以针对同一个dao接口或者同一个命名空间(namespace)创建多个SqlSession )
二级缓存特点:
1、只要发生增删改,就会将·同一命名空间(namespace)下的缓存清空 2、使用二级缓存实体类必须实现序列化,否则报错 3、使用查询语句,默认只写入一级缓存,只有调用close(),commit()方法, 才会将数据提交到二级缓存,其他的sqlsession才能拿到,不再执行sql语句
2、二级缓存 开启手工设置
1、在Mybatis框架中声明 使用二级缓存 在mybatis-config.xml中配置
mybatis-config.xml中
mybatis-config.xml: <!-- 设置缓存--> <settings> <!--开启Mybatis二级缓存 cacheEnabled:启动mybatis二级缓存 值:布尔值 false:不启用 【默认值】 true :开启 --> <setting name="cacheEnabled" value="true"/> </settings>
注意:
2、在需要进行缓存的操作Mapper中指定使用缓存 在mapper文件中声明
<!--开启当前查询操作缓存--> <cache></cache>
3、要缓存的实体类必须实现序列化接口(实体类中)
public class User implements Serializable { private Integer id; private String username; private String password; private Integer age; private Date birthday;
- 实体类实现序列化接口的原因:
数据的数据唤出:长时间不用的放到缓存文件里
数据的唤入:当再次使用时,
4、缓存测试
1、在mybatis工具类中重写一个执行sqlSession的方法:
作用:用于测试缓存,实际开发不使用
//mybatisUtil.java 工具类 public class MybatisUtil { /** * 线程绑定对象,保证是同一个对象 */ private static final ThreadLocal<SqlSession> t1 = new ThreadLocal<>(); private static SqlSessionFactory factory; static { InputStream is = null; try { is = Resources.getResourceAsStream("mybatis-config.xml"); } catch (IOException e) { e.printStackTrace(); } factory = new SqlSessionFactoryBuilder().build(is); } /** * 创建SqlSession * * @return sqlSession */ private static SqlSession openSession() { SqlSession sqlSession = t1.get(); if (sqlSession == null) { sqlSession = factory.openSession(); t1.set(sqlSession); } return sqlSession; } /** * 仅用于测试缓存 每次调用方法获取的都是全新的连接 实际开发不使用 * @return sqlSession */ public static SqlSession getSqlSession(){ return factory.openSession(); } /** * 调用功能方法 * * @param clazz * @return Object */ public static Object getMapper(Class clazz) { return openSession().getMapper(clazz); } /** * 关闭资源 */ public static void close() { if (openSession() != null) { openSession().close(); } t1.remove(); } /** * 提交事务,关闭资源 */ public static void commit() { //1、提交事务 commit(); //2、关闭资源 close(); t1.remove(); } /** * 回滚事务,关闭资源 */ public static void rollback() { //1、提交事务 openSession().rollback(); //2、关闭资源 close(); t1.remove(); } }
工具类的sqlSession不开启线程绑定对象【这样每次查询都会创建一个sqlSession】
/** * 仅用于测试缓存 每次调用方法获取的都是全新的连接 实际开发不使用 * @return sqlSession */ public static SqlSession getSqlSession(){ return factory.openSession(); }
2、测试
//注意:测试之前mybatis工具类调用的方法不能开启ThreadLocal线程绑定对象,否则调用的都是同一个sqlSession //必须关闭链接,不关闭放在一级缓存里,关掉才会放在二级缓存区 @Test public void CacheTest() { //第一次id查询id为1的同学 SqlSession sqlSession1 = MybatisUtil.getSqlSession(); UserDao mapper = sqlSession1.getMapper(UserDao.class); User user = new User(); user.setId(1); List<User> users = mapper.selectUserAll(user); System.out.println("第一次id查询id为1的同学:"); for (User user1 : users) { System.out.println(user1); } //必须关闭链接,不关闭放在一级缓存里,关掉才会放在二级缓存 MybatisUtil.close(); //第二次id查询id为1的同学 SqlSession sqlSession2 = MybatisUtil.getSqlSession(); UserDao mapper2 = sqlSession1.getMapper(UserDao.class); User user2 = new User(); user.setId(1); List<User> users2 = mapper.selectUserAll(user); System.out.println("第二次id查询id为1的同学:"); for (User user3: users2) { System.out.println(user3); } //必须关闭链接,不关闭放在一级缓存里,关掉才会放在二级缓存 MybatisUtil.close(); }
3、注意事项:
1、注意:测试之前mybatis工具类调用的方法不能开启ThreadLocal线程绑定对象, 否则调用的都是同一个sqlSession 2、**必须关闭链接,不关闭放在一级缓存里,关掉才会放在二级缓存区中**
3、二级缓存执行机制
开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询,具体的工作流程如下所示。
二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
1、一级缓存与二级缓存调用顺序:
获取数据先去二级缓存获取,如果有得到,写入(更新)一级缓存** 若没有得到,再去一级缓存,如果仍没有,再去数据库** 优先级: 二级缓存--->一级缓存--->数据库**
三、缓存中的脏数据
1、出现脏数据的原因:
1、主要原因: 数据库增删改之后,缓存的数据和数据库中的数据不匹配 2、详细原因: 使用二级缓存,当有一个session发生修改(增删改)时, 将二级缓存清空了,然而另一个session缓存过以前的查询结果 (此时二级缓存没数据,去一级缓存却拿到了之前的),则可能产生脏数据。
如上图:
1、第一步sqlsession1 去查询id=1的数据, 并写入了sqlsession1的一级缓存与共同的二级缓存 2、第二步sqlsession2 去查询id=1的数据, 发现二级缓存有数据,就不执行sql,并将其写入sqlsession2的一级缓存 3、第三步sqlsession2执行**修改** id=1的数据, 清空了二级缓存与sqlsession2的一级缓存 4、第四步sqlsession1 再去查询id=1的数据,二级缓存没有, 但却从sqlsession1的一级缓存读到了数据,但这却是修改之前的,就产生了脏数据
2、脏数据解决方案
判断是否刷新缓存flushCache;
在默认的设置的select语句是不会刷新缓存的,insert/update/delte会刷新缓存。
3、Mybatis脏数据解决:
根据是否控制事务来判断是否刷新(清空)缓存:
SqlSession.commit() SqlSession.rollback()
注意:
1、Mybatis会在事务控制时清空缓存,
2、后续功能查询一定不要控制事务(清空缓存) ,
3、增删改一定要控制事务
文末
👉在此,鸣谢:刘浩老师讲解
📌 作者:王恒杰
❌ 勘误: 无
📜 声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!
🍅 欢迎点赞 👍 收藏 ⭐留言 📝