Spring

数据库事务和@Transaction注解

2019-03-10  本文已影响0人  斯文遮阳

数据库事务(Database Transaction) ,是指作为单个逻辑工作单元执行的一系列操作,要么完全地执行,要么完全地不执行。 事务处理可以确保除非事务性单元内的所有操作都成功完成,否则不会永久更新面向数据的资源。通过将一组相关操作组合为一个要么全部成功要么全部失败的单元,可以简化错误恢复并使应用程序更加可靠。

事务的基本要素(ACID)

当单进程执行时,很容易保证事务的4中基本要素,并且不会引发问题,然而当多个并发进程共同执行事务时候,可能会引发不同的问题。

事务隔离级别

隔离级别 脏读 不可重复读 幻读 特点
未提交读(read-uncommitted) 优点在于并发能力高,适合那些对数据一致性没有要求而追求高并发的场景,缺点是脏读
读写提交(read-committed) × 出现不可重复读
可重复读(repeatable-read) × × mysql的默认事务隔离级别。会出现幻读
串行化(serializable) × × × 并行性低,一般不用。通常消除幻读使用数据库锁的方式

数据库锁

乐观锁

相对悲观锁而言,乐观锁假设认为数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则让返回用户错误的信息,让用户决定如何去做。一般的实现乐观锁的方式就是记录数据版本,并且不会造成线程的阻塞。

但是,版本的冲突会造成请求失败的概率剧增,这时往往需要通过重入的机制将请求失败的概率降低。而多次的重入又会带来过多执行SQL的问题,为了克服这个问题,可以考虑使用按时间戳或者限制重入次数的办法。

悲观锁

正如其名,它指的是对数据被外界修改持保守态度。因此在整个数据处理过程中,将数据处于锁定状态。 悲观锁的实现,往往依靠数据库提供的锁机制 (也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

数据库两种锁可以实现悲观锁,排他锁(Exclusive Lock,也叫X锁)和共享锁(Shared Lock,也叫S锁)。排他锁表示对数据进行写操作,如果一个事务对对象加了排他锁,其他事务就不能再给它加任何锁了;共享锁会为操作对象加共享锁,可以允许其他共享锁的加锁操作,但是不允许排它锁加锁操作。

锁粒度

MySQL锁的粒度几种,从小到大分别是,行锁(Record Lock)、间隙锁(Gap Lock)、Next-Key Lock、表锁。
1、 行锁:锁定一行记录
2、 间隙锁:锁定一个范围,但是不包含记录本身
3、Next-Key Lock:Record Lock和Gap Lock的组合,锁定一个区间以及对应的记录
4、 表锁:锁定整张表

MySQL innoDB加锁是通过索引项判断加锁的范围,即如果操作的对象上没有索引或者没有使用索引,则会导致锁的粒度扩大。因为InnoDB索引采用的BTree结构,所以,如果不是唯一索引,则如果使用查询值索引不存在,或者使用的条件查询则会引发Next-Key Lock。

意向锁

有了共享锁(S锁)和排它锁(X锁),能够实现数据库操作不同粒度的加锁,但是在一个操作需要给整个表加X锁的时候,需要确定整个表以及表的每一行没有被加S锁或者X锁,那么如何确定表中每一行没有被加锁,需要扫描整个表吗?如果表中有海量记录需要长时间等待,此时需要有意向锁。

Spring的@Transaction注解

在Spring中,为了 “擦除”令人厌烦的 try...catch...finally..语句,减少代码中数据库连接开闭和事务回滚提交的代码, Spring利用其 AOP 为我们提供了一个数据库事务的约定流程,这样开发的代码可读性就更高,也更好维护。

Spring数据库事务约定

@Transactional源码分析:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    // 通过 bean name 指定事务管理器
    @AliasFor("transactionManager")
    String value() default "";
    // 同 value 属性
    @AliasFor("value")
    String transactionManager() default "";
    // 传播行为
    Propagation propagation() default Propagation.REQUIRED;
    // 隔离级别
    Isolation isolation() default Isolation.DEFAULT;
    // 超时时间
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
    // 是否只读事务
    boolean readOnly() default false;
    // 方法在发生指定异常时回滚,默认是所有异常都囚滚
    Class<? extends Throwable>[] rollbackFor() default {};
    // 方法在发生指定异常名称时回滚,默认是所有异常都回滚
    String[] rollbackForClassName() default {};
    // 方法在发生指定异常时不回滚,默认是所有异常都回滚
    Class<? extends Throwable>[] noRollbackFor() default {};
    // 方法在发生指定异常名称时不回滚,默认是所有异常都回滚
    String[] noRollbackForClassName() default {};
}

事务管理器

事务的打开、回滚和提交是由事务管理器来完成的。在Spring中,事务管理器的顶层接口为PlatformTransactionManager,Spring也定义了一些其他的接口和类。

在Spring Boot中,当你依赖于mybatis-spring-boot-starter之后,它会自动创建一个DataSource­ TransactionManager对象作为事务管理器;如果依赖于spring-boot-starter-data-jpa,则它会自动创建JpaTransactionManager对象作为事务管理器,所以我们一般不需要自己创建事务管理器而直接使用它们即可。

传播行为

传播行为是方法之间调用事务采取的策略。在大部分的情况下,我们会认为数据库事务要么全部成功,要么全部失败。但当执行批量任务时,我们有时不希望因为极少数的任务不能完成而回滚所有的批量任务 。

在Spring中,当一个方法调用另外一个方法时,可以让事务采取不同的策略工作,如新建事务或者挂起当前事务等,这便是事务的传播行为。例如,批量任务我们称之为当前方法,当它调用单个任务时,称单个任务为子方法。当前方法调用子方法的时候,让每一个子方法不在当前事务中执行,而是创建一个新的事务,我们就说当前方法调用子方法的传播行为为新建事务。此外,还可能让方法在无事务、独立事务中执行,这些完全取决于业务需求。

源码分析:

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),
    // 在当前方法调用子方法时,如果子方法发生异常,只因滚子方法执行过的 SQL,而不回滚当前方法的事务
    NESTED(TransactionDefinition.PROPAGATION_NESTED);

    private final int value;

    Propagation(int value) { this.value = value; }

    public int value() { return this.value; }

}

常用的传播行为是REQUIRED、REQUIRES_NEW、NESTED三种。

@Transactional自调用失效问题

Spring数据库事务的实现原理是AOP,而AOP的原理是动态代理。在事务自调用的过程中,是类自身的调用,而不是代理对象去调用,那么就不会产生AOP,这样Spring就不能把你的代码织入到约定的流程中,于是就产生了@Transactional自调用失效的场景。

有两种解决方法:用一个Service去调用另一个Service,这样就是代理对象的调用,Spring就会将代码织入事务流程;或者从Spring IoC容器中获取代理对象去启用AOP。

参考资料

上一篇下一篇

猜你喜欢

热点阅读