小言_互联网的博客

深入Spring数据库事务管理(二)

430人阅读  评论(0)

五、数据库的相关知识

(一)ACID特性

ACID就是:原子性(Atomicity )、一致性( Consistency )、隔离性( Isolation)和持久性(Durabilily)。

原子性:一个事物必须被视为一个不可分割的最小工作单元,整个事物中的操作要么全部提交成功,要么全部失败回滚

一致性:事务对数据库的操作,总是使数据库从一个一致性状态变为另一个一致性状态

隔离性:事务之间的操作是相互独立的,互不干扰

持久性:事务一旦提交,那么它对数据库的修改是永久性的

(二)丢失更新

在并发的事务中,会存在丢失更新的问题,一般而言存在两类丢失更新
首先明确一个问题:事务回滚即事务回滚到初始时读取的数据,A查询余额1000元,A执行消费100元,之后取消了消费操作,那么回滚时的值就是1000元

第一类丢失更新

上面操作执行完成后,账户余额1000元,但这不符合事实.问题在于俩个事务一个提交,一个回滚,导致数据不一致,我们称这为第一类丢失更新
备注:MySQL、Oracle等基本上都已经消灭了这类丢失更新

第二类丢失更新

并发执行的多个事务操作了同一个数据,两者都提交成功,但是由于在不同的事务中,无法探知其他事务的操作,导致二者提交后,余额显示为900元,正确的值应该是800元,这就是第二来丢失更新
为了解决克服事务之间协助的一致性,数据库规范中定义了事务间的隔离级别,来在不同程度上减少出现的丢失更新的可能性

(三)隔离级别

隔离级别可以在不同的程度上减少丢失更新,按照SQL的标准规范把隔离级别定义为4层,分别是脏读(dirty read)、读写提交(read commit)、可重复读(repeatable read)、序列化(Serializable)

脏读

脏读是最低的隔离级别,其含义是允许一个事务读取另一个事务中未提交的数据
这个很好理解,A事务,B事务,B事务+100,A就读到这数据,但是B可能发生回滚操作,那么此时A读的就是错误数据

读写提交

为了克服脏读,SQL标注提出了第二个隔离级别——读写提交
一个事务只能读取另一个事务已经提交的数据

前面已经提到,第一类事务丢失更新因为另一个事务回滚,现在已经克服了第一类事务丢失更新,所以此时B回滚后的余额为900元

不可重复读


对于A来说,事务开始时余额1000元,但是在之后发现余额不足,对于它而言,不知道其他事务做了操作,总结来说对于A来说账户余额是不能重复读取的,而是一个会变化的值,这样的场景称为不可重复读(unrepeatable read),这是读写提交时存在的问题
为了克服不可重复读带来的错误,SQL标准提出了可重复的隔离级别来解决问题.
注意,可重复读这个概念是针对数据库同一条记录而言的,换句话说,可重复读会使得同一条数据库记录的读写按照一个序列话进行操作,不会产生交叉情况,这样就能够够保证同一条的数据的一致性,进而保证上述场景的正确性.

幻读


B在T1时刻查询到10条记录,但在T4时刻发现打印了11条,这是因为A在T3时刻添加一条记录,导致多一条记录(可重复读是针对同一条记录而言的,而这里不是同一条记录),这样的场景称为幻读

为了克服幻读,SQL标准提出序列化的隔离级别,它是一种让SQL按照顺序读写的方式,能够消除数据库事务间的并发产生的数据不一致的问题.


总结:
1:脏读:A读到B未提交数据,需要处理
2:不可重复读:对同一条记录而言,A读到的数据不同(修改数据)
3:幻读:某个事务查询范围时得到的数据不同(添加、删除数据)

六、选择隔离级别和传播行为
(一)选择隔离级别

从脏读到序列化性能直线下降,因此我们需要设置适合的隔离级别
在高并发条件下,为了保证性能可以使用读写提交
在大部分情况下,企业会选择读写提交
@Transactional(isolation = Isolation.READ_COMMITTED)
如果业务场景是并发较小,而且不看重性能的话,可以使用序列化保证数据一致性

注解Transactional默认的隔离级别是
@Transactional(isolation = Isolation.DEFAULT)
其含义是根据数据库的默认值而变化

MySQL:支持四种隔离级别,默认是可重复读
Oracle:支持两种隔离级别,默认是读写提交

(二)传播行为

传播行为:值方法之间的调用事务策略的问题.在大部分情况下,我们希望事务能够同时成功或者同时失败.但是又存在例外,对于某些批处理事件,当前需要处理一批的财务数据,当其中一条数据发生失败,那么会导致其他所有数据都失败回滚,但是这是不合理的,因为有的成功的数据也会显示交费失败,好的处理方式是每个事件都分配一个事务来处理,当发生异常是只会回滚自己的事务,而不会影响主事务和其他事务。
一个方法调度另外一个方法时,可以对事务的特性进行传播配置,我们称为传播行为

在Spring中传播行为是通过枚举类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),

	
	NESTED(TransactionDefinition.PROPAGATION_NESTED);


	private final int value;


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

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

}


REQUIRED
当方法调用时,如果不存在当前事务,那么就创建事务,如果之前的方法已经存在事务了,那么就沿用之前的事务,Spring的默认传播行为

SUPPORTS
当方法调用时,如果不存在当前事务,那么不启用事务;如果存在当前事务,那么就沿用当前事务

MANDATORY
方法必须在事务内运行,如果不存在当前事务,那么就抛出异常

REQUIRES_NEW
无论是否存在当前事务,方法都会在新的事务中运行,打开新的方法就创建新的事务

NOT_SUPPORTED
不支持事务,如果不存在当前事务也不会创建事务,如果存在当前事务,则挂起它,直至该方法结束后才恢复当前事务,适用于那些不需要事务的SQL

NEVER
不支持事务,只有在没有事务的环境中才能运行它,如果方法存在当前事务,则抛出异常

NESTED
嵌套事务,也就是调用方法如果抛出异常只回滚自己内部执行的SQL,而不回滚主方法的SQL,它的实现存在两种情况,如果当前数据库支持保存点(savepoint),那么它就会在当前事务上使用保存点技术;如果发生异常则将方法内执行的SQL回滚到保存点上,而不是全部回滚,否则就等同于REQUIRES_NEW创建新的事务运行方法代码

七、@Transactional失效问题深入分析

1:@Transactional底层是AOP的动态代理实现的,这意味着对于静态(static)方法和非public方法,注解@Transactional是失效的

protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
}

此方法会检查目标方法的修饰符是否为 public,不是 public则不会获取@Transactional 的属性配置信息。

注意:protected、private 修饰的方法上使用 @Transactional 注解,虽然事务无效,但不会有任何报错,这是我们很容犯错的一点。

2:自调用失效
自调用就是一个类的一个方法去调用自身另外一个方法的过程,在源业务代码中是自己调用自己的过程,但是不存在代理对象的调用,这样就不会产生AOP去为我们设置@Transactional配置的参数,这样就出现自调用注解失效的问题
解决办法:
方法一:可以创建多个实现类,非自调用
方法二:在自调用的过程中,调用方法通过IOC容器去获取对象,然后通过这个获取的代理对象去执行刚才的方法

   public void A() {
        B();
    }

    public void B() {

    }

改进版:

@Service
@Transactional
public class UserServiceImpl {
	
    @Autowired
    ApplicationContext applicationContext;
  public void A() {
        UserServiceImpl userService = (UserServiceImpl) applicationContext.getBean(UserServiceImpl.class);
        userService.B();
    }
	  public void B() {
 }}

3:错误捕捉异常,即在事务中某段代码里面你通过try…catch捕获了异常,但是未异常,这就导致了Spring无法识别到该异常,从而让程序正常执行,事务正常执行,但是此时由于真实发生了异常但未回滚,造成数据不一致

4:@Transactional 注解属性 propagation 设置错误
这种失效是由于配置错误,若是错误的配置以下三种 propagation,事务将不会发生回滚。

TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。

5:@Transactional 注解属性 rollbackFor 或者noRollbackFor设置错误
默认情况下Spring会捕获所有的异常,当发生异常的时候都会进行回滚,但是如果你根据某个业务场景设置了rollbackFor 或者noRollbackFor,那么如果设置不当某些异常允许正常执行事务或者回滚规则不一致,都会导致事务失效,不能保证数据完整性

6:数据库引擎不支持事务,比如myisam

八、事务典型错误用法分析
(一)错误使用Service
public class UserController {
	@Autowire
	private Uservice uservice;
	public void hello(){
          // ....
		uservice.add(user1);
		uservice.add(user2);
}}

上述代码存在的问题是在controller层中俩个Service层的方法根本不在一个事务中
当一个Controller使用service方法时,如果这个service标注@Transactional,那么它就会启用一个事务,而一个Service方法完成后,它就会释放该事务,所以前后俩个方法是在不同的事务中完成的
这个例子告诉我们使用带有事务的Service,当调用时,如果不是调用Service方法,Spring会为你创建对应的数据库事务.如果多次调用,则不在一个事务中,这会造成不同时提交和回滚不一致的问题,这个问题需要特别注意,在日常的开发中肯定会存在类似的问题

(二)过长时间占用事务

@Service
public class UserServiceImpl {

    @Autowired
    private UserDaoImpl userDao;
    @Transactional
    public T addXXX(User user) {
           userDao.add(user);
	//数据库交互之后,需要做一些文件或者其他的处理
		return T;	  
    }

}

当addXXX整个方法结束后才会去释放事务资源,也就是说中间其他的和事务关系不大的其他操作是可以调整到其他地方的,如果在这里则会一直让事务资源挂着,这样的情况看似关系不大,但是要在并发访问比较大的情况下,事务连接数量有限,则使得系统出现卡顿的情况,所以对于类似的情况需要将其他部分代码脱离出来,避免长时间占用事务

(三)错误捕捉异常

1:try…catch的使用会捕捉到你的异常,但是会使得Spring无法捕捉到你的异常,在
TransactionTemplate类的execute方法中可以看到其中Spring已经使用了ry…catch去捕捉所有的异常

2:如果要使用try…catch,切记一定自己要在catch中抛出自己的异常,如果你有特别的需要让某些异常可以继续保持事务的话,建议自定义事务管理器,这里还是抛出异常
throw new RuntimeException(ex);

3:使用事务时要时刻记住Spring和我们的约定流程


转载:https://blog.csdn.net/Octopus21/article/details/106164097
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场