关于Spring的两三事:此“事务”非彼“事务”

人生苦短,不如养狗

作者:闲宇

公众号:Brucebat的伪技术鱼塘

一、前言

  在我们日常学习和使用Spring框架的时候一定绕不开这样一个概念:Spring事务。其实,这里更准确的说法应该是Spring事务管理。为什么这里闲宇要特地强调“事务管理”而非单纯的“事务”呢?那是因为Spring框架本身并没有直接实现事务处理的能力,而是提供了一套基于Java标准的事务管理能力,实际的事务处理仍然需要开发者自身利用Spring提供的这一能力来去实现。也就是说,此“事务”并非彼“事务”。

  为了更好地理解这两者的区别,下面让我们具体地去了解一下数据库系统中的事务和Spring中的事务管理到底是什么。

二、数据库系统中的事务

  事务的概念最早诞生于数据库领域,特别是在关系型数据库发展的过程中。这一概念旨在解决数据在多步骤操作中的一致性、完整性和可靠性问题。简单来说,事务这一概念是用于数据操作过程的,脱离了数据操作,事务也就失去了意义。

1. 真实的事务

  事务(Transaction)是数据库系统中的一个重要概念,指的是一组操作或指令,它们作为一个整体被执行,要么全部执行成功,要么全部失败并回滚。在事务中,多个操作组合成一个原子性单元,确保数据的完整性和一致性。简单来说,事务就是无法被分割一组操作或者指令。

  而要想实现上面概念中要求的内容,事务就必须具备以下特性,也就是我们经常听到的ACID特性:

  • 原子性(Atomicity):原子性意味着事务中的所有操作要么全部成功,要么全部失败。如果事务在执行过程中发生了错误或中途被中断,那么所有已经执行的操作必须回滚,回到事务开始之前的状态。事务不允许部分完成,这确保了数据的完整性。
  • 一致性(Consistency):一致性保证事务执行前后,数据库保持一致的状态。事务执行的过程中,数据库可以短暂处于不一致状态,但当事务完成时,数据库必须满足所有的完整性约束和业务规则。
  • 隔离性(Isolation):隔离性确保多个事务并发执行时,它们的执行结果不会相互干扰。一个事务的操作对其他事务不可见,直到该事务提交。
  • 持久性(Durability):持久性确保一旦事务提交,它的所有变更都会永久保存在数据库中,即使系统发生崩溃也不会丢失。

  这里闲宇需要特别提醒一下大家,有不少朋友在学习和使用事务的时候会错误地认为只有并发环境才需要事务,而在脱离了并发环境的情况下事务就变得可有可无了,这样的认知其实是错误的。从上面介绍的事务的核心特点中我们可以发现,只有隔离性谈到了并发环境的处理,其他三点并没有特别点出并发环境的这一条件。也就是说,在脱离了并发环境的情况下,对于正常的业务处理逻辑来说,其余三点依然是非常重要的。

  其实这样的认知并不是难以理解,这里我们通过几个简单的场景来看一下为什么非并发环境依然需要事务。

  • 批量操作:在非并发环境下执行一组批量操作,如批量更新或批量插入。如果某个操作失败,事务可以确保批量操作作为一个整体,要么完全成功,要么完全失败。
  • 下单购买逻辑:某些业务逻辑需要在一个事务中执行多个数据库操作,例如订单创建、库存更新、付款处理等操作。如果没有事务,任何一步失败都会导致业务逻辑执行失败,而不会自动回滚。
  • 系统错误处理:即使在单线程或非并发的应用程序中,代码执行过程中也可能出现异常或逻辑错误。事务可以保证即使发生了错误,数据库仍然保持一致的状态,不会有半途更改的操作影响数据的完整性。

2. 事务处理

  事务处理实际上就是实现上述事务概念中提到的内容的操作或者机制,在我们使用的绝大部分数据库系统都实现了事务处理的能力。这里我们通过MySQL来看下数据库是如何提供事务处理能力的。

  对于MySQL来说,事务处理由两种模式,分别是隐式事务控制显式事务控制

  • 隐式事务控制:默认情况下,每个 SQL 语句都是在自己的事务中执行的。这意味着,默认情况下,每次执行一个 SQL 语句时,MySQL 会隐式地开启一个新的事务,执行完该语句后立即提交事务。这种模式下,每个语句都被视为一个独立的事务。
  • 显式事务控制:显式事务控制允许开发者显式地管理事务的开始、提交和回滚。这种方式提供了更多的灵活性和控制,适用于需要将多个 SQL 语句作为单个事务来执行的场景。

  需要注意的是,在MySQL当中是否具备事务控制能力还与存储引擎有关,如果我们选择的MyISAM(不支持事务)则上述两种事务控制能力都不可用,每条SQL语句在执行后会,数据更改会立即生效,无法通过事务机制进行撤销或者回滚。而如果我们使用的是InnoDB则两种事务控制能力均可使用。

三、Spring中的事务管理

  在看完了上面关于数据库系统中事务的介绍之后,相信大家已经能够发现两者的一些不同之处。事务本身就是应用于数据操作过程的,对于生而就是为了进行数据操作的数据库而言,事务处理可以说是一项非常基本的功能,这也是为什么绝大部分数据库都会提供事务这一能力。而对于Spring这种开发框架来说,数据操作过程完全依托于实际业务逻辑,同时这些数据究竟如何存储,存储到那种或者哪几种数据库当中这些都是不可预知的,这也就导致了让Spring去实现类似数据库当中事务处理成为了一种非常不现实的事情。

1. 基于业务逻辑的事务管理

  由于实际的数据操作过程依赖业务逻辑,而业务逻辑由完全由开发者自身掌控,所以Spring选择将事务管理的权利交托给开发者自行掌控。也就是说Spring提供了一套显式创建事务、提交事务以及回滚事务的能力,即我们经常碰到的声明式事务编程式事务

  • 声明式事务:这是一种基于AOP实现的事务管理方式,通过注解或者XML配置文件来声明事务的边界(当然,在SpringBoot诞生之后我们更多的会使用@Transactional注解方式)。通过这种方式我们只需要简单讲注解设置在方法或者类维度就可以完成对于指定方法的事务管理;
  • 编程式事务:这种方式是通过代码显示地管理事务,通过这种方式开发者可以进行更细粒度的事务控制,但是也带来了更高的代码冗余度和复杂度。

  需要注意的是,Spring虽然提供了事务管理能力,但其本身并没有事务处理能力,实际的事务处理仍需依赖数据库本身的事务处理能力。也就是说,看似你好像用Spring实现了一套事务管理能力,但是如果在你的方法内部没有操作数据库或者数据库本身并没有事务处理能力,那么你这一套事务管理逻辑也是没有任何用处的。

2. Spring事务管理的局限性

  除了上面提到的问题,Spring的事务管理还存在另外一个非常头疼的问题:即在单一应用当中如果出现跨多个数据库(无论是同种类型或者不同种类型)的数据操作过程,此时声明式事务将无法保证事务的一致性,也就失去了它的作用。

  这里我们需要先了解一下事务的两种类型:本地事务全局事务

  • 本地事务:指在单个资源(通常是单个数据库)上的事务操作。它仅涉及一个数据源或数据库,并且事务的边界在这个数据源内部进行控制。
  • 全局事务:也叫分布式事务,是指在多个资源上执行的事务操作,通常涉及多个不同的数据库、消息队列或微服务等不同系统(本质上就是涉及多个数据库多个资源管理器)。这类事务需要跨多个数据源或系统保持一致性,尤其在分布式系统中非常常见。

  @Transactional注解所管理的事务通常是针对单一数据库的本地事务,对于多数据库的场景显然是没有办法处理。因为不同的数据库各自有各自的事务边界,没有办法简单的通过一个@Transactional注解就能将其合并成一个。毕竟声明式事务实际的处理逻辑就是简单的利用本地事务在方法的开头添加“开启事务”指令,在方法的执行完成后发出“提交”指令,以及在检测到异常时发出“回滚”指令。

  有人可能会说,声明式事务不起作用,那么编程式事务呢?确实,编程式事务由于提供了更细粒度的事务控制能力可以在一定程度上帮助开发者更好地控制事务的边界,但是它并不能直接解决全局事务中的一致性问题,要想解决这一个问题你就需要一种分布式事务协议来去保证。当然,这就是另外的价钱,哦,不,这就是另外的问题了,有机会我们以后再聊。

四、总结

  在介绍了这么多之后,相比大家应该明白标题当中所说的此“事务”非彼“事务”的含义了。除了了解两者的区别,闲宇还和大家分析了一下Spring事务管理的局限性,当然如果系统本身不涉及全局事务的情况,我们还是可以放心的去使用Spring的事务管理能力,当然能不能用好那就是另外一个问题了[狗头]。

  最后,祝大家身体健康,心想事成,早日财富自由~~