MyBatis事务

 

数据很重要

在介绍MyBatis事务之前,先普及下数据库事务相关知识

事务(Transaction)是访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。事务通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起,并用形如begin transactionend transaction语句(或函数调用)来界定。事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。一个事务可以是一条SQL语句,一组SQL语句或整个程序。

它有四大特性:原子性、一致性、隔离性、持续性,简称ACID

1.原子性:事务是应用中最小的执行单位,就如原子是自然界最小颗粒,具有不可再分的特征一样。事务是应用中不可再分的最小逻辑执行体,一组事务,要么成功;要么撤回。

2.一致性:事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库中只包含事务成功提交的结果时,数据库处于一致性状态。一致性是通过原子性来保证的。有非法数据(外键约束之类),事务撤回。

3.隔离性:各个事务的执行互不干扰,任意一个事务的内部操作对其他并发的事务,都是隔离的。也就是说:并发执行的事务之间不能看到对方的中间状态,并发执行的事务之间不能相互影响。事务独立运行。一个事务处理后的结果,影响了其他事务,那么其他事务会撤回。事务的100%隔离,需要牺牲速度。

4.持续性:持续性也称为持久性,指事务一旦提交,对数据所做的任何改变,都要记录到永久存储器中,通常是保存进物理数据库。软、硬件崩溃后,InnoDB数据表驱动会利用日志文件重构修改。可靠性和高速度不可兼得, innodb_flush_log_at_trx_commit 选项 决定什么时候吧事务保存到日志里。

读取数据的三个概念:

1.脏读(Dirty Reads):所谓脏读就是对脏数据的读取,而脏数据所指的就是未提交的数据。一个事务正在对一条记录做修改,在这个事务完成并提交之前,这条数据是处于待定状态的(可能提交也可能回滚),这时,第二个事务来读取这条没有提交的数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被称为脏读。

2.不可重复读(Non-Repeatable Reads):一个事务先后读取同一条记录,但两次读取的数据不同,我们称之为不可重复读。也就是说,这个事务在两次读取之间该数据被其它事务所修改。

3.幻读(Phantom Reads):一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为幻读。

事务的隔离级别:

1、Read Uncommitted(未授权读取、读未提交):这是最低的隔离等级,允许其他事务看到没有提交的数据。这种等级会导致脏读。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。避免了更新丢失,却可能出现脏读。也就是说事务B读取到了事务A未提交的数据。SELECT语句以非锁定方式被执行,所以有可能读到脏数据,隔离级别最低。

2.Read Committed(授权读取、读提交):读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。该隔离级别避免了脏读,但是却可能出现不可重复读。事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。

3.repeatable read(可重复读取):就是在开始读取数据(事务开启)时,不再允许修改操作,事务开启,不允许其他事务的UPDATE修改操作,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。避免了不可重复读取和脏读,但是有时可能出现幻读。这可以通过“共享读锁”和“排他写锁”实现。

4.串行化、序列化:提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。

原生JDBC给了事务定义的级别,在java.sql.Connection.java文件中:

public interface Connection  extends Wrapper, AutoCloseable {
    /**
     * 表示不支持事务的常量。
     */
    int TRANSACTION_NONE             = 0;
   /**
     * 可读到未提交
     */
    int TRANSACTION_READ_UNCOMMITTED = 1;
    /**
     * 只能读已提交
     */
    int TRANSACTION_READ_COMMITTED   = 2;
   /**
     * 可重复读
     */
    int TRANSACTION_REPEATABLE_READ  = 4;
   /**
     * 串行化操作
     */
    int TRANSACTION_SERIALIZABLE     = 8;
   /**
     * 设置事务
     */
    void setTransactionIsolation(int level) throws SQLException;
}

具体实现:

 /**
     * @param level
     * @throws SQLException
     */
    public void setTransactionIsolation(int level) throws SQLException {
        synchronized (getConnectionMutex()) {
            checkClosed();

            if (this.hasIsolationLevels) {
                String sql = null;

                boolean shouldSendSet = false;

                if (getAlwaysSendSetIsolation()) {
                    shouldSendSet = true;
                } else {
                    if (level != this.isolationLevel) {
                        shouldSendSet = true;
                    }
                }

                if (getUseLocalSessionState()) {
                    shouldSendSet = this.isolationLevel != level;
                }

                if (shouldSendSet) {
                    switch (level) {
                        case java.sql.Connection.TRANSACTION_NONE:
                            throw SQLError.createSQLException("Transaction isolation level NONE not supported by MySQL", getExceptionInterceptor());

                        case java.sql.Connection.TRANSACTION_READ_COMMITTED:
                            sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED";

                            break;

                        case java.sql.Connection.TRANSACTION_READ_UNCOMMITTED:
                            sql = "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED";

                            break;

                        case java.sql.Connection.TRANSACTION_REPEATABLE_READ:
                            sql = "SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ";

                            break;

                        case java.sql.Connection.TRANSACTION_SERIALIZABLE:
                            sql = "SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE";

                            break;

                        default:
                            throw SQLError.createSQLException("Unsupported transaction isolation level '" + level + "'", SQLError.SQL_STATE_DRIVER_NOT_CAPABLE,
                                    getExceptionInterceptor());
                    }

                    execSQL(null, sql, -1, null, DEFAULT_RESULT_SET_TYPE, DEFAULT_RESULT_SET_CONCURRENCY, false, this.database, null, false);

                    this.isolationLevel = level;
                }
            } else {
                throw SQLError.createSQLException("Transaction Isolation Levels are not supported on MySQL versions older than 3.23.36.",
                        SQLError.SQL_STATE_DRIVER_NOT_CAPABLE, getExceptionInterceptor());
            }
        }
    }

普及了数据库和jdbc的事务级别后,然后再说MyBatis事务

MyBatis作为Java语言的数据库框架,对数据库的事务管理是其非常重要的一个方面。本文将从事务的分类、配置和实现分析MyBatis的事务管理的实现机制。部分组件图

MyBatis事务分类

事务接口定义在org.apache.ibatis.transaction.Transaction。核心方法:

public interface Transaction {

  /**
   * Retrieve inner database connection.
   * @return DataBase connection
   * @throws SQLException
   *           the SQL exception
   */
  Connection getConnection() throws SQLException;

  /**
   * Commit inner database connection.
   * @throws SQLException
   *           the SQL exception
   */
  void commit() throws SQLException;

  /**
   * Rollback inner database connection.
   * @throws SQLException
   *           the SQL exception
   */
  void rollback() throws SQLException;

  /**
   * Close inner database connection.
   * @throws SQLException
   *           the SQL exception
   */
  void close() throws SQLException;
}

它有两种实现方式:

1、使用JDBC的事务管理机制:利用java.sql.Connection对象完成对事务的提交(commit())、回滚(rollback())、关闭(close())等

2、使用MANAGED的事务管理机制:这种机制MyBatis自身不会去实现事务管理,而是让程序的容器如(tomcat,JBOSS,Weblogic,spring)来实现对事务的管理,此文暂不考虑

可在mybatis配置文件中配置其中一种事务处理方式:

 

Mybatis事务创建 

MyBatis事务的创建是交给TransactionFactory 事务工厂来创建的,如果我们将<transactionManager>的type 配置为"JDBC",那么,在MyBatis初始化解析<environment>节点时,会根据type="JDBC"创建一个JdbcTransactionFactory工厂,核心代码:

/**
     * 解析<transactionManager>节点,创建对应的TransactionFactory
     * @param context
     * @return
     * @throws Exception
     */
  private TransactionFactory transactionManagerElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      /*
            在Configuration初始化的时候,会通过以下语句,给JDBC和MANAGED对应的工厂类
            typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
            typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
            下述的resolveClass(type).newInstance()会创建对应的工厂实例
       */
      TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a TransactionFactory.");
  }

dom树解析就不说了,以前开发不经常来回解析xml吗,xml文档与java类之间的互转,数据交换的一种,可以把xml当成一种组装数据的方式,可以多层嵌套,现在流行后台java注解解析和前台json了。

如果type = "JDBC",则MyBatis会创建一个JdbcTransactionFactory.class 实例;如果type="MANAGED",则MyBatis会创建一个MangedTransactionFactory.class实例。
MyBatis对<transactionManager>节点的解析会生成 TransactionFactory实例;而对<dataSource>解析会生成datasouce实例。作为<environment>节点,会根据TransactionFactory和DataSource实例创建一个Environment对象,代码如下所示:

 private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        //是和默认的环境相同时,解析之
        if (isSpecifiedEnvironment(id)) {
          //1.解析<transactionManager>节点,决定创建什么类型的TransactionFactory
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          //2. 创建dataSource
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          //3. 使用了Environment内置的构造器Builder,传递id 事务工厂TransactionFactory和数据源DataSource
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
        }
      }
    }
  }

Environment表示着一个数据库的连接,生成后的Environment对象会被设置到Configuration实例中。

通过事务工厂TransactionFactory很容易获取到Transaction对象实例。我们以JdbcTransaction为例,看一下JdbcTransactionFactory是怎样生成JdbcTransaction的,代码如下:

public class JdbcTransactionFactory implements TransactionFactory {
 
  public void setProperties(Properties props) {
  }
 
    /**
     * 根据给定的数据库连接Connection创建Transaction
     * @param conn Existing database connection
     * @return
     */
  public Transaction newTransaction(Connection conn) {
    return new JdbcTransaction(conn);
  }
 
    /**
     * 根据DataSource、隔离级别和是否自动提交创建Transacion
     *
     * @param ds
     * @param level Desired isolation level
     * @param autoCommit Desired autocommit
     * @return
     */
  public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
    return new JdbcTransaction(ds, level, autoCommit);
  }
}

JdbcTransactionFactory会创建JDBC类型的Transaction,即JdbcTransaction,ManagedTransactionFactory也会创建ManagedTransaction。下面分别深入JdbcTranaction 和ManagedTransaction,看它们到底是怎样实现事务管理的。

 JdbcTransaction事务

 JdbcTransaction使用JDBC的提交和回滚事务管理机制 ,它依赖与从dataSource中取得的连接connection 来管理transaction 的作用域,connection对象的获取被延迟到调用getConnection()方法。如果autocommit设置为on,开启状态的话,它会忽略commit和rollback。

    直观地讲,就是JdbcTransaction是使用的java.sql.Connection 上的commit和rollback功能,JdbcTransaction只是相当于对java.sql.Connection事务处理进行了一次包装(wrapper),Transaction的事务管理都是通过java.sql.Connection实现的。JdbcTransaction的代码实现如下:

/**
 * @see JdbcTransactionFactory
 */
/**
 * @author Clinton Begin
 */
public class JdbcTransaction implements Transaction {
  
  private static final Log log = LogFactory.getLog(JdbcTransaction.class);
  
  //数据库连接
  protected Connection connection;
  //数据源
  protected DataSource dataSource;
  //隔离级别(下面会重点讲到)
  protected TransactionIsolationLevel level;
  //是否为自动提交
  protected boolean autoCommmit;
  
  public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
    dataSource = ds;
    level = desiredLevel;
    autoCommmit = desiredAutoCommit;
  }
  
  public JdbcTransaction(Connection connection) {
    this.connection = connection;
  }
  
  public Connection getConnection() throws SQLException {
    if (connection == null) {
      openConnection();
    }
    return connection;
  }
  
    /**
     * commit()功能 使用connection的commit()
     * @throws SQLException
     */
  public void commit() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Committing JDBC Connection [" + connection + "]");
      }
      connection.commit();
    }
  }
  
    /**
     * rollback()功能 使用connection的rollback()
     * @throws SQLException
     */
  public void rollback() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Rolling back JDBC Connection [" + connection + "]");
      }
      connection.rollback();
    }
  }
  
    /**
     * close()功能 使用connection的close()
     * @throws SQLException
     */
  public void close() throws SQLException {
    if (connection != null) {
      resetAutoCommit();
      if (log.isDebugEnabled()) {
        log.debug("Closing JDBC Connection [" + connection + "]");
      }
      connection.close();
    }
  }
  
  protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
    try {
      if (connection.getAutoCommit() != desiredAutoCommit) {
        if (log.isDebugEnabled()) {
          log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");
        }
        connection.setAutoCommit(desiredAutoCommit);
      }
    } catch (SQLException e) {
      // Only a very poorly implemented driver would fail here,
      // and there's not much we can do about that.
      throw new TransactionException("Error configuring AutoCommit.  "
          + "Your driver may not support getAutoCommit() or setAutoCommit(). "
          + "Requested setting: " + desiredAutoCommit + ".  Cause: " + e, e);
    }
  }
  
  protected void resetAutoCommit() {
    try {
      if (!connection.getAutoCommit()) {
        // MyBatis does not call commit/rollback on a connection if just selects were performed.
        // Some databases start transactions with select statements
        // and they mandate a commit/rollback before closing the connection.
        // A workaround is setting the autocommit to true before closing the connection.
        // Sybase throws an exception here.
        if (log.isDebugEnabled()) {
          log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]");
        }
        connection.setAutoCommit(true);
      }
    } catch (SQLException e) {
      log.debug("Error resetting autocommit to true "
          + "before closing the connection.  Cause: " + e);
    }
  }
  
  protected void openConnection() throws SQLException {
    if (log.isDebugEnabled()) {
      log.debug("Opening JDBC Connection");
    }
    connection = dataSource.getConnection();
    if (level != null) {
      connection.setTransactionIsolation(level.getLevel());
    }
    setDesiredAutoCommit(autoCommmit);
  }
}

 事务隔离级别(TransactionIsolationLevel )

mybatis定义了五种事务级别,在包org.apache.ibatis.session下,源代码如下:

/**
 * @author Clinton Begin
 */
public enum TransactionIsolationLevel {
  NONE(Connection.TRANSACTION_NONE),
  READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED),
  READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED),
  REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ),
  SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE);

  private final int level;

  TransactionIsolationLevel(int level) {
    this.level = level;
  }

  public int getLevel() {
    return level;
  }
}

和上面介绍的隔离级别对应

 

测试核心代码:

            String resource = "conf/mybatis-config.xml";
			InputStream inputStream = Resources.getResourceAsStream(resource);
			SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
					.build(inputStream);
			
			SqlSession sqlSession = sqlSessionFactory.openSession();
			userDao = new MybatisDaoImpl(sqlSession);
			User user =  userDao.selectUserById(1);
			
			sqlSession.commit();
			sqlSession.close();

 

总结:事务,先搞懂数据库事务,然后看原生JDBC的事务表现,最后才是框架包装的事务。

 

参考网址:

0 . 这一次,带你搞清楚MySQL的事务隔离级别!http://blog.itpub.net/31559358/viewspace-2221931/

1.  十分钟搞懂MySQL四种事务隔离级别 https://juejin.im/post/5c9756296fb9a070ad504a05

2. JDBC 4.2 Specification规范 https://download.oracle.com/otn-pub/jcp/jdbc-4_2-mrel2-eval-spec/jdbc4.2-fr-spec.pdf?AuthParam=1588653978_d9c7458a368a7a1630e262c8325ca34b

也可在我的github上获取:https://github.com/dongguangming/java/blob/master/jdbc4.2-fr-spec.pdf

3. 事务基础和分布式事务

https://www.ibm.com/developerworks/cn/cloud/library/cl-manage-cloud-transactions_1/index.html

4. mybatis jdbc transaction.java https://mybatis.org/mybatis-3/zh/jacoco/org.apache.ibatis.transaction.jdbc/JdbcTransaction.java.html 

5. The ABCs of JDBC, Part 5 - Transactions 

https://dzone.com/articles/jdbc-faq-transactions

6. Java JDBC Transaction Management and Savepoint

https://www.journaldev.com/2483/java-jdbc-transaction-management-savepoint

7. JDBC transaction performance tips

https://www.javaperformancetuning.com/tips/jdbctransaction.shtml

8. spring4-and-mybatis-transaction-example(要FQ) https://edwin.baculsoft.com/2015/01/a-simple-spring-4-and-mybatis-transaction-example/

 

参考书籍:

MyBatis技术内幕

posted @ 2020-05-07 22:09  董广明  阅读(5610)  评论(0编辑  收藏  举报