住房和城乡建设部网站园林一级,国网商旅云网站地址,单位网站建设管理工作总结,做旅游景点网站的目的和意义1、SqlSession 简单使用
先简单说下 SqlSession 是什么#xff1f;SqlSession 是对 Connection 的包装#xff0c;简化对数据库操作。所以你获取到一个 SqlSession 就相当于获取到一个数据库连接#xff0c;就可以对数据库进行操作。
SqlSession API 如下图示#xff1a;…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?useSSLfalseallowPublicKeyRetrievaltrue, 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(personperson1 (person person1));}最终结果输出为 true因为在 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();}// 随便调用其中一个查询方法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.Listat 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.Listat 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 中有分三个基本执行器 SimpleExecutor每次数据库操作都需要重新编译 SQL 语句然后开始操作数据库ResuExecutor (推荐)只有第一次访问数据库会编译 SQL 语句后面不会重新编译提高效率然后操作数据库BatchExecutor当需要批量操作数据库时进行打包分批访问数据库 除了上面三个基本 Executor 之外因为还有一些公共的操作所以向上衍生出一个 BaseExecutor比如最基本的一级缓存就是在这个执行器做的因为一级缓存是本地缓存不能跨线程使用所以又继续向上衍生出 CachingExecutor二级缓存就是在这里做的这里可以定义一些缓存比如Redis、MongoDB 等等。 看到 SqlSession 操作一级缓存的地方BaseExecutor 类中源码如下 Overridepublic E ListE query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// ...Object object localCache.getObject(key);ListE list resultHandler null ? (ListE) 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 ListE queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// ...localCache.putObject(key, EXECUTION_PLACEHOLDER);ListE 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 的一个实现 DefaultSqlSessionMyBatis 作者也对此类加以Note that this class is not Thread-Safe的注释。
或者换个理解 SqlSesion 线程不安全SqlSesion 是 Mybatis 中的会话单元对于 Mybatis 中而言一个会话对应一个 SqlSession也对应一个JDBC中的 Connection。多个线程同时操作 ConnectionA线程执行完 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() 方法核心源码如下 Nullablepublic 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;}Nullableprivate static Object doGetResource(Object actualKey) {MapObject, 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 ThreadLocalMapObject, Object resources new NamedThreadLocal(Transactional resources);
}发现竟然是一个 ThreadLocal 变量这是每个线程私有的东西人手一份互不影响当你开启事务之后这个变量就已经保存好一个 SqlSession 连接所以每次调用 API 时获取到的都是同一个 SqlSession 对象是同一个会话那么一级缓存就会开始生效。如果你没有开启事务就会通过 SqlSessionFactory 工厂调用 openSession() 方法打开 SqlSession 会话但是此时 SqlSessionTemplate 每次都会通过 SqlSessionFactory 打开一个新的 SqlSession这样就不存在说啥一级缓存了都完全两个 SqlSession。