Mybatis源码阅读
核心四步:
- MapperProxy -> 动态代理
- SqlSession -> SQL会话
- Executor -> 执行器
- StatementHandler -> JDBC处理器
import com.harlon.mybatis.entity.User;
import org.apache.ibatis.executor.*;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.apache.ibatis.transaction.jdbc.JdbcTransaction;
import org.junit.Before;
import org.junit.Test;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;
/**
* 执行次序:
* SqlSession -> CacheExecutor(二级缓存) -> BaseExecutor (一级缓存)
* -> Simple、Reuse、Batch Executor(最下层执行器)
*/
public class TestExecutor {
private Configuration configuration;
private Connection connection;
private JdbcTransaction jdbcTransaction;
@Before
public void init() throws SQLException {
SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
SqlSessionFactory build = factoryBuilder.build(User.class.getResourceAsStream("/mybatis-config.xml"));
configuration = build.getConfiguration();
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatisplus", "root",
"thr010410");
jdbcTransaction = new JdbcTransaction(connection);
}
/**
* simpleExecutor,每次执行SQL需要预编译SQL语句
*/
@Test
public void simpleTest() throws SQLException {
SimpleExecutor executor = new SimpleExecutor(configuration, jdbcTransaction);
MappedStatement ms = configuration.getMappedStatement("com.harlon.mybatis.dao.UserDao.queryById");
List<Object> objects = executor.doQuery(ms, 3, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
objects.forEach(System.out::println);
}
/**
* 可重用执行器,可以减少重复Sql的编译
* 原理在于其内部维护一个 Map<Sql, Statement> 执行过后会将Statement放入Map
*/
@Test
public void reuseTest() throws SQLException {
ReuseExecutor executor = new ReuseExecutor(configuration, jdbcTransaction);
MappedStatement ms = configuration.getMappedStatement("com.harlon.mybatis.dao.UserDao.queryById");
List<Object> objects = executor.doQuery(ms, 3, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
objects.forEach(System.out::println);
List<Object> objects1 = executor.doQuery(ms, 3, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
objects1.forEach(System.out::println);
}
/**
* 批处理执行器 --> 只针对增删改操作
* 必须手动刷新
*
* 手动刷新过程就是 ——批处理刷新
*/
@Test
public void batchTest() throws SQLException {
BatchExecutor executor = new BatchExecutor(configuration, jdbcTransaction);
MappedStatement ms = configuration.getMappedStatement("com.harlon.mybatis.dao.UserDao.queryById");
List<Object> objects = executor.doQuery(ms, 3, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
objects.forEach(System.out::println);
List<Object> objects1 = executor.doQuery(ms, 3, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(10));
objects1.forEach(System.out::println);
executor.doFlushStatements(false);
}
/**
* BaseExecutor 基础执行器实现了一级缓存,通过调用query进行缓存命中,如果未命中则执行子类实现的doQuery查数据库
*/
@Test
public void baseTest(){
Executor executor = new SimpleExecutor(configuration, jdbcTransaction);
//executor.query()
}
/**
* 使用 cacheExecutor 实现了二级缓存功能,装饰者模式的体现
* 注意点:二级缓存必须提交事物后才能查到
*/
@Test
public void cacheTest() throws SQLException {
Executor executor = new SimpleExecutor(configuration, jdbcTransaction);
Executor cacheExecutor = new CachingExecutor(executor);
MappedStatement ms = configuration.getMappedStatement("com.harlon.mybatis.dao.UserDao.queryById");
cacheExecutor.query(ms, 3, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
cacheExecutor.query(ms, 3, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
}
}
一级缓存命中场景
首先看下面这样一个例子:
public class FirstCacheTest {
private SqlSession sqlSession;
private SqlSessionFactory factory;
@Before
public void init(){
factory = new SqlSessionFactoryBuilder().build(User.class.getResourceAsStream("/mybatis-config.xml"));
sqlSession = factory.openSession();
}
@Test
public void test1(){
UserDao mapper = sqlSession.getMapper(UserDao.class);
User user = mapper.queryById(3L);
User user1 = mapper.queryById(3L);
System.out.println(user1 == user);
}
}
最终输出结果为 true, 就表明了一级缓存的命中,之所以直接说一级缓存是因为此处事务并未提交,所以此处不考虑二级缓存。
⚠️总结一下:
一级缓存命中的八个条件:
- Sql 和参数必须相同
- 必须是相同的statementID
- sqlSession必须一样(会话级缓存的由来)
- RowBounds 返回行的范围必须相同
- 不存在手动清空缓存现象 如:sqlSession.clearCache(); flushCache = true
- 中间未插入更新操作,否则会直接清空缓存
- 缓存作用域不是Statement. (同时可以看出关闭一级缓存的方式:将缓存作用域改为STATEMENT,但是同样会作用于嵌套查询)
- 期间未出现事务提交或者事务回滚
一级缓存代码分析
执行流程
一级缓存的核心类是:PerpetualCache
⚠️缓存的Key:(最后一个参数是环境变量)
这里就可以好奇一下这个Key是怎么判断重复的?
简单来看就是说对updateList中的元素进行逐个比较,这也同时应证了之前的命中条件。
一级缓存失效分析
先做一个小小总结
- 与会话相关
- 参数条件相关
- 提交修改都会清楚一级缓存
一级缓存失效的原因在于 Spring 管理 SqlSession后每次执行Sql都会创建一个新的会话,进而导致一级缓存失效
解决方案
手动开启事务,防止自动提交导致的Sql会话重制
动态代理的核心目的在于 拦截方法调用
Spring整合后的Mybatis,会将Mapper进行动态代理,结果就是 SqlSessionTemlate,又通过SqlSessionInterceptor进行会话拦截,最终才走到SqlSessionFactory
值得一提的是 SqlSessionTemlate 其实是SqlSession的具体实现
二级缓存的定义与分析
二级缓存也称作是应用级缓存,与一级缓存不同的是他的作用范围是整个应用,而且可以夸线程使用,所以二级缓存有着更高的命中率,适合缓存一些修改较少的数据
二级缓存的扩展需求:
- 硬盘
- 内存
- 第三方集成
- 溢出淘汰
- 过期清理
- 命中率统计
- 序列化
二级缓存组件结构
二级缓存命中条件
- 会话提交后
- Sql语句、参数相同
- 相同的StatementID
- RowBounds 相同
二级缓存的开启:1. 配置文件 2.mapper注解或者标签
⚠️注意: flushCache默认会清除所有缓存。
<cache-ref namespace="com.harlon.mybatis.dao.UserDao"/>
同时存在 mapper 缓存和 xml 缓存时会报错,因为缓存对象不唯一,所以需要在 xml中引用缓存。
二级缓存为什么提交后才能命中
防止脏读现象的出现
缓存区的缓存空间是 Mapper 唯一的。也就是之前的装饰器加责任链设计体系。
flushCache是清理的暂存区
二级缓存执行流程
直接调用TracscationCache 的 clear方法不会直接清除缓存,而是打上了clearOnCommit标记,将来在 commmit后会将该缓存清除掉。同样的这也是为了防止脏读的出现。
StatementHandler
定义:
JDBC处理器,基于JDBC构建Statement,并设置参数,然后执行Sql。每调用会话当中一次SQL,都会有与之对应的且唯一的Statement实例。