发布时间:2022-10-27 文章分类:编程知识 投稿人:赵颖 字号: 默认 | | 超大 打印

目标:事务失效引发的灾难

如下图(张三--->李四转账)

从阿里规约看Spring事务

tips

下订单-------订单支付-----减库存(失败)

超卖现象

代码回忆:

//实现类
public class UserServiceImpl implements UserService {
	@Autowired
	private UserMapper userMapper;
	@Resource
	private LogService logService;
	@Override
	@Transactional
//	@Transactional(rollbackFor = Exception.class)
	public void insert() throws Exception {
		method1_Test();
	}
//模拟转账@Transactional
	private  void  method1_Test() throws Exception {
		System.out.println(">>>>>>>>>>>进入到业务方法");
		User user = new User();
		user.setName("张三");
		userMapper.insertUser(user);//张三扣减500元
		addPayment();//模拟李四增加500元(检查异常)
	}
   //FileNotFoundException extends IOException
	private void addPayment() throws FileNotFoundException {
		FileInputStream in = new FileInputStream("a.txt");//模拟检查异常
	}
}
 ......略

如果说你从从事务方法中抛出的是检查异常(io、sql),那么这个时候,Spring将不能进行事务回滚。

是不是很恐怖呢??

所以说,阿里规定
1、让检查异常也回滚:你就需要在整个方法前加上@Transactional(rollbackFor=Exception.class)

2、让非检查异常不回滚:
需要加入@Transactional(notRollbackFor=RunTimeException.class)

3、不需要事务管理(or 日志丢失)
需要加入@Transactional(propagation=Propagation.NOT_SUPPORTED)

课程目标总结

1、解决事务失效:通过源码学习如何让检查异常也回滚(or 运行异常不回滚);从源码角度深入底层原理

2、解决无需事务控制;查询 or 日志记录;通过传播属性如何控制;底层是如何实现的

3、正常的事务执行流程在源码中是如何实现的

1.1 Spring事务总体介绍

在Spring中,事务有两种实现方式:

  1. 编程式事务管理: 编程式事务管理使用TransactionTemplate可实现更细粒度的事务控制。

  2. 申明式事务管理: 基于Spring AOP实现。

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

声明式事务好处:

申明式事务管理不需要入侵代码,通过@Transactional就可以进行事务操作,更快捷而且简单,且大部分业务都可以满足,推荐使用。

管是编程式事务还是申明式事务,最终调用的底层核心代码是一致的

1.1.1 编程式事务实现方式

编程式事务,Spring已经给我们提供好了模板类TransactionTemplate,可以很方便的使用,如下图

从阿里规约看Spring事务
TransactionTemplate全路径名是:org.springframework.transaction.support.TransactionTemplate。这是spring对事务的模板类

实现的接口

用来执行事务的回调方法,

public interface TransactionOperations {
	@Nullable
	<T> T execute(TransactionCallback<T> action) throws TransactionException;
}

InitializingBean这个是典型的spring bean初始化流程中 ,用来在bean属性加载完毕时执行的方法。

public interface InitializingBean {
	void afterPropertiesSet() throws Exception;
}

TransactionTemplate的2个接口的impl方法做了什么?

afterPropertiesSet如下

	//	只是校验了事务管理器不为空
@Override
	public void afterPropertiesSet() {
		if (this.transactionManager == null) {
			throw new IllegalArgumentException("Property 'transactionManager' is required");
		}
	}

execute方法如下

	public <T> T execute(TransactionCallback<T> action) throws TransactionException {
		Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");
		if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) {
			return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
		} 
		else {
			//TODO  创建事务 (与声明事务调用同一个方法)
			TransactionStatus status = this.transactionManager.getTransaction(this);
			T result;
			try {
				// 2.执行业务逻辑,这里就是用户自定义的业务代码。如果是没有返回值的,就是doInTransactionWithoutResult()。
				result = action.doInTransaction(status);
			} catch (RuntimeException | Error ex) {
				// Transactional code threw application exception -> rollback
				// 应用运行时异常/错误异常 -> 回滚,调用AbstractPlatformTransactionManager的rollback(),事务提交回滚
				//TODO  回滚((与声明事务调用同一个方法)
				rollbackOnException(status, ex);
				throw ex;
			} catch (Throwable ex) {
				// 未知异常 -> 回滚,调用AbstractPlatformTransactionManager的rollback(),事务提交回滚
				// Transactional code threw unexpected exception -> rollback
				rollbackOnException(status, ex);
				throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
			}
			// TODO  事务提交 (与声明事务调用同一个方法)
			this.transactionManager.commit(status);
			return result;
		}
	}

总结

事务模板TransactionTemplateI里面的execute方法【创建事务】【提交事务】【回滚事务】和声明式事务调用的都是同一个底层方法

1.1.2 声明式事务实现方式

声明式事务的用法

@Transactional注解可以加在类或方法上

1、类:在类上时是对该类的所有public方法开启事务。

2、方法:加在方法上时也是只对public方法起作用。

注意

@Transactional注解也可以加在接口上,但只有在设置了基于接口的代理时才会生效,因为注解不能继承。所以该注解最好是加在类的实现上。

1.2 不容忽视的异常体系

目标:Java异常体系(面试常问)与Spring事务存在什么联系

从阿里规约看Spring事务

结论:

Spring事务默认只回滚运行时异常和Error

伪代码如下

@Transaction
public  void   insert(){
 m_insert();
 in_insert();//只有运行时异常 &  Error  才可以回滚
}

Thorwable类是所有异常和错误的超类,有两个子类Error和Exception,分别表示错误和异常。

异常类Exception又分为运行时异常(RuntimeException)和非运行时异常,这两种异常有很大的区别,也称之为不检查异常(Unchecked Exception)和检查异常(Checked Exception)。

1、Error与Exception

Error是程序无法处理的错误,比如OutOfMemoryError、ThreadDeath等。这些异常发生时,Java虚拟机(JVM)一般会选择线程终止。

Exception是程序本身可以处理的异常,这种异常分两大类运行时异常和非运行时异常。程序中应当尽可能去处理这些异常。

2、运行时异常和非运行时异常

运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。

这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。

非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

1.3 事务传播行为与隔离级别

1.3.1 事务传播行为

什么是事务传播

事务传播用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的时事务如何传播。

从阿里规约看Spring事务

tips

两大类【有事务的情况】【没事务的情况】

代码理解概念

@Transactional
	private void method5_Test() throws Exception {
		System.out.println(">>>>>>>>>>>进入到业务方法");
		User user = new User();
		user.setName("张三");
		userMapper.insertUser(user);
		logService.insert(getLogEntity());
		throw new RuntimeException();
	}
	.............LogService.java
 	@Override
//	@Transactional(propagation = Propagation.NOT_SUPPORTED)
	@Transactional
	public void insert(Log log) throws Exception {
		System.out.println(">>>>>>>>>>>进入到日志方法");
//		Log log = new Log();
//		log.setName("业务日志记录");
		logMapper.insertLog(log);
	}

1.3.2 事务隔离级别

什么是事务的隔离

5大类

隔离性是指多个用户的并发事务访问同一个数据库时,一个用户的事务不应该被其他用户的事务干扰,多个并发事务之间要相互隔

隔离级别 说明
TransactionDefinition.ISOLATION_DEFAULT(默认) PlatformTransactionManager的默认隔离级别,使用数据库默认的事务隔离级别。另外四个与JDBC的隔离级别相对应。
TransactionDefinition.ISOLATION_READ_UNCOMMITTED(读未提交) 这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。这种隔离级别会产生脏读,不可重复读和幻像读
TransactionDefinition.ISOLATION_READ_COMMITTED(读已提交) 保证一个事务修改的数据提交后才能被另外一个事务读取,另外一个事务不能读取该事务未提交的数据。这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。
TransactionDefinition.ISOLATION_REPEATABLE_READ(可重复读) 这种事务隔离级别可以防止脏读、不可重复读,但是可能出现幻像读。它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了不可重复读
TransactionDefinition. ISOLATION_SERIALIZABLE(串行化) 代价最大、可靠性最高的隔离级别,所有的事务都是按顺序一个接一个地执行

如果不考虑隔离性,会发生什么事呢?

脏读:

脏读是指一个事务在处理数据的过程中,读取到另一个为提交事务的数据

不可重复读:

不可重复读是指对于数据库中的某个数据,一个事务范围内的多次查询却返回了不同的结果,这是由于在查询过程中,数据被另外一个事务修改并提交了。

幻读

幻读是事务非独立执行时发生的一种现象。例如事务T1对一个表中所有的行的某个数据项做了从“1”修改为“2”的操作,这时事务T2又对这个表中插入了一行数据项,而这个数据项的数值还是为“1”并且提交给数据库。而操作事务T1的用户如果再查看刚刚修改的数据,会发现还有一行没有修改,其实这行是从事务T2中添加的,就好像产生幻觉一样,这就是发生了幻读

1.读未提交(Read uncommitted):

这种事务隔离级别下,select语句不加锁。

此时,可能读取到不一致的数据,即“读脏 ”。这是并发最高,一致性最差的隔离级别。

2.读已提交(Read committed):

可避免 脏读 的发生。

在互联网大数据量,高并发量的场景下,几乎 不会使用 上述两种隔离级别。

3.可重复读(Repeatable read):

MySql默认隔离级别。

可避免 脏读不可重复读 的发生。

4.串行化(Serializable ):

可避免 脏读、不可重复读、幻读 的发生

1.4 Spring事务源码深度剖析

1.4.1 Spring事务环境介绍

目标:事务测试环境介绍

从阿里规约看Spring事务

1.4.2 事务是何时被织入的

目标:事务在Spring哪个阶段织入的

思考:

程序在运行的时候【userService】为什么是代理对象

从阿里规约看Spring事务

需要解决的问题:

1、代理对象是如何生成的

2、代理对象如何调用到了invoke方法

1.4.3 事务源码入口在哪里

1)事务三大接口介绍

目标:了解非常核心的事务三大接口

Spring事务三大接口介绍

1、PlatformTransactionManager: (平台)事务管理器接口

PlatformTransactionManager 接口是 Spring 提供的平台事务管理器顶级接口,用于管理事务。

从阿里规约看Spring事务
Spring并不直接管理事务,而是提供了多种事务管理器;他们将事务管理的职责委托给Hibernate或者JTA等持久化机制的事务框架来实现

通过这个接口,Spring为各个平台,比如JDBC等都提供了对应的事务管理器;但是具体实现就是下游事务框架了

public interface PlatformTransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition):用于获取事务状态信息。
 void commit(TransactionStatus status):用于提交事务。
 void rollback(TransactionStatus status):用于回滚事务。
}

该接口中提供了三个事务操作方法,具体如下。

下面是 PlatformTransactionManager各种实现

从阿里规约看Spring事务

2、TransactionDefinition:事务定义信息接口

比如:事务传播行为、隔离级别、超时、只读、回滚规则)

//7大传播+5大隔离+超时、只读、回滚
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;//默认:如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务
.....略
}

3、TransactionStatus:事务运行状态接口

public interface TransactionStatus extends SavepointManager, Flushable {
	boolean isNewTransaction();//获取是否是新事务
	....略
}

TransactionStatus 接口是事务的状态,它描述了某一时间点上事务的状态信息。其中包含六个操作

名称 说明
void flush() 刷新事务
boolean hasSavepoint() 获取是否存在保存点
boolean isCompleted() 获取事务是否完成
boolean isNewTransaction() 获取是否是新事务
boolean isRollbackOnly() 获取是否回滚
void setRollbackOnly() 设置事务回滚

2)事务源码调用入口

目标:找到Spring事务的调用入口

从阿里规约看Spring事务

tips

入口拦截器org.springframework.transaction.interceptor.TransactionInterceptor#invoke

TransactionInterceptor源码如下

	// 获取目标类
	@Override
	@Nullable
	public Object invoke(final MethodInvocation invocation) throws Throwable {
		// Work out the target class: may be {@code null}.
		// The TransactionAttributeSource should be passed the target class
		// as well as the method, which may be from an interface.
		Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
		// 开始调用父类方法
		return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
	}

Transactional 注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
	@AliasFor("transactionManager")
	String value() default "";
	@AliasFor("value")
	String transactionManager() default "";
	//	传播行为
// 一个开启了事务的方法A,调用了另一个开启了事务的方法B,此时会出现什么情况?这就要看传播行为的设置了
	Propagation propagation() default Propagation.REQUIRED;
	//isolation属性是用来设置事务的隔离级别,数据库有四种隔离级别:
	//读未提交、读已提交、可重复读、可串行化。MySQL的默认隔离级别是可重复读
	Isolation isolation() default Isolation.DEFAULT;
	//timtout是用来设置事务的超时时间,可以看到默认为-1,不会超时。
	int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
	//​ readOnly属性用来设置该属性是否是只读事务,只读事务要从两方面来理解:
// 它的功能是设置了只读事务后在整个事务的过程中,其他事务提交的内容对当前事务是不可见的
//	只读事务中只能有读操作,不能含有写操作,否则会报错
	boolean readOnly() default false;
	//当方法内抛出指定的异常时,进行事务回滚。默认情况下只对RuntimeException回滚。
	Class<? extends Throwable>[] rollbackFor() default {};
	String[] rollbackForClassName() default {};
//	 用来设置出现指定的异常时,不进行回滚。
	Class<? extends Throwable>[] noRollbackFor() default {};
	String[] noRollbackForClassName() default {};

1.4.4 Spring事务源码深入剖析

1)事务正常执行流程

目标:

1、正常流程测试(不抛运行时异常)

2、有运行时异常的情况

测试代码

//实现类
public class UserServiceImpl implements UserService {
	@Autowired
	private UserMapper userMapper;
	@Override
	@Transactional
	public void insert() throws Exception {
	method2_Test();//正常情况下执行
    method3_Test() ;//有运行时异常的情况
	}
}

找到Spring事务拦截器入口

org.springframework.transaction.interceptor.TransactionInterceptor#invoke

	// 获取目标类
	@Override
	@Nullable
	public Object invoke(final MethodInvocation invocation) throws Throwable {
		// Work out the target class: may be {@code null}.
		// The TransactionAttributeSource should be passed the target class
		// as well as the method, which may be from an interface.
		Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
		// 开始调用父类方法
		return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
	}

进入到invokeWithinTransaction

tips:具体流程

1、获取事务属性

2、创建事务

3、调用目标方法

4、回滚 事务 or 提交事务

//	包含了事务执行的整个流程,这里是使用了模板模式,具体的实现交给子类去实现
	@Nullable
	protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
											 final InvocationCallback invocation) throws Throwable {
		// 获取事务属性,如果事务属性为空,则没有事务
		TransactionAttributeSource tas = getTransactionAttributeSource();
		//继承TransactionDefinition
		final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
		// 获取实现:DataSourceTransactionManager 管理 JDBC 的 Connection。
		final PlatformTransactionManager tm = determineTransactionManager(txAttr);
		// 切点标识-->com.tx.test.impl.UserServiceImpl.insert
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
		if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
			// TODO: 创建(开启)事务(根据事务的传播行为属性去判断是否创建一个事务)
			TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
			Object retVal = null;
			try {
				// TODO: 调用目标方法
				retVal = invocation.proceedWithInvocation();
			} catch (Throwable ex) {
				// TODO: 回滚事务  目标方法调用发生了异常(后置增强),
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			} finally {
				// 清理信息
				cleanupTransactionInfo(txInfo);
			}
			//TODO: 提交事务
			commitTransactionAfterReturning(txInfo);
			return retVal;
			.............略

从阿里规约看Spring事务

TransactionAttribute继承了TransactionDefinition

提交

	protected void doCommit(DefaultTransactionStatus status) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
		Connection con = txObject.getConnectionHolder().getConnection();
		if (status.isDebug()) {
			logger.debug("Committing JDBC transaction on Connection [" + con + "]");
		}
		try {
			//TODO :jdbc链接提交事务
			con.commit();
		}
		catch (SQLException ex) {
			throw new TransactionSystemException("Could not commit JDBC transaction", ex);
		}
	}

回滚

protected void doRollback(DefaultTransactionStatus status) {
		DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();
		Connection con = txObject.getConnectionHolder().getConnection();
		if (status.isDebug()) {
			logger.debug("Rolling back JDBC transaction on Connection [" + con + "]");
		}
		try {
			//TODO  jdbc回滚事务
			con.rollback();
		}
		catch (SQLException ex) {
			throw new TransactionSystemException("Could not roll back JDBC transaction", ex);
		}
	}

总结

1、获取事务属性

2、创建事务

1、获取事务管理器DataSourceTransactionManager(通过数据源、拿到链接、在设置事务管理器)
2、判断是否存在事务,如果有就去判断传播属性!!!(第一次不存在)
3、不存在事务的情况
   如果事务超时时间小于默认(-1)或者没有事务,则抛出异常
   新建一个事务(doBegin开启事务)
   注意:第一次进入则是新建一个事务

3、调用目标方法

4、回滚 事务 or 提交事务

提交事务
1、调用doCommit提交事务
   通过事务管理器对象DataSourceTransactionObject拿到con执行con.commit()
回滚事务
1、调用doRollback回滚
    通过事务管理器对象DataSourceTransactionObject拿到con执行con.rollback()

2)生产事务失效之谜

目标:

1、事务失效的原因是什么

2、如何解决事务失效

3、在源码中什么地方判断的

测试代码

	@Override
	@Transactional
	public void insert() throws Exception {
	method4_Test();//事务失效
	}

解决方案

@Transactional(rollbackFor = Exception.class)

重点关注源码

org.springframework.transaction.interceptor.TransactionAspectSupport#completeTransactionAfterThrowing中的rollbackOn方法

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
		if (txInfo != null && txInfo.getTransactionStatus() != null) {
			if (logger.isTraceEnabled()) {
				logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +
						"] after exception: " + ex);
			}
			//TODO rollbackOn获取回滚规则;可以自定义设置回滚规则,默认会判断RuntimeException和Error,!!!!!!!
			if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
				try {
					// TODO 回滚事务
					txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
				} catch (TransactionSystemException ex2) {
  ................略

3)线上日志数据丢失

目标:通过传播属性解决业务中日志丢失问题

从阿里规约看Spring事务
测试代码入口

	public static void main(String[] args) {
		ApplicationContext context = new ClassPathXmlApplicationContext("classpath*:application.xml");
		UserService userService = (UserService) context.getBean("userService");
//		LogService logService = (LogService) context.getBean("logService");
		try {
			userService.insert();//用户
//			logService.insert();//日志
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

日志插入插入失败

使用默认的传播属性和隔离级别

//实现类
public class LogServiceImpl implements LogService {
	@Autowired
	private LogMapper logMapper;
	@Override
//	@Transactional(propagation = Propagation.NOT_SUPPORTED)
	@Transactional
	public void insert(Log log) throws Exception {
		System.out.println(">>>>>>>>>>>进入到日志方法");
//		Log log = new Log();
//		log.setName("业务日志记录");
		logMapper.insertLog(log);
	}

tips

@Transactional(propagation = Propagation.NOT_SUPPORTED)

这样修改则修改成功

用户信息插入,调用日志插入(

	@Override
	@Transactional
//	@Transactional(rollbackFor = Exception.class)
	public void insert() throws Exception {
	method5_Test();
	}

总结

1、现象

​ 用户插入的时候;调用log的service插入;如果出现异常;两者全部回滚

​ 也就是用户插入失败、日志插入失败

2、需求

​ 正常的业务情况;都是在事务失败的时候;同时会要求日志也要插入成功

3、过程

​ 目前;用户和日志的service使用的都是默认的传播属性和隔离级别

4、改进

​ 将日志的传播属性修改成Propagation.NOT_SUPPORTED【如果当前存在事务;就挂起当前事务】

本文由传智教育博学谷教研团队发布。

如果本文对您有帮助,欢迎关注点赞;如果您有任何建议也可留言评论私信,您的支持是我坚持创作的动力。

转载请注明出处!