事务特点:ACID
从业务角度出发,对数据库的一组操作要求保持 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 等实现 事务的原子性、一致性和持久性则是通过事务日志实现(见下)
事务的隔离级别
并发事务带来的问题
更新丢失(Lost Update):当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题 --最后的更新覆盖了由其他事务所做的更新。例如,两个编辑人员制作了同一 文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。 最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同 一文件,则可避免此问题。 脏读(Dirty Reads):一个事务正在对一条记录做修改,在这个事务完成并提交前, 这条记录的数据就处于不一致状态; 这时, 另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些 “脏” 数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做” 脏读”。 不可重复读(Non-Repeatable Reads):一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做 “不可重复读” 。 幻读 (Phantom Reads): 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为 “幻读” 。
幻读和不可重复读的区别:
不可重复读的重点是修改:在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样。(因为中间有其他事务提交了修改) 幻读的重点在于新增或者删除:在同一事务中,同样的条件,,第一次和第二次读出来的记录数不一样。(因为中间有其他事务提交了插入 / 删除)
并发事务处理带来的问题的解决办法:
“更新丢失” 通常是应该完全避免的。但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,因此,防止更新丢失应该是应用的责任。 “脏读” 、 “不可重复读” 和 “幻读” ,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决: 一种是加锁:在读取数据前,对其加锁,阻止其他事务对数据进行修改。 另一种是数据多版本并发控制(MultiVersion Concurrency Control,简称 MVCC 或 MCC),也称为多版本数据库:不用加任何锁, 通过一定机制生成一个数据请求时间点的一致性数据快照 (Snapshot), 并用这个快照来提供一定级别 (语句级或事务级) 的一致性读取。从用户的角度来看,好象是数据库可以提供同一数据的多个版本。
SQL 标准定义了 4 类隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
第 1 级别:Read Uncommitted (读取未提交内容)
所有事务都可以看到其他未提交事务的执行结果 本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少 该级别引发的问题是 —— 脏读 (Dirty Read):读取到了未提交的数据
第 2 级别:Read Committed (读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的) 它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变 这种隔离级别出现的问题是 —— 不可重复读 (Nonrepeatable Read):不可重复读意味着我们在同一个事务中执行完全相同的 select 语句时可能看到不一样的结果。导致这种情况的原因可能有: 有一个交叉的事务有新的 commit,导致了数据的改变; 一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的 commit。
第 3 级别:Repeatable Read (可重读)
这是 MySQL 的默认事务隔离级别 它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行 此级别可能出现的问题 —— 幻读 (Phantom Read):当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的 “幻影” 行 InnoDB 和 Falcon 存储引擎通过多版本并发控制 (MVCC,Multiversion Concurrency Control) 机制解决幻读问题;InnoDB 还通过间隙锁解决幻读问题
多版本并发控制 :
Mysql 的大多数事务型存储引擎实现都不是简单的行级锁。基于提升并发性考虑,一般都同时实现了多版本并发控制(MVCC),包括 Oracle、PostgreSQL。不过实现各不相同。
MVCC 的实现是通过保存数据在某一个时间点快照来实现的。也就是说不管实现时间多长,每个事物看到的数据都是一致的。
分为乐观(optimistic)并发控制和悲观(pressimistic)并发控制。
MVCC 是如何工作的:
InnoDB 的 MVCC 是通过在每行记录后面保存两个隐藏的列来实现。这两个列一个保存了行的创建时间,一个保存行的过期时间(删除时间)。当然存储的并不是真实的时间而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动新增。事务开始时刻的系统版本号会作为事务的版本号,用来查询到每行记录的版本号进行比较。
REPEATABLE READ(可重读)隔离级别下 MVCC 如何工作:
InnoDB 会根据以下条件检查每一行记录:
InnoDB 只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行要么是在开始事务之前已经存在要么是事务自身插入或者修改过的 行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除
只有符合上述两个条件的才会被查询出来
InnoDB 为新插入的每一行保存当前系统版本号作为行版本号
InnoDB 为删除的每一行保存当前系统版本号作为行删除标识
InnoDB 为插入的一行新纪录保存当前系统版本号作为行版本号,同时保存当前系统版本号到原来的行作为删除标识
保存这两个版本号,使大多数操作都不用加锁。使数据操作简单,性能很好,并且能保证只会读取到复合要求的行。不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作和一些额外的维护工作。
MVCC 只在 COMMITTED READ(读提交)和 REPEATABLE READ(可重复读)两种隔离级别下工作。
可以认为 MVCC 是行级锁一个变种,但是他很多情况下避免了加锁操作,开销更低。虽然不同数据库的实现机制有所不同,但大都实现了非阻塞的读操作(读不用加锁,且能避免出现不可重复读和幻读),写操作也只锁定必要的行(写必须加锁,否则不同事务并发写会导致数据不一致)。
第 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;
事务日志
事务日志可以帮助提高事务效率:
使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不用每次都将修改的数据本身持久到磁盘。 事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序 I/O,而不像随机 I/O 需要在磁盘的多个地方移动磁头,所以采用事务日志的方式相对来说要快得多。 事务日志持久以后,内存中被修改的数据在后台可以慢慢刷回到磁盘。 如果数据的修改已经记录到事务日志并持久化,但数据本身没有写回到磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这一部分修改的数据。
目前来说,大多数存储引擎都是这样实现的,我们通常称之为预写式日志(Write-Ahead Logging),修改数据需要写两次磁盘。
还有MySQL慢日志记录视频介绍 ,可点击查看。
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
start transaction; 记录 A=1 到 undo log; update A = 3; 记录 A=3 到 redo log; 记录 B=2 到 undo log; update B = 4; 记录 B = 4 到 redo log; 将 redo log 刷新到磁盘 commit
在 1-8 的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响。如果在 8-9 之间宕机,恢复之后可以选择回滚,也可以选择继续完成事务提交,因为此时 redo log 已经持久化。若在 9 之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据 redo log 把数据刷回磁盘。
所以,redo log 其实保障的是事务的持久性和一致性,而 undo log 则保障了事务的原子性。
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 table和unlock table
MySQL事务处理的两种方法可见《MySQL 事务 》
MySQL 对分布式事务的支持
分布式事务的实现方式有很多,既可以采用 innoDB 提供的原生的事务支持,也可以采用消息队列来实现分布式事务的最终一致性。这里我们主要聊一下 innoDB 对分布式事务的支持。
MySQL 从 5.0.3 开始支持分布式事务,当前分布式事务只支持 InnoDB 存储引擎。一个分布式事务会涉及多个行动,这些行动本身是事务性的。所有行动都必须一起成功完成,或者一起被回滚。
如图,mysql 的分布式事务模型。模型中分三块:应用程序(AP)、资源管理器(RM)、事务管理器(TM):
应用程序:定义了事务的边界,指定需要做哪些事务; 资源管理器:提供了访问事务的方法,通常一个数据库就是一个资源管理器; 事务管理器:协调参与了全局事务中的各个事务。
分布式事务采用两段式提交(two-phase commit)的方式:
第一阶段所有的事务节点开始准备,告诉事务管理器 ready。 第二阶段事务管理器告诉每个节点是 commit 还是 rollback。如果有一个节点失败,就需要全局的节点全部 rollback,以此保障事务的原子性。
分布式事务(XA 事务)的 SQL 语法主要包括:
XA {START|BEGIN} xid [JOIN|RESUME]
虽然 MySQL 支持分布式事务,但是在测试过程中,还是发现存在一些问题: 如果分支事务在达到 prepare 状态时,数据库异常重新启动,服务器重新启动以后,可以继续对分支事务进行提交或者回滚得操作,但是提交的事务没有写 binlog,存在一定的隐患,可能导致使用 binlog 恢复丢失部分数据。如果存在复制的数据库,则有可能导致主从数据库的数据不一致。
如果分支事务在执行到 prepare 状态时,数据库异常,且不能再正常启动,需要使用备份和 binlog 来恢复数据,那么那些在 prepare 状态的分支事务因为并没有记录到 binlog,所以不能通过 binlog 进行恢复,在数据库恢复后,将丢失这部分的数据。
如果分支事务的客户端连接异常中止,那么数据库会自动回滚未完成的分支事务,如果此时分支事务已经执行到 prepare 状态, 那么这个分布式事务的其他分支可能已经成功提交,如果这个分支回滚,可能导致分布式事务的不完整,丢失部分分支事务的内容。 总之, MySQL 的分布式事务还存在比较严重的缺陷, 在数据库或者应用异常的情况下, 可能会导致分布式事务的不完整。如果应用对于数据的完整性要求不是很高,则可以考虑使 用。如果应用对事务的完整性有比较高的要求,那么对于当前的版本,则不推荐使用分布式事务。
评论区(0)