目录

Mysql事务详解

Mysql 事务说明

Mysql 事务特点

1、ACID

  • Atomicity(原子性):一个事务必须被视为一个不可分割的最小工作单元,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
  • Consistency(一致性):数据库总是从一个一致性状态转换到另一个一致状态。下面的银行列子会说到。
  • Isolation(隔离性):通常来说,一个事务所做的修改在最终提交以前,对其他事务是不可见的。注意这里的“通常来说”,后面的事务隔离级级别会说到。
  • Durability(持久性):一旦事务提交,则其所做的修改就会永久保存到数据库中。此时即使系统崩溃,修改的数据也不会丢失。(持久性的安全性与刷新日志级别也存在一定关系,不同的级别对应不同的数据安全级别。)

示例如下,银行转账为例

1
2
3
4
5
START TRANSACTION;
SELECT balance FROM checking WHERE customer_id = 10233276;
UPDATE checking SET balance = balance - 200.00 WHERE customer_id = 10233276;
UPDATE savings SET balance = balance + 200.00 WHERE customer_id = 10233276;
COMMIT;
  • 原子性:要么完全提交(10233276的checking余额减少200,savings 的余额增加200),要么完全回滚(两个表的余额都不发生变化)
  • 一致性:这个例子的一致性体现在 200元不会因为数据库系统运行到第3行之后,第4行之前时崩溃而不翼而飞,因为事务还没有提交。
  • 隔离性:允许在一个事务中的操作语句会与其他事务的语句隔离开,比如事务A运行到第3行之后,第4行之前,此时事务B去查询checking余额时,它仍然能够看到在事务A中被减去的200元(账户钱不变),因为事务A和B是彼此隔离的。在事务A提交之前,事务B观察不到数据的改变。
  • 持久性:这个很好理解。
  • 事务的隔离性是通过锁、MVCC等实现 (MySQL锁总结)
  • 事务的原子性、一致性和持久性则是通过事务日志实现

事务隔离级别

并发带来的问题

  • 更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 --最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一 文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。 最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同 一文件,则可避免此问题。
  • 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前, 这条记录的数据就处于不一致状态; 这时, 另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。
  • 不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读” 。
  • 幻读 (Phantom Reads): 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读” 。

幻读和不可重复读的区别

  • 不可重复读的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样。(因为中间有其他事务提交了修改)
  • 幻读的重点在于新增或者删除:在同一事务中,同样的条件,,第一次和第二次读出来的记录数不一样。(因为中间有其他事务提交了插入/删除)

并发事务带来的问题解决办法

  • “更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。
  • “脏读” 、 “不可重复读”和“幻读” ,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决:
    • 一种是加锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。
    • 另一种是数据多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC),也称为多版本数据库:不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本

SQL标准定义了4类隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

第1级别:Read Uncommitted(读取未提交内容)
  • 所有事务都可以看到其他未提交事务的执行结果
  • 本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少
  • 该级别引发的问题是——脏读(Dirty Read):读取到了未提交的数据
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- 创建表
SET @@session.transaction_isolation = 'READ-UNCOMMITTED';
create database test;
use test;
create table test(id int primary key);
insert into test(id) values(1);
-- 开启一个终端,开启事务,更新ID为1的记录更新为2
begin;
update test set id = 2 where id = 1;
select * from test; -- 此时看到一条ID为2的记录
-- 开启另一个终端,开启事务,查看表中的数据
use test;
begin;
select * from test; -- 此时看到一条 ID 为 2 的记录

最后一步读取到了 mysql 终端 1 中未提交的事务(没有 commit 提交动作),即产生了 脏读 ,大部分业务场景都不允许脏读出现,但是此隔离级别下数据库的并发是最好的。

READ-UNCOMMITTED 中文叫未提交读,即一个事务读到了另一个未提交事务修改过的数据,整个过程如下图: https://user-gold-cdn.xitu.io/2019/4/21/16a3e77ccc9302f3?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

如上图,SessionA和SessionB分别开启一个事务,SessionB中的事务先将id为1的记录的name列更新为’lisi',然后Session 中的事务再去查询这条id为1的记录,那么在未提交读的隔离级别下,查询结果由’zhangsan’变成了’lisi',也就是说某个事务读到了另一个未提交事务修改过的记录。但是如果SessionB中的事务稍后进行了回滚,那么SessionA中的事务相当于读到了一个不存在的数据,这种现象也称为脏读。 可见READ-UNCOMMITTED是非常不安全。

第2级别:Read Committed(读取提交内容)
  • 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)
  • 它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变
  • 这种隔离级别出现的问题是——不可重复读(Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的select语句时可能看到不一样的结果。导致这种情况的原因可能有:
    • 有一个交叉的事务有新的commit,导致了数据的改变;
    • 一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的commit
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
-- 创建表
SET @@session.transaction_isolation = 'READ-COMMITTED';
create database test;
use test;
create table test(id int primary key);
insert into test(id) values(1);
-- 开启一个终端,开启事务,更新ID为1的记录更新为2,并确认记录数变更过来
begin;
update test set id = 2 where id = 1;
select * from test; -- 此时看到一条记录为 2
-- 开启另一个终端,开启事务,查看表中的数据
use test;
begin;
select * from test; -- 此时看一条 ID 为 1 的记录
-- 登录到第一个终端,提交事务
commit;
-- 切换到第二个终端
select * from test; -- 此时看到一条 ID 为 2 的记录

mysql 终端 2 在开启了一个事务之后,在第一次读取 test 表(此时 mysql 终端 1 的事务还未提交)时 ID 为 1 ,在第二次读取 test 表(此时 mysql 终端 1 的事务已经提交)时 ID 已经变为 2 ,说明在此隔离级别下已经读取到已提交的事务。

READ COMMITTED 中文叫已提交读,或者叫不可重复读。即一个事务能读到另一个已经提交事务修改后的数据,如果其他事务均对该数据进行修改并提交,该事务也能查询到最新值. https://user-gold-cdn.xitu.io/2019/4/21/16a3e781090f006c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

在第4步 SessionB 修改后,如果未提交,SessionA是读不到,但SessionB一旦提交后,SessionA即可读到SessionB修改的内容。

从某种程度上已提交读是违反事务的隔离性的

第3级别:Repeatable Read(可重读)
  • 这是MySQL的默认事务隔离级别
  • 它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
  • 此级别可能出现的问题——幻读(Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行
  • InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决幻读问题;InnoDB还通过间隙锁解决幻读问题

REPEATABLE READ 中文叫可重复读,即事务能读到另一个已经提交的事务修改过的数据,但是第一次读过某条记录后,即使后面其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据. https://user-gold-cdn.xitu.io/2019/4/21/16a3e7901014d542?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

InnoDB默认是这种隔离级别,SessionB无论怎么修改id=1的值,SessionA读到依然是自己开启事务第一次读到的内容。

SERIALIZABLE 串行化

SERIALIZABLE 叫串行化, 上面三种隔离级别可以进行 读-读 或者 读-写、写-读三种并发操作,而SERIALIZABLE不允许读-写,写-读的并发操作。 https://user-gold-cdn.xitu.io/2019/4/21/16a3e78adb5c0e95?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

SessionB 对 id=1 进行修改的时候,SessionA 读取id=1则需要等待 SessionB 提交事务。可以理解SessionB在更新的时候加了X锁。

分布式事务

分布式事务指允许多个独立的事务资源参与到一个全局的事务中。全局事务要求在其中的所有参与的事务要么都提交,要么都回滚。

InnoDB 分布式事务

InnoDB 是支持分布式事务,由一个或多个资源管理器(Resource Managers),一个事务管理器(Transaction Manager),以及一个应用程序(Application Program)组成。

  • 资源管理器(Resource Managers),提供访问事务资源的方法,一般一个数据库就是一个资源管理器。
  • 事务管理器(Transaction Manager),协调参与全局事务中的各个事务,需要和参与全局事务的所有资源管理器进行通信。
  • 应用程序(Application Program) 定义事务的边界,指定全局事务中的操作。

如下图: https://user-gold-cdn.xitu.io/2019/4/21/16a3e79853aa44a8?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

应用程序向一个或多个数据库执行事务操作,事务管理器进行管理事务,通过二段式提交,第一阶段所有参与的全局事务的节点都开始准备,告诉事务管理器都准备好了,可以提交了。第二阶段,事务管理器告诉每一个资源管理器是执行Commit 还是 Rollback。如果任何一个节点显示不能提交,则所有的节点被告知需要回滚

TCC分布式事务

InnoDB的分布式是数据库实现的,看看数据库外如何分布式事务,比较常见的是TCC分布式事务。 https://user-gold-cdn.xitu.io/2019/4/21/16a3e79c2f5f5aff?imageView2/0/w/1280/h/960/format/webp/ignore-error/1

上图描述了TCC分布式事务的流程,假设电商业务中,支付后需要修改库存,积分,物流仓储的数据,如果一个失败则全部回滚。

TCC分布式事务,有三个阶段,Try,Confirm, Cancel。也就是说每个参与事务的服务都需要实现这三个接口,库存、积分、仓储都需要实现这三个接口。

第一阶段,Try,业务应用调取各个服务的Try接口,告诉他们给我预留一个商品,有人要购买,可以理解为冻结,每一步都不执行成功,只是标记更新状态。

第二阶段,Confirm,确认阶段,即事务协调器调取每个服务Confirm执行事务操作,如果某一个服务的Confirm失败,则有第三个阶段。如果成功则结束事务。

第三个阶段,Cancel,如果在第二个阶段有一个事务提交失败,则事务协调器调取所有业务的Cancel接口,回滚事务,将第一阶段冻结的商品恢复。