数据库篇-数据库事务

image.png

背景

编程其实就是将现实世界的操作逻辑转为计算机进行操作。以最经典的银行转账为案例,我们在数据库中会有一张表存放账户的的信息(所属用户、余额等),如下表所示

1
2
3
4
5
6
7
8
9
mysql> select * from account;
+----+----------+---------+
| id | username | balance |
+----+----------+---------+
| 1 | mikey | 200.00 |
| 2 | leo | 200.00 |
| 3 | don | 0.00 |
+----+----------+---------+
3 rows in set (0.00 sec)

如下图中为我们的转账业务流程,在执行蓝色区域中的步骤时可能会出现在执行对mikey扣减100后突然断电了,到账mikey被扣减了,但是leo并没有增加100,出现数据不一致的问题。

在这种场景下数据库事务应运而生,专门来解决这种问题。在调用转账的操作时启用事务,此后的所有操作数据库会将我们的操作视为一个整体,只要其中一个操作出现问题,那么在这个整体中的所有的操作都会被回滚。

简介

数据库事务(transaction)是数据库的一个重要的组成部分,它能保证我们的数据能否安全的执行存储,在出现异常的时候能及时回滚,保证业务的正常执行。

事务的定义

数据库事务 (transaction) 是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

事务的四大特性ACID

ACID,是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability)

原子性

事务要么执行,要么不执行,不会出现部分执行的情况,是一个不可分割的工作单位。一旦在这个操作的过程中只要有任意一个步骤出现错误,将回滚到起始位置,所有对数据的操作都无效。

一致性

在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

隔离性

数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)

持久性

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

数据库隔离级别

事务指定一个隔离级别,该隔离级别定义一个事务必须与由其他事务进行的资源或数据更改相隔离的程度。隔离级别从允许的并发副作用(例如,脏读或幻读)的角度进行描述。

  • 脏读:一个事务能读到另一个事务修改了但未提交事务的数据。

  • 不可重复读:两次执行同样的查询,可能会得到不一样的结果。一个事务开始时,只能“看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。

  • 幻读:指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插人了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行 (Phantom Row)

读未提交

READ_UNCOMMITTED

在READ UNCOMMITTED 级别,事务中的修改,即使没有提交,对其他事务也都是可见的。事务可以读取未提交的数据,这也被称为脏读 (Dirty Read)。这个级别会导致很多问题,从性能上来说,READ UNCOMNITTED 不会比其他的级别好太多,但却缺乏其他级别的很多好处,除非真的有非常必要的理由,在实际应用中一般很少使用。

读已提交

READ_COMMITTED

大多数数据库系统的默认隔离级别都是 READ COMMITTED(但 MySQL 不是)。READ COMMITTED 满足前面提到的隔离性的简单定义:一个事务开始时,只能“看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读 (nonrepeatableread),因为两次执行同样的查询,可能会得到不一样的结果。

可重复读

REPEATABLE_READ

可重复读(Repeatable Read),当使用可重复读隔离级别时,在事务执行期间会锁定该事务以任何方式引用的所有行。因此,如果在同一个事务中发出同一个SELECT语句两次或更多次,那么产生的结果数据集总是相同的。因此,使用可重复读隔离级别的事务可以多次检索同一行集,并对它们执行任意操作,直到提交或回滚操作终止该事务。

REPEATABLE READ 解決了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,可重复读隔离级别还是无法解决另外一个幻读(Phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插人了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行 (Phantom Row)。InnoDB 和 XtraDB 存储引擎通过多版本并发控制 (MVCC, Multiversion Concurrency Control)解决了幻读的问题。

可重复读是MySQL 的默认事务隔离级别。

串行化

SERIALIZABLE

SERIALIZABLE 是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。简单来说,SERIALIZABLE 会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁年用的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。

隔离级别 脏读(Dirty Read) 不可重复读(NonRepeatable Read) 幻读(Phantom Read)
未提交读(Read uncommitted)
已提交读(Read committed)
可重复读(Repeatable read)
可串行化(Serializable )

案例

简单案例

1
2
3
4
5
6
mysql> show variables like '%tx_isolation%';
+---------------+-----------------+
| Variable_name | Value |
+---------------+-----------------+
| tx_isolation | REPEATABLE-READ |
+---------------+-----------------+

修改事务的隔离级别

1
2
set session transaction isolation level read uncommitted;
#可选参数有:read uncommited, read commited, repetable read, serializable.

开启事务操作数据

1
2
3
4
5
START TRANSACTION; 
SELECT balance FROM account WHERE username = 'mikey';
UPDATE checking SET balance = balance - 200 WHERE username = 'mikey'
UPDATE savings SET balance = balance + 200 WHERE username = 'leo';
COMMIT;

Spring中的事务

Spring事务相关的API主要在spring-tx.jar包中,提供了xml、注解的方式支持事务

声明式事务配置

1.引入依赖

1
2
3
4
5
<!--事务的AOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.配置事务管理器

1
2
3
4
5
6
7
8
@Configuration
public class TxConfig {
@Bean
public DataSourceTransactionManager dataSourceTransactionManager(DataSource datasource) {
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager(datasource);
return dataSourceTransactionManager;
}
}

3.开启事务

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableTransactionManagement//开启声明式事务
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

Spring事务的传播机制

Spring对事务支持符合ACID规范,同时对事务在多个多个方法中调用如何传递进行进一步细分,共有七种传播机制

传播机制 说明 备注
required 如果当前存在事务,就加入该事务。
如果当前没有事务,就创建一个新事务。
这是最常用的设置。 只创建一个事务。
requires_new 不管是否存在事务,都创建一个新的事务。
老事务 先挂起,再创建 新事务, 新事务 执行完并提交,
接着,继续执行 老事务,最后提交。 1、每次都创建一个新的事务。
2、创建 新事务 前,老事务 先挂起。
3、先执行的方法后提交事务,后执行的方法先提交事务。
4、老事务 的回滚,不会影响 新事务 的提交。
nested 如果当前存在事务,则在嵌套事务内执行。
如果当前没有事务,则执行与 required 类似的操作。 nested (嵌套)创建事务。
supports 支持当前事务。
如果当前存在事务,就加入该事务,
如果当前不存在事务,就以非事务执行。 supports 不会创建事务。
not_supported 不支持事务。
如果当前存在事务,就把当前事务 挂起。
如果当前没有事务,就以非事务执行。
mandatory 强制、必须使用事务。
如果当前 已经存在事务,就加入该事务,
如果当前不存在事务,就 抛出异常。 1、mandatory 不会创建事务。
2、mandatory 执行的前提是已经存在事务。
never 禁止事务 。
如果当前存在事务,则 抛出异常,
如果当前没有事务,以非事务方式执行, 必须在一个没有事务中执行,否则报错。

REQUIRED

  • 如果当前存在事务,就加入该事务。
  • 如果当前没有事务,就创建一个新事务。
  • 只创建一个事务。

配置类的调用方法的事务传播属性为required,需要注意的是事务的传播是针对类之间的方法,如果是一个类的方法无效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class NormalRequiredService extends JdbcBase {
@Resource
private ErrorRequiredService errorRequiredService;

@Transactional(propagation = Propagation.REQUIRED)
public void normalUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'mikey'");
errorRequiredService.errorUpdate(balance);
}
}

@Service
public class ErrorRequiredService extends JdbcBase {
@Transactional(propagation = Propagation.REQUIRED)
public void errorUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'leo'");
int ret = 1 / 0;
}
}

在调用normalUpdate方法后从下面的日志中(第1行)我们可以看出其创建了一个名为全类明+方法的事务,当执行到13行代码是出现异常两个方法中的操作都进行回滚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                                           # 创建一个事务
o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [com.example.demo.transaction.required.NormalRequiredService.normalUpdate]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.j.d.DataSourceTransactionManager : Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@3915a1b] for JDBC transaction
o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3915a1b] to manual commit
o.s.jdbc.core.JdbcTemplate : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'mikey']
# 发现已经存在了事务
o.s.j.d.DataSourceTransactionManager : Participating in existing transaction
o.s.jdbc.core.JdbcTemplate : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'leo']
o.s.j.d.DataSourceTransactionManager : Participating transaction failed - marking existing transaction as rollback-only
o.s.j.d.DataSourceTransactionManager : Setting JDBC transaction [com.mysql.cj.jdbc.ConnectionImpl@3915a1b] rollback-only
# 回滚操作
o.s.j.d.DataSourceTransactionManager : Initiating transaction rollback #
o.s.j.d.DataSourceTransactionManager : Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@3915a1b]
o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3915a1b] after transaction

REQUIRES_NEW

  • 不管是否存在事务,都创建一个新的事务。
  • 老事务 先挂起,再创建 新事务, 新事务 执行完并提交,接着,继续执行老事务,最后提交。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class NormalRequiredNewService extends JdbcBase {

@Resource
private ErrorRequiredNewService errorRequiredNewService;

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void normalUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'mikey'");
errorRequiredNewService.errorUpdate(balance);
}
}

@Service
public class ErrorRequiredNewService extends JdbcBase {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void errorUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'leo'");
int ret = 1 / 0;
}
}

1、每次都创建一个新的事务。创建 新事务 前,老事务 先挂起。先执行的方法后提交事务,后执行的方法先提交事务。老事务 的回滚,不会影响 新事务 的提交。

1
2
3
4
5
6
7
8
9
                                            # 创建一个新的事务
o.s.j.d.DataSourceTransactionManager : Creating new transaction with name [com.example.demo.transaction.requirednew.NormalRequiredNewService.normalUpdate]: PROPAGATION_REQUIRES_NEW,ISOLATION_DEFAULT
o.s.j.d.DataSourceTransactionManager : Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@41d080dc] for JDBC transaction
o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@41d080dc] to manual commit
o.s.jdbc.core.JdbcTemplate : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'mikey']
# 挂起当前事务,创建一个新的事务
o.s.j.d.DataSourceTransactionManager : Suspending current transaction, creating new transaction with name [com.example.demo.transaction.requirednew.ErrorRequiredNewService.errorUpdate]
o.s.j.d.DataSourceTransactionManager : Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@3221e102] for JDBC transaction
o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@3221e102] to manual commit

NESTED

  • 如果当前存在事务,则在嵌套事务内执行。
  • 如果当前没有事务,则执行与 required 类似的操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @Service
    public class NormalNestedService extends JdbcBase {

    @Resource
    private ErrorNestedService errorNestedService;

    @Transactional(propagation = Propagation.NESTED)
    public void normalUpdate(int balance){
    jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'mikey'");
    errorNestedService.errorUpdate(balance);
    }
    }

    @Service
    public class ErrorNestedService extends JdbcBase {
    @Transactional(propagation = Propagation.NESTED)
    public void errorUpdate(int balance){
    jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'leo'");
    int ret = 1 / 0;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.example.demo.transaction.nested.NormalNestedService.normalUpdate]: PROPAGATION_NESTED,ISOLATION_DEFAULT
    o.s.j.d.DataSourceTransactionManager : Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@6b96ef6d] for JDBC transaction
    o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6b96ef6d] to manual commit
    o.s.jdbc.core.JdbcTemplate : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'mikey']
    # 创建嵌套事务
    o.s.j.d.DataSourceTransactionManager : Creating nested transaction with name [com.example.demo.transaction.nested.ErrorNestedService.errorUpdate]
    o.s.jdbc.core.JdbcTemplate : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'leo']
    o.s.j.d.DataSourceTransactionManager : Rolling back transaction to savepoint
    o.s.j.d.DataSourceTransactionManager : Initiating transaction rollback
    o.s.j.d.DataSourceTransactionManager : Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@6b96ef6d]
    o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@6b96ef6d] after transaction

SUPPORTS

支持当前事务。如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。supports 不会创建事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class NormalNestedService extends JdbcBase {

@Resource
private ErrorNestedService errorNestedService;

@Transactional(propagation = Propagation.NESTED)
public void normalUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'mikey'");
errorNestedService.errorUpdate(balance);
}
}

@Service
public class ErrorNestedService extends JdbcBase {
@Transactional(propagation = Propagation.NESTED)
public void errorUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'leo'");
int ret = 1 / 0;
}
}
1
2
3
4
5
6
s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.example.demo.controller.TxController#supportsServiceTest()
o.s.jdbc.core.JdbcTemplate : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'mikey']
o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
o.s.jdbc.core.JdbcTemplate : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'leo']
o.s.j.d.DataSourceTransactionManager : Should roll back transaction but cannot - no transaction available
o.s.j.d.DataSourceTransactionManager : Should roll back transaction but cannot - no transaction available

MANDATORY

强制、必须使用事务。如果当前 已经存在事务,就加入该事务,如果当前不存在事务,就 抛出异常。

1、mandatory 不会创建事务。
2、mandatory 执行的前提是已经存在事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class ErrorMandatoryService extends JdbcBase {
@Transactional(propagation = Propagation.MANDATORY)
public void errorUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'leo'");
int ret = 1 / 0;
}
}
@Service
public class NormalMandatoryService extends JdbcBase {

@Resource
private ErrorMandatoryService errorMandatoryService;

public void normalUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'mikey'");
errorMandatoryService.errorUpdate(balance);
}
}
1
2
3
4
5
6
o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'mikey']
o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
o.s.web.servlet.DispatcherServlet : Failed to complete request: org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'] with root cause
# 抛出异常
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'

NOT_SUPPORTED

不支持事务。
如果当前存在事务,就把当前事务 挂起。
如果当前没有事务,就以非事务执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class ErrorNotSupportsService extends JdbcBase {
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void errorUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'leo'");
int ret = 1 / 0;
}
}
@Service
public class NormalNotSupportsService extends JdbcBase {

@Resource
private ErrorNotSupportsService errorNotSupportsService;

@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void normalUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'mikey'");
errorNotSupportsService.errorUpdate(balance);
}
}
1
2
3
4
5
6
7
o.s.jdbc.core.JdbcTemplate               : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'mikey']
o.s.jdbc.datasource.DataSourceUtils : Fetching JDBC Connection from DataSource
o.s.jdbc.core.JdbcTemplate : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'leo']
#应该回滚但是没有可用的事务,无法回滚
o.s.j.d.DataSourceTransactionManager : Should roll back transaction but cannot - no transaction available
o.s.j.d.DataSourceTransactionManager : Should roll back transaction but cannot - no transaction available
o.s.web.servlet.DispatcherServlet : Failed to complete request: java.lang.ArithmeticException: / by zero

NEVER

禁止事务 。
如果当前存在事务,则 抛出异常,
如果当前没有事务,以非事务方式执行,
必须在一个没有事务中执行,否则报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class ErrorNeverService extends JdbcBase {
@Transactional
public void errorUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'leo'");
int ret = 1 / 0;
}
}
@Service
public class NormalNeverService extends JdbcBase {

@Resource
private ErrorNeverService errorNeverService;

@Transactional(propagation = Propagation.NEVER)
public void normalUpdate(int balance){
jdbcTemplate.execute("UPDATE account SET balance = balance + "+balance+" WHERE username = 'mikey'");
errorNeverService.errorUpdate(balance);
}
}
1
2
3
4
5
6
7
8
9
10
o.s.j.d.DataSourceTransactionManager     : Creating new transaction with name [com.example.demo.transaction.never.ErrorNeverService.errorUpdate]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
o.s.j.d.DataSourceTransactionManager : Acquired Connection [com.mysql.cj.jdbc.ConnectionImpl@2aefb96b] for JDBC transaction
o.s.j.d.DataSourceTransactionManager : Switching JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2aefb96b] to manual commit
o.s.jdbc.core.JdbcTemplate : Executing SQL statement [UPDATE account SET balance = balance + 2 WHERE username = 'leo']
o.s.j.d.DataSourceTransactionManager : Initiating transaction rollback
o.s.j.d.DataSourceTransactionManager : Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@2aefb96b]
o.s.j.d.DataSourceTransactionManager : Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2aefb96b] after transaction
o.s.j.d.DataSourceTransactionManager : Resuming suspended transaction after completion of inner transaction
o.s.j.d.DataSourceTransactionManager : Should roll back transaction but cannot - no transaction available
o.s.web.servlet.DispatcherServlet : Failed to complete request: java.lang.ArithmeticException: / by zero

原理

MySQL实现事务原理

InnoDB是MySql中唯一支持事务的存储引擎,那么它是如何实现ACID的呢?
主要通过两门技术:并发控制技术日志恢复技术
并发控制技术保证了事务的隔离性,使数据库的一致性不会因为并发执行的操作被破坏。
日志恢复技术保证了事务的原子性,使一致性持久性不会因事务或系统故障被破坏。

并发控制

主要就是上锁,流程如下:

  • 先获得了锁,然后才能修改对应的数据A
  • 事务完成后释放锁,给下一个要修改数据A的事务
  • 同一时间,只能有一个事务持有数据A的互斥锁
  • 没有获取到锁的事务,需要等待锁释放

日志恢复

  • Redo Log:Redo Log记录的是新数据的备份。在事务提交前,只要将Redo Log持久化即可,不需要将数据持久化。当系统崩溃时,虽然数据没有持久化,但是Redo Log已经持久化。系统可以根据Redo Log的内容,将所有数据恢复到最新的状态。
  • Undo Log:Undo Log是旧数据的备份,在操作任何数据之前,首先将数据备份到Undo Log,然后进行数据的修改。如果出现了错误或者用户执行了回滚语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态。

MySQL 服务器层不管理事务,事务是由下层的存储引擎实现的。所以在同一个事务中,使用多种存储引擎是不可靠的。如果在事务中混合使用了事务型和非事务型的表(例如 InnoDB 和 MyISAM 表),在正常提交的情况下不会有什么问题。但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以,为每张表选择合适的存储引擎非常重要。在非事务型的表上执行事务相关操作的时候,MySQL 通常不会发出提醒,也不会报错。有时候只有回滚的时候才会发出一个警告.“某些非事务型的表上的变更不能被回滚”但大多数情况下,对非事务型表的操作都不会有提示。

Spring对事务的支持

Spring不是直接管理事务的,而是提供了多种事务管理器,通过这些事务管理器,Spring将事务委托给了Hibernate、MyBaits、JPA、Spring-jdbc等持久性框架的事务来实现

Spring事务分类

  • 编程式事务: 如果系统需要明确的事务,并且需要细粒度的控制各个事务的边界,此时建议使用编程式事务

  • 声明式事务: 如果系统对于事务的控制粒度较为粗糙,则建议使用声明式事务,可以通过注解**@Transational实现,原理是利用Spring框架通过AOP代理自动完成开启事务,提交事务,回滚事务**。回滚的异常默认是运行时异常,可以通过rollbackFor属性制定回滚的异常类型。

Spring事务核心类

TransactionDefinition.png

  • TransactionManager接口主要定义了三个规范接口,提交事务、获取事务和回滚事务
  • TransactionDefinition存储事务的相关数据,隔离级别、传播机制
  • TransactionStatus接口统一抽象了flush(刷入磁盘)、hasSavepoint(是否还有保存点)两个方法

Spring事务失效的场景

  1. 数据库不支持事务
  2. 事务方法未被Spring管理,也就是事务方法所在的类没有加载到Spring IOC容器中
  3. 方法没有被public修饰
    1
    2
    3
    4
    5
    @Override
    @Transactional(rollbackFor = Exception.class)
    private void todo() {

    }
    因为方法是私有的,加在私有方法上的事务注解不生效。
  4. 在同一个类中的方法调用。
  5. 方法的事务传播类型不支持事务。
  6. 不正确地捕获异常。使用了try-catch代码块将异常捕捉了,没有向上抛出异常,事务不会回滚。
  7. 标注错误的异常类型。Spring事务默认回滚类型是RuntimeException类型,如果没有制定回滚的类型,抛出的错误不是RuntimeException类型,则无法回滚

分布式事务

分布式事务指的是事务的发起者、资源及资源管理器和事务协调者分别位于分布式系统的不同节点之上。

二阶段提交:

阶段一:开始向事务涉及到的全部资源发送提交前信息。此时,事务涉及到的资源还有最后一次机会来异常结束事务。如果任意一个资源决定异常结束事务,则整个事务取消,不会进行资源的更新。否则,事务将正常执行,除非发生灾难性的失败。为了防止会发生灾难性的失败,所有资源的更新都会写入到日志中。这些日志是永久性的,因此,这些日志会幸免于难并且在失败之后可以重新对所有资源进行更新。

阶段二:只在阶段一没有异常结束的时候才会发生。此时,所有能被定位和单独控制的资源管理器都将开始执行真正的数据更新。 在分布式事务两阶段提交协议中,有一个主事务管理器负责充当分布式事务协调器的角色。事务协调器负责整个事务并使之与网络中的其他事务管理器协同工作。 为了实现分布式事务,必须使用一种协议在分布式事务的各个参与者之间传递事务上下文信息,IIOP便是这种协议。这就要求不同开发商开发的事务参与者必须支持一种标准协议,才能实现分布式的事务。

资料

  1. 《MySQL是怎样运行的:从根儿上理解MySQL》
  2. 《高性能MySQL第三版》

数据库篇-数据库事务
https://mikeygithub.github.io/2020/06/09/yuque/数据库篇-数据库事务/
作者
Mikey
发布于
2020年6月9日
许可协议