Mybatis|Mybatis事务管理深入理解

种一棵树最好的时间是十年前,其次是现在;

1.提到数据库事务,就会浮现出事务的四大特性(ACID)、四大隔离级别、七大传播特性。

事务四大特性
  • 原子性(Atomicity)
事务中所有操作是不可再分割的原子单位。事务中所有操作要么全部执行成功,要么全部执行失败。
  • 一致性(Consistency)
事务执行后,数据库状态与其它业务规则保持一致。如转账业务,无论事务执行成功与否,参与转账的两个账号余额之和应该是不变的。
  • 隔离性(Isolation)
隔离性是指在并发操作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。
  • 持久性(Durability)
一旦事务提交成功,事务中所有的数据操作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须能保证通过某种机制恢复数据。

四大隔离级别
  • Read uncommitted
读未提交,顾名思义,就是一个事务可以读取另一个未提交事务的数据。
  • Read committed
【Mybatis|Mybatis事务管理深入理解】读提交,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。若有事务对数据进行更新(UPDATE)操作时,读操作事务要等待这个更新操作事务提交后才能读取数据,可以解决脏读问题。但在这个事例中,出现了一个事务范围内两个相同的查询却返回了不同数据,这就是不可重复读。
  • Repeatable read
重复读(MySQL 默认事务级别),就是在开始读取数据(事务开启)时,不再允许修改操作。重复读可以解决不可重复读问题。这里应该明白的一点就是,不可重复读对应的是修改,即UPDATE操作。但是可能还会有幻读问题。因为幻读问题对应的是插入INSERT操作,而不是UPDATE操作。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
  • Selializable
Serializable 是最高的事务隔离级别,在该级别下,事务串行化顺序执行,可以避免脏读、不可重复读与幻读。但是这种事务它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。隔离级别效率低下,比较耗数据库性能,一般不使用。

四大还好说,问题是七大传播特性是哪儿来的?是Spring在当前线程内,处理多个数据库操作方法事务时所做的一种事务应用策略。事务本身并不存在什么传播特性,不要混淆事务本身和Spring的事务应用策略。

2.说到事务,可能又会想起create、begin、commit、rollback、close、suspend。可实际上,只有commit、rollback是实际存在的,剩下的create、begin、close、suspend都是虚幻的,是业务层或数据库底层应用语意,而非JDBC事务的真实命令。
create(事务创建):不存在。
begin(事务开始):姑且认为存在于DB的命令行中,比如Mysql的start transaction 命令,以及其他数据库中的begin transaction命令。JDBC中不存在。
close(事务关闭):不存在。应用程序接口中的close()方法,是为了把connection放回数据库连接池中,供下一次使用,与事务毫无关系。
suspend(事务挂起):不存在。Spring中事务挂起的含义是,需要新事务时,将现有的connection1保存起来(它还有尚未提交的事务),然后创建connection2,connection2提交、回滚、关闭完毕后,再把connection1取出来,完成提交、回滚、关闭等动作,保存connection1的动作称之为事务挂起。在JDBC中,是根本不存在事务挂起的说法的,也不存在这样的接口方法。

因此,事务的三个真实存在的方法,不要被各种事务状态名词所迷惑,它们分别是:conn.setAutoCommit()、conn.commit()、conn.rollback()。
conn.close()含义为关闭一个数据库连接,这已经不再是事务方法了。

1. Mybaits中的事务接口Transaction
// 3.4.6 public interface Transaction { // Retrieve inner database connection Connection getConnection() throws SQLException; // Commit inner database connection. void commit() throws SQLException; // Rollback inner database connection. void rollback() throws SQLException; // Close inner database connection. void close() throws SQLException; // Get transaction timeout if set Integer getTimeout() throws SQLException; }


当你再次看到close()方法时,千万别再认为是关闭一个事务了,而是关闭一个conn连接,或者是把conn连接放回连接池内。
MyBatis 事务的两个实现
Mybatis|Mybatis事务管理深入理解
文章图片


JdbcTransaction:单独使用Mybatis时,默认的事务管理实现类,就和它的名字一样,它就是我们常说的JDBC事务的极简封装,和编程使用mysql-connector-java.jar事务驱动一样。其极简封装,仅是让connection支持连接池而已。
ManagedTransaction:含义为托管事务,空壳事务管理器,皮包公司。仅是提醒用户,在其它环境中应用时,把事务托管给其它框架,比如托管给Spring,让Spring去管理事务。
org.apache.ibatis.transaction.jdbc.JdbcTransaction.java close源码

@Override public void close() throws SQLException { if (connection != null) { // 重新设置事务提交 resetAutoCommit(); if (log.isDebugEnabled()) { log.debug("Closing JDBC Connection [" + connection + "]"); } connection.close(); } }

前面说的close 是关闭连接可以在这里看见调用的是connection.close()。但是在之前竟然调用了resetAutoCommit(),这里指的是把connection的自动提交的值设置为初始状态。但是为什么呢?
首先我们要明确,我们使用JDBC做数据库操作时,每次开启连接和关闭连接是占用的大部分资源的,这个一般使用连接池,在连接池中建立连接实例,用的时候从连接池中拿取,用完了之后就放回去。这里的resetAutoCommit()就是是连接实例恢复初始状态。
2. 事务工厂TransactionFactory org.apache.ibatis.transaction.TransactionFactory.java 接口定义

public interface TransactionFactory { // Sets transaction factory custom properties. void setProperties(Properties props); // Creates a {@link Transaction} out of an existing connection. // @since 3.1.0 Transaction newTransaction(Connection conn); // Creates a {@link Transaction} out of a datasource. // @since 3.1.0 Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit); }

MyBatis 事务工厂的两个实现

Mybatis|Mybatis事务管理深入理解
文章图片





一个生产JdbcTransaction实例,一个生产ManagedTransaction实例。两个工厂类,只有新建对应的实例;
使用:
mybatis-config.xml配置文件内,可配置事务管理类型。
dataSource 属性type就可以设置连接类型:UNPOOLED,POOLED,JNDI;



3. Transaction的用法 无论是SqlSession,还是Executor,它们的事务方法,最终都指向了Transaction的事务方法,即都是由Transaction来完成事务提交、回滚的。
Mybatis|Mybatis事务管理深入理解
文章图片


/** * @author zhaojm * @date 2020/4/12 10:53 */ public class TransactionTest { public static void main(String[] args) { String resource = "org/mybatis/internal/example/Configuration.xml"; Reader reader; try { // mybatis 初始化 reader = Resources.getResourceAsReader(resource); SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader); SqlSession sqlSession = factory.openSession(); try { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User addUser = new User("transaction", 14); userMapper.addUser(addUser); sqlSession.commit(); } catch (Exception e) { sqlSession.rollback(); } finally { sqlSession.close(); } } catch (IOException e) { e.printStackTrace(); } } }


如果把 sqlSession.commit(); 注释掉运行结果如下图:
updates: 1 表示执行的一句update语句,但是我们并没有异常就不会执行rollback()方法,但是确实有回滚了,这是因为默认的autoCommit是关闭的。
Mybatis|Mybatis事务管理深入理解
文章图片

然后再来看rollback()的调用用链(如下图),可以发现调用rollback的地方在close()中,在关闭之前会判断是否需要提交回滚(isCommitOrRollbackRequired(false))发现我们没用设置自动提交,dirty表示是否有脏数据(未提交update会设置dirty为true,insert,delete 实际调用的也是update方法)返回需要强制回滚,最终调用Transaction 的回滚。
Mybatis|Mybatis事务管理深入理解
文章图片



private boolean isCommitOrRollbackRequired(boolean force) { return (!autoCommit && dirty) || force; }

4. 有关事务的几种特殊场景表现(重要) 1. 一个conn生命周期内,可以存在无数多个事务。
再之前的代码中第一次提交之后再加入一条insert语句,然后再commit,如果不执行第二次的commit()方法,那这时候的回滚是两条数据一起回滚还是一条数据回滚?
Mybatis|Mybatis事务管理深入理解
文章图片


运行之后发现只有一次回滚了,第一次提交的数据已经写入库中了,由此可知rollback只回滚当前为提交的事务。
对于JDBC来说,autoCommit=false时,是自动开启事务的,执行commit()后,该事务结束。JDBC中不存在Hibernate中的session的概念,在JDBC中,insert了几次,数据库就会有几条记录,切勿混淆。
Mybatis的JdbcTransaction,和纯粹的Jdbc事务,几乎没有差别,它仅是扩展支持了连接池的connection。另外,需要明确,无论你是否手动处理了事务,只要是对数据库进行任何update操作(update、delete、insert),都一定是在事务中进行的,这是数据库的设计规范之一。

参考:https://my.oschina.net/zudajun/blog/666764
《Mybatis 解读》

    推荐阅读