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

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。

猜你喜欢

转载自blog.csdn.net/qq_35971258/article/details/128975516