一、事务简单介绍
事务具有四大特性ACID:分别指:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
1.1 事务ACID
-
原子性(Atomicity):指同一个事务中的操作要么都执行,要么都回滚,回滚后就像事情没有发生过一样。
-
一致性(Consistency):数据库数据在执行前处于正确状态,在执行后也要处于正确状态,即不能破坏数据的完整性约束。比如A转账100给B,不能出现A的钱最后减100,B的钱却没有加100。一致性还可分为强一致性和最终一致性。一般来说,关系型数据库需要保证强一致性,而NoSQL数据库需要保证数据最终一致性。
-
隔离性(Isolation):指并发事务执行互不干扰,sql规范定义了4种隔离级别,分别为脏读、读写提交、可重复读、序列化。不同隔离级别对事务影响不同。
-
持久性(Durability):指事务对数据的修改是永久不可改变的。
1.2 Spring事务属性
来看看Spring事务定义:
public interface TransactionDefinition {
/**
* 返回传播行为
*/
int getPropagationBehavior();
/**
* 返回事务隔离级别
*/
int getIsolationLevel();
/**
* 返回事务超时时间
*/
int getTimeout();
/**
* 返回是否优化为只读事务。
*/
boolean isReadOnly();
}
除此之外还有rollbackFor、noRollbackFor等配置,这些配置属性组成了事务执行策略,包含以下五个策略:传播行为,隔离规则,回滚规则,超时时间,是否只读。
1.2.1 传播行为
事务的传播行为指的是当一个事务方法被另一个事务方法调用,存在多个事务操作时,如何处理这些事务行为,比如是用同一个事务还是开启新的事务。TransactionDefinition中定义了七种传播行为:
传播行为 | 说明 |
---|---|
PROPAGATION_REQUIRED | 支持当前事务,如果当前不存在事务则新建一个事务,通常这是Spring默认的传播行为 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,则以非事务方式执行 |
PROPAGATION_MANDATORY | 支持当前事务,如果当前事务不存在则会抛出一个异常 |
PROPAGATION_REQUIRES_NEW | 新建一个事务,如果当前存在一个事务,则将当前事务挂起。新建的事务和当前事务是两个独立的事务,可通过捕获新建事务执行异常判断当前事务是否需要回滚操作 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务中执行。如果当前不存在事务,则执行类似PROPAGATION_REQUIRED操作 |
1.2.2 隔离规则
事务的隔离级别控制一个事务在并发情况下可能受到的影响程度。并发情况下,事务可能出现以下几个问题:
- 脏读(Dirty reads):A事务更新了数据,但未提交事务,B读取了A更新的数据,当A事务更新的数据回滚了,B读取到的数据就是脏数据。
- 不可重复读(Nonrepeatable read):A事务多次读取同一个数据,在读取过程中B事务将数据更新了,导致A事务多次读取的同一个数据不一致。
- 幻读(Phantom read):A事务对一定范围内数据进行批量修改,在过程中B事务在这个范围内插入了一条新的数据,最后导致批量修改操作完成后再次查询相同范围的数据,发现没有修改到B事务插入的数据,就像出现了幻觉一样。
总结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
为了避免并发情况下出现的脏读,不可重复读,幻读的问题,Spring在TransactionDefinition中定义了五种隔离规则:
隔离级别 | 说明 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
ISOLATION_DEFAULT | 使用数据库默认的隔离级别 | 默认 | 默认 | 默认 |
ISOLATION_READ_UNCOMMITTED | 允许读取事务尚未提交的数据,是最低的隔离级别 | 会 | 会 | 会 |
ISOLATION_READ_COMMITTED | 允许读取事务已经提交的数据 | 不会 | 会 | 会 |
ISOLATION_REPEATABLE_READ | 多次读取结果都一致,除非结果由自己修改 | 不会 | 不会 | 会 |
ISOLATION_SERIALIZABLE | 最高的隔离级别,通过锁表实现 | 不会 | 不会 | 不会 |
总结:实际应用中,最高隔离级别ISOLATION_SERIALIZABLE一般很少使用,因为锁表会导致其他事务执行时直接报错。一般采用READ_COMMITTED或者REPEATABLE_READ隔离级别,如MySQL默认的隔离级别是REPEATABLE-READ,Oracle默认的隔离级别是READ_COMMITTED。
1.2.3 回滚规则
回滚规则是定义哪些异常会回滚,哪些不会回滚事务。Spring事务默认捕获到未检查异常会回滚事务,其中包括RuntimeException和Error异常。你也可以使用rollbackFor自定义哪些异常会回滚事务、noRollbackFor自定义哪些异常不会回滚事务。
1.2.4 超时时间
为了合理利用数据库资源,设置事务执行超时时间的设置是有必要的,@Transactional注解的timeout属性可设置事务超时时间,超过设置的时间事务会自动回滚。
这里小插一句,timeout设置的超时时间容易使人产生错误的理解,简单总结就是:Spring事务超时 = 事务开始时到最后一个Statement创建时时间 + 最后一个Statement的执行时超时时间(即其queryTimeout)。
1.2.5 是否只读
数据库引擎可以根据客户端的事务规则优化事务,提升数据库效率。如果设置@Transactional注解的属性(readOnly = true),表示当前事务只会读取数据库数据,而不会更新数据。
二、@Transactional注解
2.1 简单介绍
- Spring 事务管理分为编码式和声明式的两种,我们这里主要讲声明式事务实现方式。声明时事务基于AOP,本质是对事务方法执行前后进行拦截,将目标方法加入到事务中。可以通过xml配置方式和@Transactional注解方式实现,很明显注解方式更优雅。
- @Transactional注解 可以作用于接口、接口方法、类以及类方法上。由于基于AOP,因此用于方法上时,必须是public方法才会生效,使用到其他类型方法上也不会抛异常,只是会自动忽略。
- 默认情况下,也是由于AOP动态代理的缘故。类中的方法若没有来自外部的方法调用;或者同一个类中有来自外部方法调用却没有添加@Transactional注解,再调用本类内部添加了@Transactional注解的其他方法,事务均不会生效。这里理解起来可能比较绕, 2.3 的使用举例会详细说明注解生效规则。
2.2 注解属性
我们来看看@Transactional注解中定义的属性:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
/**
* 当有多个transactionManager配置时,可以指定Manager
*/
@AliasFor("transactionManager")
String value() default "";
/**
* 作用同上
*/
@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 {};
}
- value()和transactionManager()属性可以指定存在多个TransactionManager的其中一个qualifier属性值或者bean名称。
- propagation()属性可以设置Propagation枚举类中的七个传播行为,参考1.2.1,默认为REQUIRED。
- isolation()属性可以设置Isolation枚举类中的五个隔离级别,参考1.2.2,默认为DEFAULT。
- timeout()设置事务超时时间,默认不超时。
- readOnly()设置事务是否是只读事务。
- rollbackFor()和rollbackForClassName()设置哪些异常事务会回滚。
- noRollbackFor()和noRollbackForClassName()设置哪些异常事务不会回滚。
2.3 使用举例
注:以下举例仅第一个例子是完整代码,后面均省略公共代码
@Data
@Entity
@Table(name = "sal_emp")
public class SalEmpEntity {
public SalEmpEntity() { }
public SalEmpEntity(Integer id, String name) {
this.id = id;
this.name = name;
}
@Id
@Column(name = "id")
private Integer id;
@Column(name = "name")
private String name;
}
@Repository
public interface SalEmpRepository extends CrudRepository<SalEmpEntity, Integer> { }
public interface IAClass {
void aFunction();
void aInnerFunction();
}
2.3.1 同类不同方法
情况一:aFunction添加@Transactional注解,aInnerFunction函数不添加,aInnerFunction抛异常。
@Slf4j
@Service
public class AClassImpl implements IAClass {
@Autowired
private SalEmpRepository salEmpRepository;
@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
SalEmpEntity entity1 = new SalEmpEntity(4, "name1");
salEmpRepository.save(entity1);
this.aInnerFunction();
}
@Override
public void aInnerFunction() {
SalEmpEntity entity2 = new SalEmpEntity(5, "name2");
salEmpRepository.save(entity2);
throw new RuntimeException("occur Exception!");
}
}
结果:两个函数的操作都会回滚。
情况二:aFunction添加@Transactional注解,aInnerFunction函数不添加,aInnerFunction抛异常,不过在aFunction里面把异常捕获了。
@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
SalEmpEntity entity1 = new SalEmpEntity(4, "name1");
salEmpRepository.save(entity1);
try {
this.aInnerFunction();
} catch (Exception e) {
log.info(e.getMessage());
}
}
@Override
public void aInnerFunction() {
SalEmpEntity entity2 = new SalEmpEntity(5, "name2");
salEmpRepository.save(entity2);
throw new RuntimeException("occur Exception!");
}
结果:日志打印“occur Exception!”,并且两个方法均不会回滚,因为@Transactional需要捕获到异常才会回滚事务,例子中使用try catch将异常捕获了,并且没有继续往上抛异常。
情况三:aFunction和aInnerFunction均添加注解,aInnerFunction抛异常。
@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
SalEmpEntity entity1 = new SalEmpEntity(4, "name1");
salEmpRepository.save(entity1);
this.aInnerFunction();
}
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
@Override
public void aInnerFunction() {
SalEmpEntity entity2 = new SalEmpEntity(5, "name2");
salEmpRepository.save(entity2);
throw new RuntimeException("occur Exception!");
}
结果:和情况一相同,事务会回滚,因为同类方法相互调用,只有外部方法的注解生效,内部添加的注解无效。
情况四:aFunction不添加注解,aInnerFunction添加注解,aInnerFunction抛异常。
@Override
public void aFunction() {
SalEmpEntity entity1 = new SalEmpEntity(4, "name1");
salEmpRepository.save(entity1);
this.aInnerFunction();
}
@Transactional(rollbackFor = Exception.class)
@Override
public void aInnerFunction() {
SalEmpEntity entity2 = new SalEmpEntity(5, "name2");
salEmpRepository.save(entity2);
throw new RuntimeException("occur Exception!");
}
结果:事务不会回滚,因为只有外部方法的注解生效,内部添加的注解不会生效。
2.3.2 不同类不同方法
新增加IBClass接口:
public interface IBClass {
void bFunction();
}
情况一:aFunction添加注解,bFunction不添加注解,bFunction抛异常。
@Slf4j
@Service
public class AClassImpl implements IAClass {
@Autowired
private SalEmpRepository salEmpRepository;
@Autowired
private IBClass bClass;
@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
SalEmpEntity entity1 = new SalEmpEntity(17, "name5");
salEmpRepository.save(entity1);
bClass.bFunction();
}
}
@Slf4j
@Service
public class BClassImpl implements IBClass {
@Autowired
private SalEmpRepository salEmpRepository;
@Override
public void bFunction() {
SalEmpEntity entity2 = new SalEmpEntity(18, "name6");
salEmpRepository.save(entity2);
throw new RuntimeException("occur Exception!");
}
}
结果:两个操作都会回滚
情况二:aFunction、bFunction都添加注解,bFunction抛异常。
@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
SalEmpEntity entity1 = new SalEmpEntity(17, "name5");
salEmpRepository.save(entity1);
bClass.bFunction();
}
@Transactional(rollbackFor = Exception.class)
@Override
public void bFunction() {
SalEmpEntity entity2 = new SalEmpEntity(18, "name6");
salEmpRepository.save(entity2);
throw new RuntimeException("occur Exception!");
}
结果:两个操作都会回滚,因为两个方法用的还是同一个事务
情况三:aFunction、bFunction都添加注解,bFunction抛异常,aFunction捕获抛出的异常。
@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
SalEmpEntity entity1 = new SalEmpEntity(17, "name5");
salEmpRepository.save(entity1);
try {
bClass.bFunction();
} catch (Exception e) {
log.info(e.getMessage());
}
}
@Transactional(rollbackFor = Exception.class)
@Override
public void bFunction() {
SalEmpEntity entity2 = new SalEmpEntity(18, "name6");
salEmpRepository.save(entity2);
throw new RuntimeException("occur Exception!");
}
结果:首先结论是都会回滚。但是控制台还打印了报错信息:“org.springframework.orm.jpa.JpaSystemException: Transaction was marked for rollback only; cannot commit; nested exception is org.hibernate.TransactionException: Transaction was marked for rollback only; cannot commit”。(我这里使用的是Spring Data JPA,因此是jpa打印的堆栈信息),我们只用关注标黄文字(事务已经被标记成rollback only,不能提交)。其实这句话很好理解,bFunction里面出现异常,会立马调用事务的rollback函数,事务就被标记成了rollback only状态。然后由于aFunction catch了异常,而且没有抛出将异常抛出,事务捕获不到异常,最后会调用commit函数。由于是同一个事务,但事务状态之前已经被标记成了rollback only,再提交就会异常。
情况四:aFunction、bFunction都添加注解,bFunction抛异常,bFunction @Transactional注解加了参数propagation = Propagation.REQUIRES_NEW,控制事务的传播行为。
@Transactional(rollbackFor = Exception.class)
@Override
public void aFunction() {
SalEmpEntity entity1 = new SalEmpEntity(17, "name5");
salEmpRepository.save(entity1);
try {
bClass.bFunction();
} catch (Exception e) {
log.info(e.getMessage());
}
}
@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
@Override
public void bFunction() {
SalEmpEntity entity2 = new SalEmpEntity(18, "name6");
salEmpRepository.save(entity2);
throw new RuntimeException("occur Exception!");
}
结果:aFunction中由于没有异常抛出,save成功;而bFunction事务会回滚;与情况三的差别是bFunction的注解添加了参数propagation = Propagation.REQUIRES_NEW,表明是新建事务。既然都是不同事务了,也就不会报错,能顺利回滚事务。
初次使用Spring事务一定要理解清楚其中的原理,特别是使用注解方式来控制事务,一定要对事务传播机制和隔离级别有详细的理解,不然在使用中可能会出现一些莫名其妙的问题不好分析。以上例子基本覆盖了所有情况,了解规则后使用就不会出错了。