本地事务的坑


本地事务

事务的定义

所谓事务,它是一个操作集合,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。

事务有4个重要特性:ACID

原子性(Atomicity) 当事务结束,它对所有资源状态的改变都被视为一个操作,这些操作要不同时成功,要不同时失败
一致性(Consistency) 操作完成后,所有数据必须符合业务规则,否则事务必须中止
隔离性(Isolation) 事务以相互隔离的方式执行,事务以外的实体无法知道事务过程中的中间状态
持久性(Durable) 事务提交后,数据必须以一种持久性方式存储起来

事务产生的问题:

场景:同一个事务内(同一个服务内)

名称 数据的状态 实际行为 产生原因
脏读 未提交 打算提交但是数据回滚了,读取了提交的数据 数据的读取
不可重复读 已提交 读取了修改前的数据 数据的修改
幻读 已提交 读取了插入前的数据 数据的插入

事务隔离级:

名称 结果 脏读 不可重复读 幻读
Read UnCommitted(读未提交) 什么都不解决
Read Committed(读提交) 解决了脏读的问题
Repeatable Read(重复读) mysql的默认级别,解决了不可重复读
Serializable(序列化) 解决所有问题

READ UNCOMMITTED(读未提交数据):允许事务读取未被其他事务提交的变更数据,会出现脏读、不可重复读和幻读问题。
READ COMMITTED(读已提交数据):只允许事务读取已经被其他事务提交的变更数据,可避免脏读,仍会出现不可重复读和幻读问题。
REPEATABLE READ(可重复读):确保事务可以多次从一个字段中读取相同的值,在此事务持续期间,禁止其他事务对此字段的更新,可以避免脏读和不可重复读,仍会出现幻读问题。
SERIALIZABLE(序列化):确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,可避免所有并发问题,但性能非常低。

Spring中事务的使用

Spring 事务其实就是对数据库事务的支持。Spring 事务的隔离级别和数据库中的隔离级别是相对应的。Spring事务分为声明式事务和编程式事务(基本很少人使用)。

Spring事务5种隔离级别:

隔离级别 含义
isolation_default 使用数据库默认的事务隔离级别
isolation_read_uncommitted 允许读取尚未提交的修改,可能导致脏读、幻读和不可重复读
isolation_read_committed 允许从已经提交的事务读取,可防止脏读、但幻读,不可重复读仍然有可能发生
isolation_repeatable_read 对相同字段的多次读取的结果是一致的,除非数据被当前事务自生修改。可防止脏读和不可重复读,但幻读仍有可能发生
isolation_serializable 完全服从acid隔离原则,确保不发生脏读、不可重复读、和幻读,但执行效率最低。

Spring事务7种传播行为

传播行为 含义
propagation_required(xml文件中为required) 表示当前方法必须在一个具有事务的上下文中运行,如有客户端有事务在进行,那么被调用端将在该事务中运行,否则的话重新开启一个事务。(如果被调用端发生异常,那么调用端和被调用端事务都将回滚)
propagation_supports(supports) 表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行
propagation_mandatory(mandatory) 表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常
propagation_nested(nested) 表示如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同propagation_required的一样
propagation_never(never) 表示当方法务不应该在一个事务中运行,如果存在一个事务,则抛出异常
propagation_requires_new(requires_new) 表示当前方法必须运行在它自己的事务中。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。
propagation_not_supported(not_supported) 表示该方法不应该在一个事务中运行。如果有一个事务正在运行,他将在运行期被挂起,直到这个事务提交或者回滚才恢复执行

使用Spring中的事务非常简单只需要两步:

  1. 添加数据源配置,在启动类上添加@EnableTransactionManagement注解
  2. 在业务方法上添加@Transactional即可

@Transactional属性

属性 类型 描述
value String 可选的限定描述符,指定使用的事务管理器
propagation enum: Propagation 可选的事务传播行为设置
isolation enum: Isolation 可选的事务隔离级别设置
readOnly boolean 读写或只读事务,默认读写
timeout int (in seconds granularity) 事务超时时间设置
rollbackFor Class对象数组,必须继承自Throwable 导致事务回滚的异常类数组
rollbackForClassName 类名数组,必须继承自Throwable 导致事务回滚的异常类名字数组
noRollbackFor Class对象数组,必须继承自Throwable 不会导致事务回滚的异常类数组
noRollbackForClassName 类名数组,必须继承自Throwable 不会导致事务回滚的异常类名字数组

用法:

  1. 在需要事务管理的地方加@Transactional 注解。@Transactional 注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上。

  2. @Transactional 注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错, 但是这个被注解的方法将不会展示已配置的事务设置。

  3. 注意仅仅 @Transactional 注解的出现不足于开启事务行为,它仅仅 是一种元数据。必须在配置文件中使用配置元素,才真正开启了事务行为。

  4. 通过 元素的 “proxy-target-class” 属性值来控制是基于接口的还是基于类的代理被创建。如果 “proxy-target-class” 属值被设置为 “true”,那么基于类的代理将起作用(这时需要CGLIB库cglib.jar在CLASSPATH中)。如果 “proxy-target-class” 属值被设置为 “false” 或者这个属性被省略,那么标准的JDK基于接口的代理将起作用。

  5. Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。

  6. @Transactional 的事务开启 ,是基于接口的 或者是基于类的代理被创建。所以在同一个类中一个方法调用另一个方法有事务的方法,事务是不会起作用的。

详细使用可以参考:Spring事务隔离级别、传播机制以及简单配置

Spring事务失效问题

Spring事务失效的场景有:

  • @Transactional 应用在非 public 修饰的方法上
  • @Transactional 注解属性 propagation 设置错误
  • @Transactional 注解属性 rollbackFor 设置错误
  • 同一个类中方法调用,导致@Transactional失效
  • 异常被catch捕获导致@Transactional失效

举个例子:

class AService(){

	@Transactional(isolation = Isolation.READ_COMMITTED)
	public void a(){
		b();
		c();
	}

	@Transactional(isolation = Isolation.READ_UNCOMMITTED)
	public void b(){
		//....
	}

	@Transactional(isolation = Isolation.REPEATABLE_READ)
	public void c(){
		//....
	}
	
}

在以上这种情景中a调用了同一个类里面的b和c方法,那么b和c方法的事务会失效。a的事务设置会覆盖b和c的

class AService(){

		@Transactional
		void a(){
			b();
			c();
            int i=10/0;
		}

		@Transactional(propagation = Propagation.REQUIRED)
		void b(){
			//....
		}

		@Transactional(propagation = Propagation.REQUIRES_NEW)
		void c(){
			//....
		}

	}

以上情景中a和b将共用一个事务,c为一个新的事务。因此会造成a,b回滚事务,c不回滚

class AService(){
		@Transactional
		void a(){
			bService.b();
			cService.c();
            int i=10/0;
		}
	}

这种情况下a,b,c事务均生效。原因在于Spring的事务是通过代理对象来实现的,同一个代理对象内调用方法会覆盖。

事务失效解决方法

同一个对象内事务方法互相调用默认失效的,原因在于绕过了代理对象。

因此解决方法是,使用代理对象来调用事务方法,具体实施如下:

  1. 引入aop依赖,aop中包含了aspectj

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
  2. 开启动态代理,在启动类添加@EnableAspectJAutoProxy,对外暴露代理对象

  3. 使用代理对象在本类互调

    class AService{
    
    	@Transactional
    	void a(){
    		AService aProxyService =(AService) AopContext.currentProxy();
    		aProxyService.b();
    		aProxyService.c();
    	}
    
    	@Transactional(propagation = Propagation.REQUIRED)
    	void b(){
    		//....
    	}
    
    	@Transactional(propagation = Propagation.REQUIRES_NEW)
    	void c(){
    		//....
    	}
    
    }

长事务问题

长事务产生原因?

@Transactional 注解,是使用 AOP 实现的,本质就是在目标方法执行前后进行拦截。在目标方法执行前加入或创建一个事务,在执行方法执行后,根据实际情况选择提交或是回滚事务。

当 Spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,对于@Transactional注解包裹的整个方法都是使用同一个connection连接。如果我们出现了耗时的操作,比如第三方接口调用,业务逻辑复杂,大批量数据处理等就会导致我们我们占用这个connection的时间会很长,数据库连接一直被占用不释放。一旦类似操作过多,就会导致数据库连接池耗尽。

在一个事务中执行RPC操作导致数据库连接池撑爆属于是典型的长事务问题,类似的操作还有在事务中进行大量数据查询,业务规则处理等…

何为长事务?

顾名思义就是运行时间比较长,长时间未提交的事务,也可以称之为大事务

长事务会引发哪些问题?

长事务引发的常见危害有:

  1. 数据库连接池被占满,应用无法获取连接资源;
  2. 容易引发数据库死锁;
  3. 数据库回滚时间长;
  4. 在主从架构中会导致主从延时变大。

如何避免长事务?

解决长事务的宗旨就是 对事务方法进行拆分,尽量让事务变小,变快,减小事务的颗粒度。

避免长事务最简单的方法就是不要使用声明式事务@Transactional,而是使用编程式事务手动控制事务范围。


文章作者: wmg
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 wmg !
  目录