事务是逻辑处理原⼦性的保证⼿段,通过使⽤事务控制,可以极⼤的避免出现逻辑处理失败导致的脏数 据等问题。
事务最重要的两个特性,是事务的传播级别和数据隔离级别。传播级别定义的是事务的控制范围,事务 隔离级别定义的是事务在数据库读写⽅⾯的控制范围。
PROPAGATION_REQUIRED
Spring
默认的事务传播级别,使⽤该级别的特点是,如果上下⽂中 已经存在事务,那么就加⼊到事务中执⾏,如果当前上下⽂中不存在事务,则新建事务执⾏。所以这个 级别通常能满⾜处理⼤多数的业务场景。
PROPAGATION_SUPPORTS
从字⾯意思就知道,supports
,⽀持,该传播级别的特点是,如果上下⽂存在事务,则⽀持事务加⼊事务,如果没有事务,则使⽤⾮事务的⽅式执⾏。所以说,并⾮所有的包在transactionTemplate.execute
中的代码都会有事务⽀持。这个通常是⽤来处理那些并⾮原⼦性的⾮核⼼业务逻辑操作。应⽤场景较少。
PROPAGATION_MANDATORY
该级别的事务要求上下⽂中必须要存在事务,否则就会抛出异常!配置该⽅式的传播级别是有效的控制上下⽂调⽤代码遗漏添加事务控制的保证⼿段。⽐如⼀段代码不能单独被调⽤执⾏,但是⼀旦被调⽤,就必须有事务包含的情况,就可以使⽤这个传播级别。
PROPAGATION_REQUIRES_NEW
从字⾯即可知道,new,每次都要⼀个新事务,该传播级别的特点是,每次都会新建⼀个事务,并且同时将上下⽂中的事务挂起,执⾏当前新建事务完成以后,上下⽂事务恢复再执⾏。
这是⼀个很有⽤的传播级别,举⼀个应⽤场景:现在有⼀个发送100个红包的操作,在发送之前,要做 ⼀些系统的初始化、验证、数据记录操作,然后发送100封红包,然后再记录发送⽇志,发送⽇志要求 100%的准确,如果⽇志不准确,那么整个⽗事务逻辑需要回滚。
怎么处理整个业务需求呢?就是通过这个PROPAGATION_REQUIRES_NEW
级别的事务传播控制就可以 完成。发送红包的⼦事务不会直接影响到⽗事务的提交和回滚。
PROPAGATION_NOT_SUPPORTED
这个也可以从字⾯得知,not supported
,不⽀持,当前级别的特点就是上下⽂中存在事务,则挂起事务,执⾏当前逻辑,结束后恢复上下⽂的事务。
这个级别有什么好处?
可以帮助你将事务极可能的缩⼩。我们知道⼀个事务越⼤,它存在的⻛险也就越多。所以在处理事务的过程中,要保证尽可能的缩⼩范围。⽐如⼀段代码,是每次逻辑操作都必须调⽤的,⽐如循环1000次的某个⾮核⼼业务逻辑操作。这样的代码如果包在事务中,势必造成事务太⼤,导致出现⼀些难以考虑周全的异常情况。所以这个事务这个级别的传播级别就派上⽤场了。
PROPAGATION_NEVER
该事务更严格,上⾯⼀个事务传播级别只是不⽀持⽽已,有事务就挂 起,⽽PROPAGATION_NEVER
传播级别要求上下⽂中不能存在事务,⼀旦有事务,就抛出runtime异 常,强制停⽌执⾏!这个级别上辈⼦跟事务有仇。
PROPAGATION_NESTED
字⾯也可知道,nested,嵌套级别事务。该传播级别特征是,如果上 下⽂中存在事务,则嵌套事务执⾏,如果不存在事务,则新建事务
Serializable
最严格的级别,事务串⾏执⾏,资源消耗最⼤;
REPEATABLE READ
保证了⼀个事务不会修改已经由另⼀个事务读取但未提交(回滚)的数据。 避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。
READ COMMITTED
⼤多数主流数据库的默认事务等级,保证了⼀个事务不会读到另⼀个并⾏事务 已修改但未提交的数据,避免了“脏读取”。该级别适⽤于⼤多数系统。
Read Uncommitted
保证了读取过程中不会读取到⾮法数据
我们来举个事务传播的例子:
假设有一个方法childTest()
,它是一个事务方法,使用了PROPAGATION_REQUIRED
事务传播选项。如果在一个事务中调用这个方法,那么它会在同一个事务中执行。
现在假设有一个方法mainTest()
,它调用了childTest()
方法。如果在mainTest()
方法中没有显式地声明事务,而childTest()
方法中声明了事务,那么childTest()
方法将在一个新的事务中执行,这个新的事务不会影响mainTest()
方法的执行。
但是,如果mainTest()
方法也使用了事务传播选项,例如PROPAGATION_REQUIRED
,那么在执行childTest()
方法时,它的事务会隐式地与mainTest()
方法的事务关联。这意味着如果childTest()
方法的事务成功,则mainTest()
方法的事务也会成功;如果childTest()
方法的事务失败,则mainTest()
方法的事务也会失败。
在使用Spring事务时,我们需要注意内部方法和外部方法是否在同一个事务中。
如果内部方法和外部方法在同一个事务中,那么当内部方法发生异常时,外部方法也会受到影响,此时需要将异常统一在外层处理。
如果内部方法和外部方法不在同一个事务中,那么内部方法的异常不会影响到外部方法,但是外部方法的异常可能会影响到内部方法。
然而,Nested有一个特殊情况,即当内部方法使用Nested传播机制时,内部方法和外部方法不在同一个事务中,但是内部方法的异常仍然会影响到外部方法。
为了更好地理解Spring事务的传播机制,我们可以通过一个简单的电商项目来演示上述三种情况。
在该电商项目中,我们有两个Service,一个是OrderService,一个是GoodsService。OrderService负责生成订单,而GoodsService负责扣减库存。两个Service中都有一个reduceStock方法,用来扣减库存。在扣减库存的同时,我们还需要判断库存是否充足。如果库存不足,我们需要抛出一个RuntimeException。
首先,我们来看一下内部方法和外部方法在同一个事务中的情况。在这种情况下,我们可以使用required传播机制。具体实现如下:
在这个例子中,如果库存不足,会抛出一个RuntimeException,整个事务会回滚。如果扣减库存失败,同样会抛出一个RuntimeException,整个事务也会回滚。如果订单生成成功,整个事务会被提交。
接下来,我们来看一下内部方法和外部方法不在同一个事务中的情况。在这种情况下,我们可以使用requires_new传播机制。具体实现如下:
在这个例子中,我们将reduceStock方法的事务传播机制设置为REQUIRES_NEW。在执行该方法时,Spring会将当前事务挂起,创建一个新的事务来执行reduceStock方法。如果reduceStock方法执行成功,则会提交新的事务。如果reduceStock方法执行失败,则会回滚新的事务,但不会影响当前事务。
在这个例子中,我们将需要扣减的库存数量加了1。这样在扣减库存时,就会发现库存不足。此时,reduceStock方法会抛出一个RuntimeException,新的事务会回滚,但当前事务不会受到影响。因此,订单生成成功,但库存并没有被扣减。
最后,我们来看一下嵌套事务的情况。在这种情况下,我们可以使用nested传播机制。具体实现如下:
在这个例子中,我们将reduceStock方法的事务传播机制设置为NESTED。在执行该方法时,Spring会创建一个嵌套事务来执行reduceStock方法。如果reduceStock方法执行成功,则嵌套事务会提交。如果reduceStock方法执行失败,则嵌套事务会回滚,但不会影响外层事务。
在这个例子中,我们在createOrder方法中调用了reduceStock方法,并将需要扣减的库存数量加了2。这样在扣减库存时,就会发现库存不足。此时,reduceStock方法会抛出一个RuntimeException,嵌套事务会回滚,但当前事务不会受到影响。因此,订单生成失败,库存也没有被扣减。
综上所述,Spring事务传播机制提供了灵活的事务管理方式,可以根据不同的业务场景选择不同的传播机制来控制事务的行为。其中,REQUIRES_NEW和NESTED是比较特殊的传播机制,可以在需要的时候使用。
在使用Spring事务时,需要注意以下几点:
事务传播机制的选择要根据业务场景来确定。
如果内层方法和外层方法在同一个事务中,那么内层方法抛出异常时,异常应该由外层方法来处理。
如果内层方法和外层方法在不同的事务中,那么内层方法抛出异常时,不会影响到外层方法。
NESTED传播机制是一种比较特殊的传播机制,需要慎重使用。
在实际开发中,我们需要根据不同的业务场景选择合适的事务传播机制,并且要根据实际情况来处理事务异常,以保证事务的正确执行。