Mysql数据库事务

一、前言

MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说:银行系统,一个人转钱给另外一个人,这个过程应该放到一个事务中,不能因为其中一个环节失败,而导致钱的丢失,要么全部成功,要么全部失败。

下面带大家了解下数据库事务的特点、事务的隔离级别以及并发事务所带来的问题等等。

二、正文

1.事务的四个特性?

从业务角度出发,对数据库的一组操作要求保持4个特征:

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

为了更好地理解ACID,以银行账户转账为例:

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等实现
  • 事务的原子性、一致性和持久性则是通过事务日志实现(见下)

ACID 之间的关系

事务的 ACID 特性概念很简单,但不好理解,主要是因为这几个特性不是一种平级关系:

  • 只有满足一致性,事务的结果才是正确的。
  • 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
  • 事务满足持久化是为了能应对数据库崩溃的情况。

在这里插入图片描述

2.并发事务带来的问题

  • 更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 ,最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一 文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。 最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同 一文件,则可避免此问题。

例如:

T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。

在这里插入图片描述

  • 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前, 这条记录的数据就处于不一致状态; 这时, 另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。

列如:

T1 修改一个数据,T2 随后读取这个数据。如果 T1 撤销了这次修改,那么 T2 读取的数据是脏数据。

在这里插入图片描述

  • 不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读” 。

    :在 InnoDB 存储引擎中,SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题是通过 Record Lock (行锁) 解决的,INSERT 的不可重复读问题是通过 Next-Key Lock(Record Lock + Gap Lock)解决的。

例如:

T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。

在这里插入图片描述

  • 幻读 (Phantom Reads): 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读” 。

幻读和不可重复读的区别:

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

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

  • “更新丢失”通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。

  • “脏读” 、 “不可重复读”和“幻读” ,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决:

    (1)一种是加锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。

    (2)另一种是数据多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC),也称为多版本数据库:不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本。

更详细的关于MVCC讲解请看:MVCC详解

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

3.事务的隔离级别

第1级别:Read Uncommitted(未提交读)

  • 所有事务都可以看到其他未提交事务的执行结果

  • 本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少

  • 该级别引发的问题是:

    (1)脏读(Dirty Read):读取到了未提交的数据。

    (2)不可重复读

    (3)幻影读

第2级别:Read Committed(提交读)

  • 这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)

  • 它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变

  • 这种隔离级别出现的问题是:

    (1)不可重复读(Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的select语句时可能看到不一样的结果。导致这种情况的原因可能有:

    ​ (1)有一个交叉的事务有新的commit,导致了数据的改变;

    ​ (2)一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的commit

    (2)幻影读

第3级别:Repeatable Read(可重复读)

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

第4级别:Serializable(可串行化)

  • 这是最高的隔离级别
  • 它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。
  • 在这个级别,可能导致大量的超时现象和锁竞争

隔离级别和会导致的问题的比较

在这里插入图片描述

各具体数据库并不一定完全实现了上述 4 个隔离级别,例如:

  • Oracle 只提供 Read committed 和 Serializable 两个标准隔离级别,另外还提供自己定义的 Read only 隔离级别;

  • SQL Server 除支持上述 ISO/ANSI SQL92 定义的 4 个隔离级别外,还支持一个叫做“快照”的隔离级别,但严格来说它是一个用 MVCC 实现的 Serializable 隔离级别。

  • MySQL 支持全部 4 个隔离级别,但在具体实现时,有一些特点,比如在一些隔离级别下是采用 MVCC一致性读,但某些情况下又不是。

  • Mysql可以通过执行 set transaction isolation level命令来设置隔离级别,新的隔离级别会在下一个事务开始的时候生效。 例如: set session transaction isolation level read committed

    上面提到了提到了行锁(Record Lock),间隙锁(Gap Lock)等,下面来详细说下。

    4.锁算法

    Record Lock(行锁)

    锁定一个记录上的索引,而不是记录本身。

    如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。

    Gap Lock(间隙锁)

    锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。

    SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
    

    Next-Key Lock

    它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:

(-, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +)

在 InnoDB 存储引擎中,SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题通过 Record Lock 解决,INSERT 的不可重复读问题是通过 Next-Key Lock(Record Lock + Gap Lock)解决的。

5.事务日志?

事务日志可以帮助提高事务效率:

  • 使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。
  • 事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序I/O,而不像随机I/O需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。
  • 事务日志持久以后,内存中被修改的数据在后台可以慢慢刷回到磁盘。
  • 如果数据的修改已经记录到事务日志并持久化,但数据本身没有写回到磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这一部分修改的数据。

目前来说,大多数存储引擎都是这样实现的,我们通常称之为预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘。

Mysql中的事务实现原理

事务的实现是基于数据库的存储引擎。不同的存储引擎对事务的支持程度不一样。mysql中支持事务的存储引擎有innoDB和NDB。

innoDB是mysql默认的存储引擎,默认的隔离级别是RR(Repeatable Read),并且在RR的隔离级别下更进一步,通过多版本并发控制(MVCC,Multiversion Concurrency Control )解决不可重复读问题,加上间隙锁(也就是并发控制)解决幻读问题。因此innoDB的RR隔离级别其实实现了串行化级别的效果,而且保留了比较好的并发性能。

事务的隔离性是通过锁实现,而事务的原子性、一致性和持久性则是通过事务日志实现。说到事务日志,不得不说的就是redo和undo。

1.redo log

在innoDB的存储引擎中,事务日志通过重做(redo)日志和innoDB存储引擎的日志缓冲(InnoDB Log Buffer)实现。事务开启时,事务中的操作,都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化,这就是DBA们口中常说的“日志先行”(Write-Ahead Logging)。当事务提交之后,在Buffer Pool中映射的数据文件才会慢慢刷新到磁盘。此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据redo log中记录的日志,把数据库恢复到崩溃前的一个状态。未完成的事务,可以继续提交,也可以选择回滚,这基于恢复的策略而定。

在系统启动的时候,就已经为redo log分配了一块连续的存储空间,以顺序追加的方式记录Redo Log,通过顺序IO来改善性能。所有的事务共享redo log的存储空间,它们的Redo Log按语句的执行顺序,依次交替的记录在一起。如下一个简单示例:

记录1:<trx1, insert…>

记录2:<trx2, delete…>

记录3:<trx3, update…>

记录4:<trx1, update…>

记录5:<trx3, insert…>

2.undo log

undo log主要为事务的回滚服务。在事务执行的过程中,除了记录redo log,还会记录一定量的undo log。undo log记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据undo log进行回滚操作。单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。

以下是undo+redo事务的简化过程

假设有2个数值,分别为A和B,值为1,2

  1. start transaction;

  2. 记录 A=1 到undo log;

  3. update A = 3;

  4. 记录 A=3 到redo log;

  5. 记录 B=2 到undo log;

  6. update B = 4;

  7. 记录B = 4 到redo log;

  8. 将redo log刷新到磁盘

  9. commit

在1-8的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响。如果在8-9之间宕机,恢复之后可以选择回滚,也可以选择继续完成事务提交,因为此时redo log已经持久化。若在9之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据redo log把数据刷回磁盘。

所以,redo log其实保障的是事务的持久性和一致性,而undo log则保障了事务的原子性。

5.Mysql中的事务使用

MySQL的服务层不管理事务,而是由下层的存储引擎实现。比如InnoDB。

MySQL支持本地事务的语句:

START TRANSACTION | BEGIN [WORK] 
COMMIT [WORK] [AND [NO] CHAIN] [[NO] RELEASE] 
ROLLBACK [WORK] [AND [NO] CHAIN] [[NO] RELEASE] 
SET AUTOCOMMIT = {0 | 1}
  • START TRANSACTION 或 BEGIN 语句:开始一项新的事务。
  • COMMIT 和 ROLLBACK:用来提交或者回滚事务。
  • CHAIN 和 RELEASE 子句:分别用来定义在事务提交或者回滚之后的操作,CHAIN 会立即启动一个新事物,并且和刚才的事务具有相同的隔离级别,RELEASE 则会断开和客户端的连接。
  • SET AUTOCOMMIT 可以修改当前连接的提交方式, 如果设置了 SET AUTOCOMMIT=0,则设置之后的所有事务都需要通过明确的命令进行提交或者回滚

事务使用注意点:

  • 如果在锁表期间,用 start transaction 命令开始一个新事务,会造成一个隐含的 unlock tables 被执行。
  • 在同一个事务中,最好不使用不同存储引擎的表,否则 ROLLBACK 时需要对非事
    务类型的表进行特别的处理,因为 COMMIT、ROLLBACK 只能对事务类型的表进行提交和回滚。
  • 和 Oracle 的事务管理相同,所有的 DDL 语句是不能回滚的,并且部分的 DDL 语句会造成隐式的提交。
  • 在事务中可以通过定义 SAVEPOINT(例如:mysql> savepoint test; 定义 savepoint,名称为 test),指定回滚事务的一个部分,但是不能指定提交事务的一个部分。对于复杂的应用,可以定义多个不同的 SAVEPOINT,满足不同的条件时,回滚
    不同的 SAVEPOINT。需要注意的是,如果定义了相同名字的 SAVEPOINT,则后面定义的SAVEPOINT 会覆盖之前的定义。对于不再需要使用的 SAVEPOINT,可以通过 RELEASE SAVEPOINT 命令删除 SAVEPOINT, 删除后的 SAVEPOINT, 不能再执行 ROLLBACK TO SAVEPOINT命令。

自动提交(autocommit):
Mysql默认采用自动提交模式,可以通过设置autocommit变量来启用或禁用自动提交模式

  • 隐式锁定

InnoDB在事务执行过程中,使用两阶段锁协议:

随时都可以执行锁定,InnoDB会根据隔离级别在需要的时候自动加锁;

锁只有在执行commit或者rollback的时候才会释放,并且所有的锁都是在同一时刻被释放。

  • 显式锁定

InnoDB也支持通过特定的语句进行显示锁定(存储引擎层):

select ... lock in share mode //共享锁 
select ... for update //排他锁 

MySQL Server层的显示锁定:

lock tableunlock table
三、总结

本章首先数据库事务的4个特性,然后讲解了并发事务带来的问题,事务的隔离级别以及每种级别下所产生的问题,最后介绍了事务日志和事务的实现原理。

最后引用我很佩服的一个人经常说的话:你知道的越多,你不知道的越多!

文章参考:

https://zhuanlan.zhihu.com/p/29166694

https://mp.weixin.qq.com/s/J3kCOJwyv2nzvI0_X0tlnA

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值