MyBatis的缓存问题
MyBatis的理论问题主要涉及到事务和缓存。
一、事务
事务是多个操作要么同时成功,要么同时失败。有四大特性:原子性、一致性、隔离性、持久性。目前关于事务只是知道下面两点:
Mybatis是在主配置文件sqlMapConfig.xml中配置事务的。
<configuration>
<environments default="mysql">
<environment id="mysql">
<!-- 配置事务的类型,使用本地事务策略-->
<transactionManager type="JDBC"></transactionManager>
</environment>
</environments>
</configuration>
还有就是在测试类中设置事务的自动提交。
SqlSession session;
//创建SqlSession对象,参数为true是设置事务的自动提交
session = factory.openSession(true);
二、缓存
1. MyBatis的缓存策略
MyBatis主要是对jdbc进行简单的封装,然后访问数据库的。但数据库文件在磁盘中,当要处理大量数据时,磁盘延迟会严重影响应用性能。所以内存为了减少和磁盘的交互,将 频繁查询且很少修改或不修改 的数据库文件放到了内存上的缓存中。内存缓存作用是提高数据库访问性能。
要想看清楚MyBatis的缓存策略,得先看清楚MyBatis框架操作数据库的步骤。
从图上可以看出,MyBatis有两级缓存。其中MyBatis查询数据的顺序是:二级缓存 ——> 一级缓存 ——> 数据库
2. 一级缓存和二级缓存
每个 SqlSession 对象都有自己的一级缓存(本地缓存),一级缓存是默认开启的。二级缓存是全局缓存,在 SqlSessionFactory中存储,所以一个 SqlSessionFactory 下的多个 SqlSession 对象有着相同的二级缓存,但是二级缓存没有默认开启。
当二级缓存没有开启时,每个 SqlSession 对象创建的接口代理对象的第一次查询会连接数据库,之后的查询会先由Executor对象访问 local cache,如果其中有缓存结果,就不需要连接数据库了,直接返回结果。如果没有再连接数据库。
当二级缓存开启时,不同的 SqlSession 对象共用相同的二级缓存。但是需要注意的是,SqlSession 对象查询后返回的数据 默认存储在它们自己的一级本地缓存中,只有手动提交数据或者关闭当前会话(调用session.commit()或者session.close()方法),也就是说 清除掉一级缓存,数据才会写入二级缓存中。之后 SqlSession 对象的每次请求 会先由 CatchingExecutor 对象判断在二级缓存中是否有缓存结果,如果有,直接返回;如果缓存中没有,再交给真正的Executor对象来完成查询操作(访问一级缓存或者连接数据库查询),之后CatchingExecutor会将真正Executor返回的查询结果放置到二级缓存中,然后再返回给用户。
3. 一级缓存
需要注意的是,缓存的数据是很少被修改或不被修改的数据,一旦进行增删改操作并提交数据之后,缓存中的数据很明显就没有存储的意义了(因为是增删改前的数据),就会被清除掉。所以一级缓存是会失效的。
一级缓存失效的四种情况:
- 上一次查询的SqlSession对象和本次的不同(一级缓存就不同)
- SqlSession对象相同但查询条件不同
- SqlSession对象相同但是两次查询中间执行了增删改操作(进行了提交数据:调用session.commit()方法)
- SqlSession对象相同但是手动清除了一级缓存(调用session.clearCache()方法)
下面的示例测试一级缓存的作用:
public class TestUser{
SqlSession session; //SqlSession对象
InputStream in;
UserDao userDao; //接口代理对象
@Before
public void init() throws IOException {
//加载主配置文件,目的是为了构建SqlSessionFactory对象
in = Resources.getResourceAsStream("mybatis.xml");
//创建SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//创建SqlSession对象
session = factory.openSession(true);
//通过SqlSession对象创建UserDao接口的代理对象
userDao = session.getMapper(UserDao.class);
}
@After
public void destory() throws IOException {
//关闭资源
session.close();
in.close();
}
/*
测试一级缓存
*/
@Test
public void run(){
User user = userDao.findById(3);
//session.clearCache(); //手动清除一级缓存会使缓存失效
System.out.println("----------");
//userDao.delete(66); //SQL Session相同的两次相同查询操作之间执行删操作,会导致会话提交,缓存清除失效
User user1 = userDao.findById(3);
System.out.println(user == user1);
}
}
Cache Hit Ratio [com.qcby.dao.UserDao]: 0.0
Opening JDBC Connection
Created connection 313540687.
==> Preparing: select * from user where id = ?
==> Parameters: 3(Integer)
<== Columns: id, username, birthday, sex, address
<== Row: 3, 熊二, 2018-03-04 11:34:34, 女, 深圳
<== Total: 1
User{id=3, username='熊二', birthday=Sun Mar 04 19:34:34 CST 2018, sex='女', address='深圳'}
----------
Cache Hit Ratio [com.qcby.dao.UserDao]: 0.0
User{id=3, username='熊二', birthday=Sun Mar 04 19:34:34 CST 2018, sex='女', address='深圳'}
true
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12b0404f]
Returned connection 313540687 to pool.
可以从上面的日志中看到由于有一级缓存,所以只查询了一次。当把代码中的注释依次放开后,可以从下面的日志中看到查询了两次,证明一级缓存失效。
Cache Hit Ratio [com.qcby.dao.UserDao]: 0.0
Opening JDBC Connection
Created connection 313540687.
==> Preparing: select * from user where id = ?
==> Parameters: 3(Integer)
<== Columns: id, username, birthday, sex, address
<== Row: 3, 熊二, 2018-03-04 11:34:34, 女, 深圳
<== Total: 1
User{id=3, username='熊二', birthday=Sun Mar 04 19:34:34 CST 2018, sex='女', address='深圳'}
----------
==> Preparing: delete from user where id= ?
==> Parameters: 66(Integer)
<== Updates: 0
Cache Hit Ratio [com.qcby.dao.UserDao]: 0.0
==> Preparing: select * from user where id = ?
==> Parameters: 3(Integer)
<== Columns: id, username, birthday, sex, address
<== Row: 3, 熊二, 2018-03-04 11:34:34, 女, 深圳
<== Total: 1
User{id=3, username='熊二', birthday=Sun Mar 04 19:34:34 CST 2018, sex='女', address='深圳'}
false
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@12b0404f]
Returned connection 313540687 to pool.
4.二级缓存
通过创建两个SqlSession对象测试二级缓存:
<!-- 在主配置文件中开启二级缓存 -->
<!-- 注意settings顺序 -->
<settings>
<!-- 开启二级缓存 -->
<setting name="cacheEnabled" value="true"/>
</settings>
public class TestUser{
SqlSession session;
SqlSession session2; //两个SqlSession对象
InputStream in;
UserDao userDao;
UserDao userDao2;
@Before
public void init() throws IOException {
//加载主配置文件,目的是为了构建SqlSessionFactory对象
in = Resources.getResourceAsStream("mybatis.xml");
//创建SqlSessionFactory对象
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);
//通过不同的SqlSession对象创建接口代理对象
//创建SqlSession对象
session = factory.openSession(true);
//通过SqlSession对象创建UserDao接口的代理对象
userDao = session.getMapper(UserDao.class);
session2 = factory.openSession(true);
userDao2 = session2.getMapper(UserDao.class);
}
@After
public void destory() throws IOException {
//关闭资源
session.close();
session2.close();
in.close();
}
/*
测试二级缓存
*/
@Test
public void run6(){
List<User> users = userDao.findByUserName("熊");
//session.commit(); //手动提交清除一级缓存
List<User> users2 = userDao2.findByUserName("熊");
System.out.println(users == users2);
}
}
当不手动提交清除一级缓存时,会看到查询了两次
Cache Hit Ratio [com.qcby.dao.UserDao]: 0.0
Opening JDBC Connection
Created connection 345902941.
==> Preparing: select * from user where username like '%熊%'
==> Parameters:
<== Columns: id, username, birthday, sex, address
<== Row: 2, 熊大, 2018-03-02 15:09:37, 女, 上海
<== Row: 3, 熊二, 2018-03-04 11:34:34, 女, 深圳
<== Total: 2
Cache Hit Ratio [com.qcby.dao.UserDao]: 0.0
Opening JDBC Connection
Created connection 204715855.
==> Preparing: select * from user where username like '%熊%'
==> Parameters:
<== Columns: id, username, birthday, sex, address
<== Row: 2, 熊大, 2018-03-02 15:09:37, 女, 上海
<== Row: 3, 熊二, 2018-03-04 11:34:34, 女, 深圳
<== Total: 2
false
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@149e0f5d]
Returned connection 345902941 to pool.
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@c33b74f]
Returned connection 204715855 to pool.
当手动提交,清除一级缓存后,会看到只查询了一次。证明数据写回了二级缓存。
Cache Hit Ratio [com.qcby.dao.UserDao]: 0.0
Opening JDBC Connection
Created connection 345902941.
==> Preparing: select * from user where username like '%熊%'
==> Parameters:
<== Columns: id, username, birthday, sex, address
<== Row: 2, 熊大, 2018-03-02 15:09:37, 女, 上海
<== Row: 3, 熊二, 2018-03-04 11:34:34, 女, 深圳
<== Total: 2
Cache Hit Ratio [com.qcby.dao.UserDao]: 0.5
true
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@149e0f5d]
Returned connection 345902941 to pool.
2, 熊大, 2018-03-02 15:09:37, 女, 上海
<== Row: 3, 熊二, 2018-03-04 11:34:34, 女, 深圳
<== Total: 2
Cache Hit Ratio [com.qcby.dao.UserDao]: 0.5
true
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@149e0f5d]
Returned connection 345902941 to pool.