MyBatis 学习(2)—SqlSession 和 SqlSessionTemplate 简单使用及注意事项

  • Post author:
  • Post category:其他




1、SqlSession 简单使用

先简单说下

SqlSession

是什么?

SqlSession

是对

Connection

的包装,简化对数据库操作。所以你获取到一个

SqlSession

就相当于获取到一个数据库连接,就可以对数据库进行操作。


SqlSession API

如下图示:

在这里插入图片描述

配置好数据,直接通过

SqlSessionFactory

工厂获取 SqlSession 示例,代码如下:

public class MyBatisCacheTest {

  private static SqlSessionFactory sqlSessionFactory;
  private static Configuration configuration;
  private static JdbcTransaction jdbcTransaction;
  private static Connection connection;
  private static MappedStatement mappedStatement;
  private static SqlSession sqlSession;


  static {
    try {
      InputStream inputStream = MyBatisCacheTest.class.getResourceAsStream("/mybatis-config.xml");
      sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
      configuration = sqlSessionFactory.getConfiguration();
      configuration.setCacheEnabled(true);
      connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/gwmdb?useSSL=false&allowPublicKeyRetrieval=true", "root", "itsme999");
      jdbcTransaction = new JdbcTransaction(connection);
      String statement = "org.apache.ibatis.gwmtest.dao.PersonMapper.getPerson";
      mappedStatement = configuration.getMappedStatement( statement);
      // 注意这里设置了自动提交
      sqlSession = sqlSessionFactory.openSession(true);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

}



2、SqlSession 缓存使用


SqlSession

获取到后开始演示下它的缓存使用。代码如下:

  public static void main(String[] args) throws Exception {

    PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);

    Person person = mapper.getPerson(1);
    Person person1 = mapper.getPerson(1);

    System.out.println("person==person1 = " + (person == person1));
  }

最终结果输出为

true

,因为在

SqlSession

里面是有缓存的,默认一级缓存开启,二级缓存不开启,这里暂时不讲二级缓存,想了解请

MyBatis 二级缓存简单使用步骤

但是在使用这个一级缓存时,需要注意,在多线程环境下面,会出现数据安全问题,多线程并发操作代码如下:

  public static void main(String[] args) throws Exception {
    for (int i = 0; i < COUNT; i++) {
      new Thread(() -> {
      	// 准备好 10 个线程
        try {cdl.await();} catch (Exception e) {e.printStackTrace();}
        
        // 随便调用其中一个查询方法
        PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
        Person person = mapper.getPerson(1);
        System.out.println("person = " + person);
      }).start();
      cdl.countDown();
    }
  }

抛出异常如下:

### Cause: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List
	at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:155)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76)
	at org.apache.ibatis.gwmtest.MyBatisCacheTest.lambda$main$0(MyBatisCacheTest.java:77)
	at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.ClassCastException: org.apache.ibatis.executor.ExecutionPlaceholder cannot be cast to java.util.List
	at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:163)
	at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:137)
	at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:90)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:153)
	... 5 more

具体原因是为什么呢?因为在多线程环境下面,共用同一个

SqlSession

导致的,具体原因看源码,SqlSession 底层调用 Executor,在 MyBatis 中它们是一对一关系。

在 MyBatis 中有分三个基本执行器:


  1. SimpleExecutor

    :每次数据库操作都需要重新编译 SQL 语句,然后开始操作数据库

  2. ResuExecutor (推荐)

    :只有第一次访问数据库会编译 SQL 语句,后面不会重新编译,提高效率,然后操作数据库

  3. BatchExecutor

    :当需要批量操作数据库时,进行打包分批访问数据库

除了上面三个基本 Executor 之外,因为还有一些公共的操作,所以向上衍生出一个

BaseExecutor

,比如最基本的一级缓存就是在这个执行器做的,因为一级缓存是本地缓存不能跨线程使用,所以又继续向上衍生出

CachingExecutor

,二级缓存就是在这里做的,这里可以定义一些缓存比如:Redis、MongoDB 等等。

看到 SqlSession 操作一级缓存的地方(

BaseExecutor

类中),源码如下:

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  	  // ...
      Object object = localCache.getObject(key);
      List<E> list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    return list;
  }
  
  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // ...
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    List<E> list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    localCache.putObject(key, list);
    return list;
  }

假设现在两个线程并发调用

mapper.getPerson(1)

,最终都要拿到 SqlSession 实例去操作数据库。而 SqlSession 和 Executor 是一对一关系,SqlSession 最终会给到 BaseExecutor 处理,最终调用上面的源码 query() 方法。

而上面的源码你只需要关注两个地方:存和取缓存。存缓存的地方注意细节,MyBatis 会先往一级缓存中保存一个占位符

EXECUTION_PLACEHOLDER

,具体作用是为了能够解决子查询中循环依赖问题,不展开叙述。注意这里保存的是占位符。假设现在线程1过来恰好往一级缓存中保存完这个占位符,但是线程1此时没来得及往下执行,CPU 执行权被线程2抢走,那么现在线程2过来执行 query() 方法,因为是同一个 SqlSession,所以 cacheKey 是一模一样的,线程2会去一级缓存中取值,此时线程2取出来的肯定是线程1之前在里面保存的占位符。线程1拿到这个占位符之后,开始执行类型转换,也就是对应这句代码:

(List) localCache.getObject(key)

,你觉得此时泛型转换能成功么?肯定不能,所以直接抛出异常。

解决办法是什么?源码不太好改,只能从使用层面进行改进,主要是因为缓存 key 是一样的,线程1从缓存中可以取出一个占位符,那么让缓存 key 不一样不就行了么?最快最简单的让缓存 key 不一样就是换一个 SqlSession。用不同的会话去操作数据库是不会出现这样的问题。所以最终改进的代码如下:

  public static void main(String[] args) throws Exception {
    for (int i = 0; i < COUNT; i++) {
      new Thread(() -> {
      	// 准备好 10 个线程
        try {cdl.await();} catch (Exception e) {e.printStackTrace();}
        
        // 调用查询方法
        sqlSession = sqlSessionFactory.openSession(true);
        PersonMapper mapper = sqlSession.getMapper(PersonMapper.class);
        Person person = mapper.getPerson(1);
        System.out.println("person = " + person);
      }).start();
      cdl.countDown();
    }
  }

就是每次都重新生成一个

SqlSession

实例。其实底层也换了一个

Connection

实例。这个就是我们常说的线程安全问题是

SqlSession

的一个实现

DefaultSqlSession

,MyBatis 作者也对此类加以

Note that this class is not Thread-Safe

的注释。

或者换个理解 SqlSesion 线程不安全,

SqlSesion

是 Mybatis 中的会话单元,对于 Mybatis 中而言,一个会话对应一个

SqlSession

,也对应一个

JDBC

中的

Connection

。多个线程同时操作

Connection

,A线程执行完 SQL,还想再执行点其他的,但是B线程对这个

Connection

进行

commit

操作,导致A线程一脸懵逼。



2、SqlSessionTemplate 简单使用

上面 SqlSession 存在这样的安全问题,Spring 在继承它的时候,做了改进,在 SqlSession 上继续封装一层,具体是通过动态代理做的。SqlSessionTemplate 在每次调用 API 时都会重新给你创建 SqlSession 实例。这样就能保证每次都在不同的 SqlSession 会话中操作数据库,比较安全。

下面开始演示个问题,代码如下:

    public static void main(String[] args) {
        PaymentMapper paymentMapper = context.getBean(PaymentMapper.class);
        Payment payment = paymentMapper.queryAccount(1);
        Payment payment1 = paymentMapper.queryAccount(1);
        System.out.println("payment1 == payment = " + (payment1 == payment));
    }

最终输出结果为:false,和之前测试的结果不一样。SqlSession 不是有一级缓存嘛,为什么这里结果是 false。为什么?是因为 Spring 对 SqlSession 对象做了一层优化。之前说过同一个 SqlSession 在多线程环境下会出现安全问题,所以 Spring 在你每次操作 API 时都会重新创建新的 SqlSession 实例。所以 SqlSession 都是不一样的,就不用再去谈什么缓存。除非你是同一个 SqlSession 才有缓存之说。

那么怎么让一级缓存生效呢?可以开启事务,保证这些操作都在同一个事务下。改进代码如下:

    public static void main(String[] args) {
        DataSourceTransactionManager tx = (DataSourceTransactionManager)context.getBean(TransactionManager.class);
        TransactionStatus transaction = tx.getTransaction(TransactionDefinition.withDefaults());

		PaymentMapper paymentMapper = context.getBean(PaymentMapper.class);
        Payment payment = paymentMapper.queryAccount(1);
        Payment payment1 = paymentMapper.queryAccount(1);
        System.out.println("payment1 == payment = " + (payment1 == payment));
        
        tx.commit(transaction);
    }

最终结果为:true,进入 SqlSessionTemplate 核心源码如下:

  public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator) {
      
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    LOGGER.debug(() -> "Creating a new SqlSession");
    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }

可以看到是从

TransactionSynchronizationManager

事务管理器中获取到一个 SqlSession 实例。如果没有开启事务,这个

TransactionSynchronizationManager

中获取不到,就会走下面的 openSession() 创建新的实例。

在看到

getResource

() 方法,核心源码如下:

	@Nullable
	public static Object getResource(Object key) {
		Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);
		Object value = doGetResource(actualKey);
		if (value != null && logger.isTraceEnabled()) {
			logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +
					Thread.currentThread().getName() + "]");
		}
		return value;
	}
	
	@Nullable
	private static Object doGetResource(Object actualKey) {
		Map<Object, Object> map = resources.get();
		if (map == null) {
			return null;
		}
		Object value = map.get(actualKey);
		// Transparently remove ResourceHolder that was marked as void...
		if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {
			map.remove(actualKey);
			// Remove entire ThreadLocal if empty...
			if (map.isEmpty()) {
				resources.remove();
			}
			value = null;
		}
		return value;
	}

最终看到变量

resources

源码如下:

public abstract class TransactionSynchronizationManager {

	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");
}

发现竟然是一个

ThreadLocal

变量,这是每个线程私有的东西,人手一份,互不影响,当你开启事务之后,这个变量就已经保存好一个

SqlSession

连接,所以每次调用 API 时获取到的都是同一个

SqlSession

对象,是同一个会话,那么一级缓存就会开始生效。如果你没有开启事务,就会通过 SqlSessionFactory 工厂调用 openSession() 方法打开 SqlSession 会话,但是此时 SqlSessionTemplate 每次都会通过 SqlSessionFactory 打开一个新的 SqlSession,这样就不存在说啥一级缓存了都,完全两个 SqlSession。



版权声明:本文为qq_35971258原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。