mybatis使用和分析

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】总结
  1. MyBatis一级缓存的生命周期和SqlSession一致。
  2. MyBatis一级缓存内部设计简单,只是一个没有容量限定的HashMap,在缓存的功能性上有所欠缺。
  3. MyBatis的一级缓存最大范围是SqlSession内部,有多个SqlSession或者分布式的环境下,数据库写操作会引起脏数据,建议设定缓存级别为Statement。

2、二级缓存

二级缓存是statement级别的缓存,可以在不同的session会话之间共享,通过CachingExecutor实现。

【1】CachingExecutor

此类只实现二级缓存的方法,具体的sql执行操作仍然委派给具体的Executor去执行;

二级缓存和一级缓存的区别是二级缓存可以跨会话,sqlSession2可以使用sqlSession1的缓存;二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。

当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

img

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

猜你喜欢

转载自blog.csdn.net/khuangliang/article/details/108211112