Skip to main content Link Menu Expand (external link) Document Search Copy Copied

1. 事务的特性

  1. 原子性(Atomicity): 事务是数据库的逻辑工作单位,事务中包括的诸操作要么全做,要么全不做。
  2. 一致性(Consistency): 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。 一致性与原子性是密切相关的。
  3. 隔离性(Isolation): 一个事务的执行不能被其他事务干扰。
  4. 持续性/永久性(Durability): 一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。

2. 脏读、不可重复读、幻读

  1. 脏读:事务A读取事务B没有提交的数据,如余额B修改前是1000,修改后是2000,事务读取了2000, 但B发生异常,回滚后,真实的金额是1000,所以A读取的脏数据
  2. 不可重复读:事务A第一次读取了单据的状态是未提交,去请求一个较长的IO数据,事务B把这个单据状态给置为取消了, 事务A的IO结束,再取一次单据的状态,和第一次不一样,就是不可重复读
  3. 幻读:事务A在执行读取操作,需要两次统计数据的总量,前一次查询数据总量后,此时事务B执行了新增数据的操作并提交后, 这个时候事务A读取的数据总量和之前统计的不一样,就像产生了幻觉一样,平白无故的多了几条数据,成为幻读。

不可重复读和幻读的区别是,前者是update、delete,后者是insert

3. 事务的隔离级别

Mysql中,默认的事务隔离级别是可重复读(repeatable-read),为了解决不可重复读,Innodb采用了MVCC(多版本并发控制)来解决这一问题。 MVCC是利用在每条数据后面加了隐藏的两列(创建版本号和删除版本号),每个事务在开始的时候都会有一个递增的版本号, 用来和查询到的每行记录的版本号进行比较。

事务级别 脏读 不可重复读 幻读
串行化 Serializable N N N
可重复读 Repeatable read N N Y
读提交 Read committed N Y Y
读未提交 Read uncommitted Y Y Y

3.1. MVCC

在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。 在可重读Repeatable reads事务隔离级别下:

  • SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。
  • INSERT时,保存当前事务版本号为行的创建版本号
  • DELETE时,保存当前事务版本号为行的删除版本号
  • UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行

通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用, 大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。

-

4. 事务的传播性

4.1. 什么时候出现事务的传播

在事务A中,存在事务B,就出现了事务的传播。Spring中事务的传播有以下几类:

// Spring中org.springframework.transaction.annotation.Propagation类的定义
public enum Propagation {
    // 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
    // 当前方法存在事务时,子方法加入该事务。
    // 此时父子方法共用一个事务,无论父子方法哪个发生异常回滚,
    // 整个事务都回滚。即使父方法捕捉了异常,也是会回滚。
    // 而当前方法不存在事务时,子方法新建一个事务。
    REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED),

    // 如果当前存在事务,则加入该事务;如果当前没有事务,
    // 则以非事务的方式继续运行。
    SUPPORTS(TransactionDefinition.PROPAGATION_SUPPORTS),

    // 如果当前存在事务,则加入该事务;
    // 如果当前没有事务,则抛出异常。
    MANDATORY(TransactionDefinition.PROPAGATION_MANDATORY),

    // 创建一个新的事务,如果当前存在事务,则把当前事务挂起。
    // 无论当前方法是否存在事务,子方法都新建一个事务。此时父子方法的事务时独立的,
    // 它们都不会相互影响。但父方法需要注意子方法抛出的异常,
    // 避免因子方法抛出异常,而导致父方法回滚。
    REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),

    // 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    NOT_SUPPORTED(TransactionDefinition.PROPAGATION_NOT_SUPPORTED),

    // 以非事务方式运行,如果当前存在事务,则抛出异常。
    NEVER(TransactionDefinition.PROPAGATION_NEVER),

    // 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;
    // 如果当前没有事务,则该取值等价于 REQUIRED
    // 当前方法存在事务时,子方法加入在嵌套事务执行。
    // 当父方法事务回滚时,子方法事务也跟着回滚。
    // 当子方法事务发送回滚时,父事务是否回滚取决于是否捕捉了异常。
    // 如果捕捉了异常,那么就不回滚,否则回滚。
    NESTED(TransactionDefinition.PROPAGATION_NESTED);
}

5. Spring 事务的两种方式

5.1. 声明式

声明式事务是建立在 AOP 之上的。其本质是对方法前后进行拦截, 然后在目标方法开始之前创建或者加入一个事务, 在执行完目标方法之后根据执行情况“提交”或者“回滚”事务。

@Transactional
public void methodA(){
        jdbcTemplate.batchUpdate(updateSql,params);
        methodB();
        }

// 传播行为配置为 - 方式2,不使用当前事务,独立一个新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){
        jdbcTemplate.batchUpdate(updateSql,params);
        }

5.2. 编程式

编程式事务使用 TransactionTemplate 或者 直接使用底层的 PlatformTransactionManager 实现事务。 对于编程式事务 Spring 比较推荐使用 TransactionTemplate 来对事务进行管理。 其中 TransactionTemplate 的 execute 能接受两种类型参数执行事务,分别为:

 TransactionCallback<Object>(): 执行事务且可以返回一个值。
        TransactionCallbackWithoutResult(): 执行事务没有返回值。

6. 事务的回滚

6.1. 回滚失败

6.1.1. 异常分类不当

@Transaction时,当拋出的异常不是RuntimeException或Error时, 是不会回滚的,如就是Exception,事务就不会加滚

@Transactional(rollbackFor=Exception.class)

通过上面的声明,可以到遇到Exception时,可以回滚

6.1.2. 异常被捕获

7. 事务的失效

7.1. 自调用失败


@Service
public class DefaultTransactionService implement Service {

    public void saveUser() throws Exception {
        //do something
        doInsert();
    }

    @Transactional(rollbackFor = Exception.class)
    public void doInsert() throws IOException {
        User user = new User();
        UserService.insert(user);
        throw new IOException();

    }
}

因为Spring的事务管理功能是通过动态代理实现的,而Spring默认使用JDK动态代理,而JDK动态代理采用接口实现的方式, 通过反射调用目标类。简单理解,就是saveUser()方法中调用this.doInsert(),这里的this是被真实对象, 所以会直接走doInsert的业务逻辑,而不会走切面逻辑,所以事务失败。 解决方案:

  1. 方案一:解决方法可以是直接在启动类中添加@Transactional注解saveUser()
  2. 方案二:@EnableAspectJAutoProxy(exposeProxy = true)在启动类中添加,会由Cglib代理实现。

7.2. 非Spring容器管理的类来

// @Service
public class OrderServiceImpl implements OrderService {
    @Transactional
    public void updateOrder(Order order) {
        // update order
    }
}

如果此时把 @Service 注解注释掉,这个类就不会被加载成一个 Bean, 那这个类就不会被 Spring 管理了,事务自然就失效了。

7.3. 只能public

@Transactional 注解只有作用到 public 方法上事务才生效。被 @Transactional 注解的方法所在的类必须被 Spring 管理。

7.4. 底层数据库要支持

底层使用的数据库必须支持事务机制,否则不生效。

7.5. 多线程

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);
        new Thread(() -> {
            try {
                test();
            } catch (Exception e) {
                roleService.doOtherThing();
            }
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        try {
            int i = 1 / 0;
            System.out.println("保存role表数据");
        } catch (Exception e) {
            throw new RuntimeException();
        }
    }
}

可以看到事务方法add中,调用了事务方法doOtherThing, 但是事务方法doOtherThing是在另外一个线程中调用的。 这样会导致两个方法不在同一个线程中,获取到的数据库连接不一样, 从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。 我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。 如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

8. 参考文献

  • https://www.cnblogs.com/chanshuyi/p/head-first-of-spring-transaction.html
  • https://segmentfault.com/a/1190000040130617
  • https://zhuanlan.zhihu.com/p/337921874