本文章为【极客时间】课程【MySQL 实战 45 讲】的内容整理

什么是事务

image.png

在计算机术语中,事务(Transaction)指的是访问并可能更新数据库中各种数据项的一个程序执行单元。

我们为什么需要事务?

事务是为了解决数据安全操作提出的解决方案,事务的控制实际上就是控制数据的安全访问与隔离。

举一个简单的例子:

银行转账,A 账户将自己的 1000 元转账给 B ,那么业务实现的逻辑首先是将 A 的余额减少 1000,然后往 B 的余额里增加 1000,假如这个过程中出现意外,导致过程中断,A 已经扣款成功,B 还没来得及增加,就会导致 B 损失了1000 元。所以我们必须做出控制,要求 A 账户转帐业务撤销,这才能保证业务的正确性,完成这个操作就需要事务,将 A 账户资金减少和 B 账户资金增加放到同一个事务里,要么全部执行成功,要么全部失败,这样就保证了数据的安全性。

MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。

MySQL 事务四大特性

MySQL 事务包含四大特性(ACID),分别为:原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)。

原子性(Atomicity)

什么是原子性?

原子性是指事务是应用中不可再分的最小执行体,即事务包含的一系列操作要么全部成功,要么全部失败(rollback),绝对不存在部分成功或者部分失败的情况。

一致性(Consistency)

一致性可以理解为堆数据完整性约束的遵循。这些约束可能包括主键约束,唯一索引约束,外键约束等等。事务执行前后,数据都是合法的,不会违背任何数据的完整性。

举例:拿转账来说,A 和 B 加起来有 5000 元,无论 A 和 B 如何转账,转了几次账,A 和 B 加起来的钱永远都是 5000 元。

隔离性(Isolation)

隔离性是指当多个用户以并发的方式操作数据库,比如操作同一个表,数据库为每一个用户开启的事物,不能被其他的事务所干扰或者影响,事务之间是彼此隔离的。

永久性(Durability)

永久性是指一个事务一旦提交了,那么其对数据库中数据的改变就是永久的,即使是数据库发生了故障时,也不会丢失事务提交的数据。

隔离性与隔离级别

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read),不可重复读(non-repeatable read),幻读(phantom read)这些问题,为了解决这些问题,就有了 MySQL“隔离级别”的概念。

在了解 MySQL 的“隔离级别”之前,我们先来了解一下什么是脏读,幻读与不可重复读。

  1. 脏读

脏读是指当事务 A 正在访问数据,并且对数据进行了修改,而这个修改还没有提交到数据库中,此时另一个事务 B 也访问到了这个数据,然后使用了这个数据,结果事务 A 发生回滚,那么事务 B 读到的就是一个“脏数据”。

示意图:
image.png

  1. 不可重复读

不可重复读是指在一个事务内 ,多次读同一数据。例如:事务 B 读取某一数据,在事务 B 还没有结束时,另外一个事务 A 也访问了该同一数据,并且修改了这一数据。那么,在事务 B 的两次读数据之间,由于事务 A 对数据的修改,导致事务 B 两次读到的的数据可能是不一样的,这就是不可重复读。例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。

示意图:
image.png

  1. 幻读

幻读是指:同样一个查询在整个事务中多次执行,查询所得结果不同。

例如,事务 B 对表中全部记录做了更新操作,尚未提交前,事务 _A 又插入了一条记录,那么事务 _B 再次读取数据库时,就会发现还有一条记录(即事务 A 新插入的记录)没有做更新。

示意图:
image.png

事务的隔离级别

在谈事务的隔离级别之前,你需要知道的是,事务的隔离级别越高,效率就越低。因此很多的时候,我们需要在二者之间寻找一个平衡点。

SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )。

  • 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。

  • 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。

  • 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。

  • 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

image.png

示例:

数据表 T 中只有一列,其中一行的值为 1;

  1. create table T(c int) engine=InnoDB;
  2. insert into T(c) values(1);

image.png
我们来看看在不同的隔离级别下,事务 A 会得到哪些返回结果,也就是图中的 V1,V2,V3 的值分别是什么。

  • 若隔离级别是“读未提交”,V1 的值 就是 2。此时虽然事务 B 还没有提交,但是结果已经被 A 看到了。这就导致了脏读,因此,V2,V3 的值也都是 2。
  • 若隔离级别是“读提交”,脏读就可以避免,V1 的值是 1;但是在事务 B 提交之后,事务 A 查询得到的值 V2 就是 2,这就导致了原始读取不可重复,即:不可重复读。V3 的值也是 2。
  • 若隔离级别是“可重复读”,则 V1,V2 都是 1,V3 是 2。因为隔离级别设置为可重复读,就要求事务在执行期间看到的数据前后必须是一致的,所以 V2 查询的值为 1。
  • 若隔离级别是“串行化”,则事务 B 在执行 “将 1 改成 2” 的时候,会被锁住。直到事务 A 提交之后,事务 B 才可以继续执行。所以从 A 的角度来看,V1,V2 的值是 1,V3 的值是 2。

在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。

  • RU(读未提交)隔离级别下直接返回记录上的最新的值,没有视图这一概念
  • RC(读提交)隔离级别下,MVCC 视图会在每一个语句前创建一个,所以在 RC 级别下,一个事务是可以看到另外一个事务已经提交的内容,因为它在每一次查询之前都会重新给予最新的数据创建一个新的 MVCC 视图
  • RR(可重复读)隔离级别下,MVCC 视图是在开始事务的时候就创建好了,这个视图会一直使用,直到该事务结束。
  • Serializable(序列化)隔离级别下,也没有视图这一概念,它是通过锁来实现数据访问隔离的。

值得一提的是,Oracle 数据库的默认隔离级别是 RC,因此如果对于一些从 Oracle 迁移到 MySQL 上的应用,为了保证数据隔离级别的一致,需要将 MySQL 的隔离级别设置为 RC(读提交)。

事务隔离的实现

在 MySQL 中,每一条记录在更新的时候,除了将变更记录到 redo log 日志中,还会记录一条变更相反的回滚记录,回滚记录被记录在 undo log 日志中。

假设一个值从 1 按照顺序改成了 2,3,4。在 undo log 日志中就会有类似下面的记录:

image.png

当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。

对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。你一定会问,回滚日志总不能一直保留吧,什么时候删除呢?

答案是,在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。

为什么尽可能不要使用长事务?

答案是:因为长事务的存在会导致 undo log 日志一直存在不被删除。

长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

事务的开启方式

MySQL 开启事务的方式有以下几种:

  1. 显示启动事务语句,begin 或 start transaction。配套的提交语句为 commit,回滚语句是 rollback。
  2. set autocommit = 0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。

Spring 的事务抽象

image.png
Spring 并不是提供了完整的事务操作 _API 而是提供了多种事务管理器,将事务的职责托管给了 JDBCHibernateJTA _等持久化平台来实现。

Spring 的抽象事务模型基于接口 PlatformTransactionManager,它的主要作用是为应用程序提供事务界定的统一方式,但是具体事务实现是由各大平台自己去实现的。

PlatformTransactionManager

  1. public interface PlatformTransactionManager extends TransactionManager {
  2. TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
  3. throws TransactionException;
  4. void commit(TransactionStatus status) throws TransactionException;
  5. void rollback(TransactionStatus status) throws TransactionException;
  6. }

事务传播特性
传播性 描述
PROPAGATION_REQUIRED 0 当前有事务就用当前的,没有就用新的
PROPAGATION_SUPPORTS 1 事务可有可无,不是必须的
PROPAGATION_MANDATORY 2 当前一定要有事务,不然就抛出异常
PROPAGATION_REQUIRES_NEW 3 无论是否有事务,都起个新的事务
PROPAGATION_NOT_SUPPORTED 4 不支持事务,按照非事务的方式运行
PROPAGATION_NEVER 5 不支持事务,如果有事务则抛出异常
PROPAGATION_NESTED 6 当前有事务就在当前事务里再启一个事务

我们来重点看下 PROPAGATION_REQUIREDPROPAGATION_REQUIRES_NEWPROPAGATION_NESTED 这三者的差异:

我们定义了 _service.methodA() 方法,该方法以 PROPAGATION_REQUIRED _修饰;service.methodB() 方法则分别以如下表格中的三种方式进行修饰;methodA() 方法中调用了 methodB() 方法

异常状态 PROPAGATION_REQUIRED
(同一个事务)
PROPAGATION_REQUIRES_NEW
(两个独立事务)
PROPAGATION_NESTED
(B事务嵌套在A事务中)
methodA 抛异常,methodB 正常 A 与 B 一起回滚 A回滚,B正常提交 A与B一起回滚
methodA 正常,methodB 抛异常 A 与 B 一起回滚 1.如果A中捕获了B的异常,并没有继续向上抛异常,则B先回滚,A再正常提交。
2.如果A没有捕获B的异常,默认将B的异常继续向上抛出,则B先回滚,A再回滚。
1.如果A中捕获了B的异常,并没有继续向上抛异常,则B先回滚,A再正常提交。
2.如果A没有捕获B的异常,默认将B的异常继续向上抛出,则B先回滚,A再回滚。
methodA 抛异常,methodB 抛异常 A 与 B 一起回滚 B先回滚,A再回滚 A与B一起回滚
methodA 正常,methodB 正常 A 与 B 一起提交 B先提交,A再提交 A与B一起提交

编程式事务

TransactionTemplate

  • TransactionCallback(有返回值)

  • TransactionCallbackWithoutResult(无返回值)

示例程序如下:

schema.sql

  1. CREATE TABLE FOO (ID INT IDENTITY ,BAR VARCHAR(64));

ProgrammaticTransactionDemoApplication

  1. package com.geektime.programmatictransactiondemo;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.boot.CommandLineRunner;
  5. import org.springframework.boot.SpringApplication;
  6. import org.springframework.boot.autoconfigure.SpringBootApplication;
  7. import org.springframework.jdbc.core.JdbcTemplate;
  8. import org.springframework.transaction.TransactionStatus;
  9. import org.springframework.transaction.support.TransactionCallbackWithoutResult;
  10. import org.springframework.transaction.support.TransactionTemplate;
  11. @SpringBootApplication
  12. @Slf4j
  13. public class ProgrammaticTransactionDemoApplication implements CommandLineRunner {
  14. @Autowired
  15. private TransactionTemplate transactionTemplate;
  16. @Autowired
  17. private JdbcTemplate jdbcTemplate;
  18. public static void main(String[] args) {
  19. SpringApplication.run(ProgrammaticTransactionDemoApplication.class, args);
  20. }
  21. @Override
  22. public void run(String... args) throws Exception {
  23. log.info("COUNT BEFORE TRANSACTION:{}", getCount());
  24. transactionTemplate.execute(new TransactionCallbackWithoutResult() {
  25. @Override
  26. protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
  27. jdbcTemplate.execute("INSERT INTO FOO(ID,BAR) VALUES(1,'aaa')");
  28. log.info("COUNT IN TRANSACTION:{}", getCount());
  29. // 回滚
  30. transactionStatus.setRollbackOnly();
  31. }
  32. });
  33. log.info("COUNT AFTER TRANSACTION:{}", getCount());
  34. }
  35. private long getCount() {
  36. return (long) jdbcTemplate.queryForList("SELECT COUNT(*) AS CNT FROM FOO")
  37. .get(0).get("CNT");
  38. }
  39. }

程序输出结果如下:

  1. COUNT BEFORE TRANSACTION:0
  2. COUNT IN TRANSACTION:1
  3. COUNT AFTER TRANSACTION:0

声明式事务

image.png

如图所示,Spring 的声明式事务管理在底层是建立在 AOP 的基础之上的。其本质是对方法前后进行拦截,然后再目标方法开始之前创建或加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过等价的基于注解的方式),便可以将事务规则应用到业务逻辑中。因为事务管理本身就是一个典型的横切逻辑,正是 AOP 的用武之地。Spring 开发团队也意识到了这一点,为声明式事务提供了简单而强大的支持。

基于注解的配置方式

开启事务注解的方式是通过 @EnableTranscationManagement 注解来完成的

@EnableTranscationManagement

  • proxyTargetClass


  • true

    • 目标对象实现了接口 —— 使用 CGLIB 代理机制
    • 目标对象没有接口(只有实现类) —— 使用 CGLIB 代理机制
  • false

    • 目标对象实现了接口 —— 使用 JDK 动态代理机制(代理所有实现了的接口)
    • 目标对象没有接口(只有实现类) —— 使用 CGLIB 代理机制
  • mode


  • mode = AdviceMode.PROXY

事务管理功能底层实现使用 _JDK _动态代理

  • mode = AdviceMode.ASPECTJ

事务管理功能底层实现使用_ _AspectJ

  • order

对事务 AOP 拦截的顺序

@Transactional

  • transactionManager
  • propagation
  • isolation
  • timeout
  • readOnly
  • rollbackFor

示例程序

schema.sql

  1. CREATE TABLE FOO(ID INT IDENTITY ,BAR VARCHAR(64));

RollbackException

  1. package com.geektime.declarativetransactiondemo;
  2. public class RollbackException extends Exception {
  3. }

FooService

  1. package com.geektime.declarativetransactiondemo;
  2. public interface FooService {
  3. void insertRecord();
  4. void insertThenRollback() throws RollbackException;
  5. void invokeInsertThenRollback() throws RollbackException;
  6. }

FooServiceImpl

  1. package com.geektime.declarativetransactiondemo;
  2. import org.springframework.beans.factory.annotation.Autowired;
  3. import org.springframework.jdbc.core.JdbcTemplate;
  4. import org.springframework.stereotype.Component;
  5. import org.springframework.transaction.annotation.Transactional;
  6. @Component
  7. public class FooServiceImpl implements FooService {
  8. @Autowired
  9. private JdbcTemplate jdbcTemplate;
  10. @Autowired
  11. private FooService fooService;
  12. @Override
  13. public void insertRecord() {
  14. jdbcTemplate.execute("INSERT INTO FOO(BAR) VALUES('AAA')");
  15. }
  16. @Override
  17. @Transactional(rollbackFor = RollbackException.class)
  18. public void insertThenRollback() throws RollbackException {
  19. jdbcTemplate.execute("INSERT INTO FOO(BAR) VALUES('BBB')");
  20. throw new RollbackException();
  21. }
  22. @Override
  23. public void invokeInsertThenRollback() throws RollbackException {
  24. insertThenRollback();
  25. }
  26. }

DeclarativeTransactionDemoApplication

  1. package com.geektime.declarativetransactiondemo;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.boot.CommandLineRunner;
  5. import org.springframework.boot.SpringApplication;
  6. import org.springframework.boot.autoconfigure.SpringBootApplication;
  7. import org.springframework.context.annotation.AdviceMode;
  8. import org.springframework.jdbc.core.JdbcTemplate;
  9. import org.springframework.transaction.annotation.EnableTransactionManagement;
  10. @SpringBootApplication
  11. @EnableTransactionManagement(mode = AdviceMode.PROXY)
  12. @Slf4j
  13. public class DeclarativeTransactionDemoApplication implements CommandLineRunner {
  14. @Autowired
  15. private JdbcTemplate jdbcTemplate;
  16. @Autowired
  17. private FooService fooService;
  18. public static void main(String[] args) {
  19. SpringApplication.run(DeclarativeTransactionDemoApplication.class, args);
  20. }
  21. private void queryRecordsCount(){
  22. log.info("COUNT : {}",jdbcTemplate.queryForObject("SELECT COUNT(*) FROM FOO ",Long.class));
  23. }
  24. @Override
  25. public void run(String... args) throws Exception {
  26. // insertRecord
  27. fooService.insertRecord();
  28. queryRecordsCount();
  29. // insertThenRollback
  30. try {
  31. fooService.insertThenRollback();
  32. }catch (RollbackException e){
  33. queryRecordsCount();
  34. }
  35. // invokeInsertThenRollback
  36. try {
  37. fooService.invokeInsertThenRollback();
  38. }catch (RollbackException e){
  39. queryRecordsCount();
  40. }
  41. }
  42. }

程序输出结果为:

  1. COUNT : 1
  2. COUNT : 1
  3. COUNT : 2

我们发现,invokeInsertThenRollback 方法并没有发生回滚!

其原因在于,声明式事务是通过 AOP 动态代理实现的,这样会产生一个代理类来做事务管理,而目标类(service)本身是不能感知到代理类的存在的。

对于加了 @Transactional 注解的方法来说,在调用代理类的方法时,会先通过拦截器 TransactionInterceptor 开启事务,然后再调用目标类的方法,最后调用结束后,TransactionInterceptor 会提交或回滚事务。如果我们用 invokeInsertThenRollback 方法去调用使用了 @Transactional 注解标识的 insertThenRollback 方法时,这里面实际上是通过隐式地通过 this 调用,此时的类是未被增强的类,所以也就不存在执行事务的代理,也就没有发生回滚。我们只用真正地用到被代理的类,才会起作用。

我们可以将 FooServiceImpl 的 invokeInsertThenRollback 方法修正为:

  1. @Override
  2. public void invokeInsertThenRollback() throws RollbackException {
  3. fooService.insertThenRollback();
  4. // 或
  5. // ((FooService)AopContext.currentProxy()).invokeInsertThenRollback();
  6. // 或
  7. // 我们可以在 invokeInsertThenRollback 方法上添加 @Transactional 注解,再加一层事务
  8. }

再次运行程序,执行结果为:

  1. COUNT : 1
  2. COUNT : 1
  3. COUNT : 1

我们看到 invokeInsertThenRollback 起了作用,也发生了回滚。