mybatis使用和分析
mybatis和hibernate一样是一个ORM 框架,hibernate设计的时候为了简化sql编写的复杂程度开发了一套hql查询语言,但是实际上针对复杂的查询场景hql显得非常不灵活,并且问题定位也特别复杂。相比而言mybatis支持更加灵活的sql操作,同时也能够灵活的支持结果集的映射。
本文从mybatis使用出发简单的介绍了下mybatis的使用原理,介绍了mybatis的一级和二级缓存,后续将进一步更新mybatis池化数据源PooledDataSource的原理说明,mybatis整合spring的原理,多个数据源配置和原理说明
一、mybaits使用
1、resources/mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 开启二级缓存(默认是开的,这里写出来是为了方便代码维护) -->
<setting name="cacheEnabled" value="true" />
</settings>
<!-- 别名 -->
<typeAliases>
<package name="maintest"/>
</typeAliases>
<!-- 数据库环境 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test?characterEncoding=UTF-8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 映射文件 -->
<mappers>
<mapper resource="maintest/Student.xml"/>
</mappers>
</configuration>
2、maintest/Student.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="maintest">
<!-- 开启本mapper所在namespace的二级缓存 -->
<cache />
<select id="listStudent" resultType="Student">
select * from student
</select>
<insert id="addStudent" parameterType="Student">
insert into student (id, studentID, name) values (#{id},#{studentID},#{name})
</insert>
<delete id="deleteStudent" parameterType="Student">
delete from student where id = #{id}
</delete>
<select id="getStudent" parameterType="_int" resultType="Student">
select * from student where id= #{id}
</select>
<update id="updateStudent" parameterType="Student">
update student set name=#{name} where id=#{id}
</update>
</mapper>
3、maintest.Student
package maintest;
/**
* @author : 18073771
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
public class Student {
int id;
int studentID;
String name;
/* getter and setter */
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getStudentID() {
return studentID;
}
public void setStudentID(int studentID) {
this.studentID = studentID;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
4、maintest.TestMyBatis
public class TestMyBatis {
public static SqlSessionFactory sqlSessionFactory;
public static int id = 0;
public static void main(String[] args) throws IOException {
// 根据 mybatis-config.xml 配置的信息得到 sqlSessionFactory
String resource = "resources/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
listUsers();
crud();
}
private static void listUsers() {
// 然后根据 sqlSessionFactory 得到 session
SqlSession session = sqlSessionFactory.openSession();
// 最后通过 session 的 selectList() 方法调用 sql 语句 listStudent
List<Student> listStudent = session.selectList("listStudent");
for (Student student : listStudent) {
System.out.println("ID:" + student.getId() + ",NAME:" + student.getName());
if (student.id > id) {
id = student.id;
}
}
}
private static void crud() {
// 然后根据 sqlSessionFactory 得到 session
SqlSession session = sqlSessionFactory.openSession();
// 增加学生
Student student1 = new Student();
student1.setId(id);
student1.setStudentID(id);
student1.setName("新增加的学生");
session.insert("addStudent", student1);
// 删除学生
Student student2 = new Student();
student2.setId(1);
session.delete("deleteStudent", student2);
// 获取学生
Student student3 = session.selectOne("getStudent", 2);
// 修改学生
student3.setName("修改的学生");
session.update("updateStudent", student3);
// 最后通过 session 的 selectList() 方法调用 sql 语句 listStudent
List<Student> listStudent = session.selectList("listStudent");
for (Student student : listStudent) {
System.out.println("ID:" + student.getId() + ",NAME:" + student.getName());
}
// 提交修改
session.commit();
// 关闭 session
session.close();
}
}
二、一次sql执行的流程步骤
1、读取mybatis-config.xml
将mybatis-config.xml读取成输入流
2、生成SqlSessionFactory
解析mybatis配置的输入流,构建Configuration配置对象
将Configuration对象加载到SqlSessionFactory中
Configuration中的几个重要的属性:Environment里面包含dataSource, mappedStatements
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");
MappedStatement对象,用来绑定sql,这也是为什么MappedStatement中需要这些属性
private String resource;
private Configuration configuration; // 配置对象
private String id; // mapper.xml中的namespace 中的sqlid
private Integer fetchSize;
private Integer timeout;
private StatementType statementType;
private ResultSetType resultSetType; // 结果集类型
private SqlSource sqlSource; // 绑定了sql的语句
private Cache cache;
private ParameterMap parameterMap;
private List<ResultMap> resultMaps;
private boolean flushCacheRequired;
private boolean useCache;
private boolean resultOrdered;
private SqlCommandType sqlCommandType;
private KeyGenerator keyGenerator;
private String[] keyProperties;
private String[] keyColumns;
private boolean hasNestedResultMaps;
private String databaseId;
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
它里面的属性和mapper.xml中的属性相关
<!ELEMENT select (#PCDATA | include | trim | where | set | foreach | choose | if | bind)*>
<!ATTLIST select
id CDATA #REQUIRED
parameterMap CDATA #IMPLIED
parameterType CDATA #IMPLIED
resultMap CDATA #IMPLIED
resultType CDATA #IMPLIED
resultSetType (FORWARD_ONLY | SCROLL_INSENSITIVE | SCROLL_SENSITIVE) #IMPLIED
statementType (STATEMENT|PREPARED|CALLABLE) #IMPLIED
fetchSize CDATA #IMPLIED
timeout CDATA #IMPLIED
flushCache (true|false) #IMPLIED
useCache (true|false) #IMPLIED
databaseId CDATA #IMPLIED
lang CDATA #IMPLIED
resultOrdered (true|false) #IMPLIED
resultSets CDATA #IMPLIED
>
例如:下面这个更新的sql变成MappedStatement后是这样的
<update id="updateStudent" parameterType="Student">
update student set name=#{name} where id=#{id}
</update>
3、创建SqlSession
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
// environment 里有数据源
final Environment environment = configuration.getEnvironment();
// 创建事务工厂
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
// 创建一个事务
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
// 创建执行器
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
【1】sqlSession 是否可以做成单例?
不能,sqlSession是会话级别的,如果做成了全局的单例会造成全局只能有一个会话不能支持高并发
【2】autoCommit 配true和false的区别?
默认是false,true 的时候修改操作会自动提交,false的时候不会自动提交。
【3】几种不同的executor区别SimpleExecutor、ReuseExecutor、BatchExecutor?
【1】SimpleExecutor
就是最简单的执行器
【2】ReuseExecutor
会将preparedStatement 缓存下来; 下次再执行的时候就可以复用connection preparedStatement 的缓存map会在事务commit之后自动清除,并关闭statement
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 如果已经存在该sql的preparedStatement
if (hasStatementFor(sql)) {
// 直接statementMap 中获取preparedStatement
stmt = getStatement(sql);
} else {
// 获取连接
Connection connection = getConnection(statementLog);
// 准备preparedStatement
stmt = handler.prepare(connection);
// 缓存 preparedStatement
putStatement(sql, stmt);
}
// 参数化
handler.parameterize(stmt);
return stmt;
}
【3】BatchExecutor
主要是批量更新的时候使用,其实是通过PrepareStatement的addBatch方法去批量更新的,
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
final Configuration configuration = ms.getConfiguration();
final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT,
null, null);
final BoundSql boundSql = handler.getBoundSql();
final String sql = boundSql.getSql();
final Statement stmt;
if (sql.equals(currentSql) && ms.equals(currentStatement)) {
int last = statementList.size() - 1;
stmt = statementList.get(last);
BatchResult batchResult = batchResultList.get(last);
batchResult.addParameterObject(parameterObject);
} else {
Connection connection = getConnection(ms.getStatementLog());
// 准备prepareed statement
stmt = handler.prepare(connection);
// 记录当前sql
currentSql = sql;
// 记录当前mappedstatement
currentStatement = ms;
// 缓存到statement集合中
statementList.add(stmt);
batchResultList.add(new BatchResult(ms, sql, parameterObject));
}
// 参数化
handler.parameterize(stmt);
// 加入批量更新
handler.batch(stmt);
return BATCH_UPDATE_RETURN_VALUE;
}
测试案例
/**
* BatchExecutor 测试,效果应该是最后一次更新; 并且更新时间要比simpleExecutor更新时间短
*/
private static void batchExecutorTest() {
long start = System.currentTimeMillis();
// 然后根据 sqlSessionFactory 得到 session
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH,false);
// 获取学生
Student student3 = session.selectOne("getStudent", 3);
System.out.println(student3);
// 获取学生
Student student4 = session.selectOne("getStudent", 4);
System.out.println(student4);
// 获取学生
Student student5 = session.selectOne("getStudent", 5);
System.out.println(student5);
// 修改学生
student3.setName("修改的学生3");
session.update("updateStudent", student3);
// 修改学生
student4.setName("修改的学生4");
session.update("updateStudent", student4);
// 修改学生
student5.setName("修改的学生5");
session.update("updateStudent", student5);
// 手动提交
session.commit();
long end = System.currentTimeMillis();
System.out.println("BatchExecutor 更新的执行时间为:" + (end - start));
}
结果
结果 只prepare一次
DEBUG [main] - ==> Preparing: update student set name=? where id=?
DEBUG [main] - ==> Parameters: 修改的学生3(String), 3(Integer)
DEBUG [main] - ==> Parameters: 修改的学生4(String), 4(Integer)
DEBUG [main] - ==> Parameters: 修改的学生5(String), 5(Integer)
DEBUG [main] - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@880ec60]
BatchExecutor 更新的执行时间为:384
不批量的操作结果
DEBUG [main] - ==> Preparing: update student set name=? where id=?
DEBUG [main] - ==> Parameters: 修改的学生3(String), 3(Integer)
DEBUG [main] - <== Updates: 1
DEBUG [main] - ==> Preparing: update student set name=? where id=?
DEBUG [main] - ==> Parameters: 修改的学生4(String), 4(Integer)
DEBUG [main] - <== Updates: 1
DEBUG [main] - ==> Preparing: update student set name=? where id=?
DEBUG [main] - ==> Parameters: 修改的学生5(String), 5(Integer)
DEBUG [main] - <== Updates: 1
DEBUG [main] - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@880ec60]
SimpleExecutor 更新的执行时间为:362
【4】绑定executor的JdbcTransaction事务是干什么的?
用于创建连接,设置隔离级别、自动提交
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
// 创建一个连接
connection = dataSource.getConnection();
if (level != null) {
// 设置事务隔离界别
connection.setTransactionIsolation(level.getLevel());
}
// connection设置是否自动提交
setDesiredAutoCommit(autoCommmit);
}
【5】connection、statement何时关闭?
commit的时候关闭statement;
BaseExecutor 中的commit方法
public void commit(boolean required) throws SQLException {
if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
// 清除本地缓存
clearLocalCache();
// 如果是ReuseExecutor清除缓存的preparedStatement 缓存map并关闭Statement
flushStatements();
if (required) {
// 事务提交
transaction.commit();
}
}
在处理结果集后关闭连接
JdbcTransaction中的close方法
public void close() throws SQLException {
if (connection != null) {
resetAutoCommit();
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + connection + "]");
}
connection.close();
}
}
【6】SqlSession、JdbcTransaction、Executor、StatementHandler、Connection之间的关系?
SqlSession:一次会话,可能有多次sql操作
JdbcTransaction:事务,可以获取连接,绑定在Executor中
Executor:执行器,绑定在SqlSession中,创建SqlSession的时候可以指定执行器
StatementHandler:statement处理器,每次sql操作都会new一个新的来创建statement
Connection:和数据库的一次连接,如果是相同的sql statement可以复用同一个连接,否则sql操作前要创建连接
关系 SqlSession:JdbcTransaction:Executor:StatementHandler:Connection = 1:1:1:n:(1-n)
4、执行sql和结果集映射
【1】需要事务的update操作
insert为例,最终执行的还是update方法
ReuseExecutor中的doUpdate方法
public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
// 获取配置
Configuration configuration = ms.getConfiguration();
// 创建stateMement处理器--PreparedStatementHandler
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
// 创建preparedStatement
Statement stmt = prepareStatement(handler, ms.getStatementLog());
// 执行更新操作
return handler.update(stmt);
}
ReuseExecutor中的prepareStatement方法
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 如果已经存在该sql的preparedStatement
if (hasStatementFor(sql)) {
// 直接statementMap 中获取preparedStatement
stmt = getStatement(sql);
} else {
// 获取连接
Connection connection = getConnection(statementLog);
// 准备preparedStatement
stmt = handler.prepare(connection);
// 缓存 preparedStatement
putStatement(sql, stmt);
}
// 参数化--参数化后sql中的?被具体的参数替换
handler.parameterize(stmt);
return stmt;
}
PreparedStatementHandler中的update方法
public int update(Statement statement) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行sql--如果ps中connection属性中的autocommit=true则自动提交
ps.execute();
// 获取更新的条数
int rows = ps.getUpdateCount();
Object parameterObject = boundSql.getParameterObject();
KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
return rows;
}
【2】不需要事务的select操作
没有缓存的情况下
最终通过ReuseExecutor 的 doQuery进行查询
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
BoundSql boundSql) throws SQLException {
// 获取配置
Configuration configuration = ms.getConfiguration();
// 创建stateMement处理器--PreparedStatementHandler
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,
boundSql);
// 创建preparedStatement
Statement stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询
return handler.<E> query(stmt, resultHandler);
}
最终调用PreparedStatementHandler的query方法进行查询操作
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
// 结果集映射
return resultSetHandler.<E> handleResultSets(ps);
}
缓存查询的结果,这就是是mybatis的一级缓存,对查询sql 的结果进行缓存,如果是同一个查询sql不会再查库,
那么缓存的删除策略是什么?
同一个session会话中如果有update、commit、rollback、或者flushCache操作会调用clearLocakCache清除本地一级缓存,通过这种方式保证了缓存的一致性。
BaseExecutor 中的update方法
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed)
throw new ExecutorException("Executor was closed.");
// 清除本地一级缓存
clearLocalCache();
return doUpdate(ms, parameter);
}
5、关闭SqlSession
SqlSession的close方法
public void close() {
try {
// 调用执行器的close方法
executor.close(isCommitOrRollbackRequired(false));
dirty = false;
} finally {
ErrorContext.instance().reset();
}
}
BaseExecutor的close方法
public void close(boolean forceRollback) {
try {
try {
rollback(forceRollback);
} finally {
// 调用事务的close方法
if (transaction != null) transaction.close();
}
} catch (SQLException e) {
// Ignore. There's nothing that can be done at this point.
log.warn("Unexpected exception on closing transaction. Cause: " + e);
} finally {
transaction = null;
deferredLoads = null;
localCache = null;
localOutputParameterCache = null;
closed = true;
}
}
JdbcTransaction 的close方法,关闭连接
public void close() throws SQLException {
if (connection != null) {
resetAutoCommit();
if (log.isDebugEnabled()) {
log.debug("Closing JDBC Connection [" + connection + "]");
}
connection.close();
}
}
三、mybatis缓存
1、一级缓存
【1】简介
mybatis一级缓存是session会话级别的缓存,即在一次sqlSession会话过程中的缓存,它的作用是针对相同的sql查询操作缓存查询结果,减少数据库的查询操作。
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
localCache.removeObject(key);
}
// 结果缓存到本地;PerpetualCache
localCache.putObject(key, list);
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
return list;
}
【2】一级缓存的命中场景
运行时参数相关
1.SQL传入参数一致
2.同一个会话(SqlSession对象,一级缓存属于会话级缓存)
3.方法名和类名必须一样(Statement ID必须一样如:com.xxx.XXXMapper.findById())
4.行范围一样 rowbound
操作配置相关
5.不手动清空缓存 (-cleanCache -commit rollback)
6.没有Update操作
7.缓存作用域不能是STATEMENT
8.未配置flushCash为false
【3】总结
- MyBatis一级缓存的生命周期和SqlSession一致。
- MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
- MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。
2、二级缓存
二级缓存是statement级别的缓存,可以在不同的session会话之间共享,通过CachingExecutor实现。
【1】CachingExecutor
此类只实现二级缓存的方法,具体的sql执行操作仍然委派给具体的Executor去执行;
二级缓存和一级缓存的区别是二级缓存可以跨会话,sqlSession2可以使用sqlSession1的缓存;二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。
当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
Congifuration 中的newExecutor方法
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
// 如果开启了使用二级缓存,用CachingExecutor包装之前的executor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
【2】实例,使用步骤
a、在全局mybatis配置中设置开启二级缓存
b、在mapper映射文件中开启cache
c、正常使用Simple、Reuse、Batch执行器正常使用即可,在执行前会将他们包装陈CachingExecutor
private static void cachingExecutorTest() {
System.out.println("----------第一个会话----------");
// 然后根据 sqlSessionFactory 得到 session
SqlSession session = sqlSessionFactory.openSession(ExecutorType.SIMPLE,false);
// 获取学生---查库
Student student3 = session.selectOne("getStudent", 3);
System.out.println(student3.toString());
// 获取学生---二级缓存能走到吗?
Student student31 = session.selectOne("getStudent", 3);
System.out.println(student31.toString());
// 获取学生---查库
Student student4 = session.selectOne("getStudent", 4);
System.out.println(student4.toString());
// 获取学生---查库
Student student5 = session.selectOne("getStudent", 5);
System.out.println(student5.toString());
// 为什么需要在这提交后面session2才能命中二级缓存
session.commit();
System.out.println("----------第二个会话----------");
// 第二个会话
SqlSession session2 = sqlSessionFactory.openSession(ExecutorType.SIMPLE,false);
// 获取学生---- 二级缓存
Student student23 = session2.selectOne("getStudent", 3);
System.out.println(student23.toString());
// 获取学生--- 二级缓存
Student student231 = session2.selectOne("getStudent", 3);
System.out.println(student231.toString());
// 获取学生---- 二级缓存
Student student24 = session2.selectOne("getStudent", 4);
System.out.println(student24.toString());
// 获取学生---- 二级缓存
Student student25 = session2.selectOne("getStudent", 5);
System.out.println(student25.toString());
}
执行结果
----------第一个会话----------
DEBUG [main] - Cache Hit Ratio [maintest]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1531333864.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5b464ce8]
DEBUG [main] - ==> Preparing: select * from student where id= ?
DEBUG [main] - ==> Parameters: 3(Integer)
DEBUG [main] - <== Total: 1
[id=3, studentID=3, name=修改的学生3]
DEBUG [main] - Cache Hit Ratio [maintest]: 0.0 // 这里为什么没有命中二级缓存?因为没有commit
[id=3, studentID=3, name=修改的学生3]
DEBUG [main] - Cache Hit Ratio [maintest]: 0.0
DEBUG [main] - ==> Preparing: select * from student where id= ?
DEBUG [main] - ==> Parameters: 4(Integer)
DEBUG [main] - <== Total: 1
[id=4, studentID=4, name=修改的学生4]
DEBUG [main] - Cache Hit Ratio [maintest]: 0.0
DEBUG [main] - ==> Preparing: select * from student where id= ?
DEBUG [main] - ==> Parameters: 5(Integer)
DEBUG [main] - <== Total: 1
[id=5, studentID=5, name=修改的学生5]
----------第二个会话----------
DEBUG [main] - Cache Hit Ratio [maintest]: 0.2
[id=3, studentID=3, name=修改的学生3]
DEBUG [main] - Cache Hit Ratio [maintest]: 0.3333333333333333
[id=3, studentID=3, name=修改的学生3]
DEBUG [main] - Cache Hit Ratio [maintest]: 0.42857142857142855
[id=4, studentID=4, name=修改的学生4]
DEBUG [main] - Cache Hit Ratio [maintest]: 0.5
[id=5, studentID=5, name=修改的学生5]
【3】底层实现
session.commit之后二级缓存才生效,commit操作做了啥?
查看源码发现session.commit操作最终是通过cache链最终完成commit操作的,其实就是通过装饰模式实现的。
TransactionalCache—>SynchronizedCache---->LoggingCache---->SerializedCache----->LruCache----->PerpetualCache
A、PerpetualCache
最终放到此类的hashMap中;key是sql,value是序列化后的查询结果
B、LruCache
LRU装饰器,它会把PerpetualCache 的map中最近最久未使用的key删除
C、SerializedCache
序列化装饰器,将查询结果集序列化
D、LoggingCache
日志打印装饰器
E、SynchronizedCache
同步处理的装饰器,防并发
F、TransactionalCache
事务处理的装饰器
TransactionalCacheManager
public void commit() {
// transactionalCaches map 的key是mapedStatement 中的cache对象,value是TransactionalCache缓存
for (TransactionalCache txCache : transactionalCaches.values()) {
txCache.commit();
}
}
【4】总结
二级缓存:MyBatis二级缓存的工作流程和前文提到的一级缓存类似,只是在一级缓存处理前,用CachingExecutor
装饰了BaseExecutor
的子类,在委托具体职责给delegate
之前,实现了二级缓存的查询和写入功能。
MyBatis的二级缓存相对于一级缓存来说,实现了SqlSession
之间缓存数据的共享,同时粒度更加的细,能够到namespace
级别,通过Cache接口实现类不同的组合,对Cache的可控性也更强。
MyBatis在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件比较苛刻。
在分布式环境下,由于默认的MyBatis Cache实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将MyBatis的Cache接口实现,有一定的开发成本,直接使用Redis、Memcached等分布式缓存可能成本更低,安全性也更高。
四、mybatis更换数据源
1、mybatis如何支持多个数据源?
mybatis数据源介绍:
dataSource 元素使用标准的 JDBC 数据源接口来配置 JDBC 连接对象的资源。
大多数 MyBatis 应用程序会按示例中的例子来配置数据源。虽然数据源配置是可选的,但如果要启用延迟加载特性,就必须配置数据源。
有三种内建的数据源类型(也就是 type="[UNPOOLED|POOLED|JNDI]"):
UNPOOLED– 这个数据源的实现会每次请求时打开和关闭连接。虽然有点慢,但对那些数据库连接可用性要求不高的简单应用程序来说,是一个很好的选择。 性能表现则依赖于使用的数据库,对某些数据库来说,使用连接池并不重要,这个配置就很适合这种情形。
POOLED– 这种数据源的实现利用“池”的概念将 JDBC 连接对象组织起来,避免了创建新的连接实例时所必需的初始化和认证时间。 这种处理方式很流行,能使并发 Web 应用快速响应请求。
JNDI – 这个数据源实现是为了能在如 EJB 或应用服务器这类容器中使用,容器可以集中或在外部配置数据源,然后放置一个 JNDI 上下文的数据源引用。
不整合spring如何实现多个数据源配置
A、配置多个sqlSessionFactory
B、可以加个路由的中间件,像mycat
C、对Configuration中的dataSource改造:参考另一篇博文修改mybatis源码
五、mybatis整合spring(后续更新)
1、如何支持spring事务?
结语
在源码测试和使用的过程中发现一个现象就是模拟10万并发查询数据库的时候发现单个sqlSession的处理时间和每次查询打开一个sqlSession的处理时间相比差不多还快一点点。后续将分析下为什么会产生这种现象
/**
* @author : 18073771
* @see [相关类/方法](可选)
* @since [产品/模块版本] (可选)
*/
public class TestMyBatis {
public static SqlSessionFactory sqlSessionFactory;
public static int id = 0;
public static void main(String[] args) throws IOException {
// 根据 mybatis-config.xml 配置的信息得到 sqlSessionFactory
String resource = "resources/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// connectionTest();
connectionTest1();
}
/**
* 将查询改为2ms后执行时间为244115
* 将连接池最大连接数调整成40后 226663
*/
private static void connectionTest() {
long start = System.currentTimeMillis();
final SqlSession session = sqlSessionFactory.openSession();
for (int i = 0; i < 10000; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 获取学生---查库
Student student3 = session.selectOne("getStudent", RandomUtils.nextInt());
if (null != student3) {
System.out.println(student3.toString());
}
}
});
thread.start();
try {
// 阻塞main线程
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println("程序执行时间为" + (end - start));
}
/**
* 程序执行时间为68628
* 单次查询改为2ms后,执行时间288915
* 将连接池最大连接数调整成40后,261498
*/
private static void connectionTest1() {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
SqlSession session = sqlSessionFactory.openSession();
// 获取学生---查库
Student student3 = session.selectOne("getStudent", RandomUtils.nextInt());
if (null != student3) {
System.out.println(student3.toString());
}
session.close();
}
});
thread.start();
try {
// 阻塞main线程
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.out.println("程序执行时间为" + (end - start));
}
}
参考资料:
https://tech.meituan.com/2018/01/19/mybatis-cache.html