前言:复习并总结数据库事务
事务概念
事务(transaction)
数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。—— 维基百科
- 事务就是要保证一组数据库操作,要么全部成功,要么全部失败
- 在 MySQL 中,事务支持是在引擎层实现的。
- 在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务务须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。
事务特性
事务具有4个特性,ACID
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔离性(Isolation)
- 持久性(Durability)
原子性
一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性
一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
持久性
事务处理结束后,对数据的修改就是永久的,需要保存到磁盘中,即便系统故障也不会丢失。
事务隔离
没有事务隔壁会产生的问题
如果事务不考虑隔离性,可能会引发如下问题:
1、脏读
脏读(dirty read),简单来说,就是一个事务在处理过程中读取了另外一个事务未提交的数据。
这种未提交的数据我们称之为脏数据。依据脏数据所做的操作肯能是不正确的。
2、不可重复读
不可重复读指在一个事务内读取表中的某一行数据,多次读取结果不同。
例如银行想查询A帐户余额,第一次查询A帐户为200元,此时A向帐户内存了100元并提交了,银行接着又进行了一次查询,此时A帐户为300元了。银行两次查询不一致,可能就会很困惑,不知道哪次查询是准的。
不可重复读和脏读的区别是,脏读是读取前一事务未提交的脏数据,不可重复读是重新读取了前一事务已提交的数据。
很多人认为这种情况就对了,无须困惑,当然是后面的为准。
我们可以考虑这样一种情况,比如银行程序需要将查询结果分别输出到电脑屏幕和写到文件中,结果在一个事务中针对输出的目的地,进行的两次查询不一致,导致文件和屏幕中的结果不一致,银行工作人员就不知道以哪个为准了。
3、虚读(幻读)
虚读(幻读)是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。
隔离级别
- 读未提交(Read Uncommitted):最低级别,以上情况均无法保证。
- 读提交(Read Committed):可避免脏读情况发生。
- 可重复读(Repeated Read):可避免脏读、不可重复读情况的发生。
- 串行化(Serializable):可避免脏读、不可重复读、虚读情况的发生。、
串行化(Serializable)
花费最高代价但最可靠的事务隔离级别。
“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
事务 100% 隔离,可避免脏读、不可重复读、幻读的发生。
可重复读(Repeatable read)
mysql默认级别
多次读取同一范围的数据会返回第一次查询的快照,即使其他事务对该数据做了更新修改。事务在执行期间看到的数据前后必须是一致的。
但如果这个事务在读取某个范围内的记录时,其他事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,这就是幻读。
可避免脏读、不可重复读的发生。但是可能会出现幻读。
读已提交 (Read committed)
保证一个事物提交后才能被另外一个事务读取。另外一个事务不能读取该事物未提交的数据。
可避免脏读的发生,但是可能会造成不可重复读。
大多数数据库的默认级别就是 Read committed,比如 Sql Server , Oracle。
读未提交(Read uncommitted )
最低的事务隔离级别,一个事务还没提交时,它做的变更就能被别的事务看到。
任何情况都无法保证
mysql操作事务命令
查询数据库当前事务隔离级别
1 | select @@tx_isolation |
mysql数据库默认的事务隔离级别是:Repeatable read(可重复读)
设置数据库当前事务隔离级别
1 | set transaction isolation level 隔离级别名 |
数据库脚本
建表并插入数据,用于下面测试
1 | /*创建账户表*/ |
开启事务
使用start transaction或者begin开始事务
1 | start transaction; |
不提交事务,同时打开另外一个窗口,进行查询,可以看到避免了脏读,没有读到0即未提交事务的数据
回滚事务
rollback或rollback work
1 | select * from account where name='A'; |
数据恢复
提交事务
commit或commit work提交事务
1 | select * from account where name='A'; |
自动提交
1 | // 禁止自动提交 |
如何保证持久性
隔离性的问题解决了,但是如果在事务提交后,事务的数据还没有真正落到磁盘上,此时数据库奔溃了,事务对应的数据会不会丢?
事务会保证数据不会丢,当数据库因不可抗拒的原因奔溃后重启,它会保证:
- 成功提交的事务,数据会保存到磁盘
- 未提交的事务,相应的数据会回滚
事务日志
数据库通过事务日志来达到这个目标。 事务的每一个操作(增/删/改)产生一条日志,内容组成大概如下:
- LSN:一个按时间顺序分配的唯一日志序列号,靠后的操作的LSN比靠前的大。
- TransID:产生操作的事务ID。
- PageID:被修改的数据在磁盘上的位置,数据以页为单位存储。
- PrevLSN:同一个事务产生的上一条日志记录的指针。
- UNDO:取消本次操作的方法,按照此方法回滚。
- REDO:重复本次操作的方法,如有必要,重复此方法保证操作成功。
磁盘上每个页(保存数据的,不是保存日志的)都记录着最后一个修改该数据操作的LSN。数据库会通过解析事务日志,将修改真正落到磁盘上(写盘),随后清理事务日志(正常情况下)。
这也是数据库在保证数据安全和性能这两个点之前的折中办法:
- 如果每次更新都写盘,由于数据是随机的,会造成大量的随机IO,性能会非常差
- 如果每次更新不马上写盘,那一旦数据库崩溃,数据就会丢失
折中的办法就是:
- 将数据的变更以事务日志的方式,按照时间先后追加到日志缓冲区,由特定算法写入事务日志,这是顺序IO,性能较好
- 通过数据管理器解析事务日志,由特定的算法择机进行写盘
事务原理
事务由InnoDB存储引擎实现
非常复杂,本次笔记不谈
- MVCC:多版本并发控制
- Redo log:重写日志
- Undo log:撤销日志
事务的机制
事务的机制是通过视图(read-view)来实现的并发版本控制(MVCC),不同的事务隔离级别创建读视图的时间点不同。
- 可重复读是每个事务重建读视图,整个事务存在期间都用这个视图。
- 读已提交是每条 SQL 创建读视图,在每个 SQL 语句开始执行的时候创建的。隔离作用域仅限该条 SQL 语句。
- 读未提交是不创建,直接返回记录上的最新值
- 串行化隔离级别下直接用加锁的方式来避免并行访问。
这里的视图可以理解为数据副本,每次创建视图时,将当前已持久化的数据创建副本,后续直接从副本读取,从而达到数据隔离效果。
隔离级别的实现
我们每一次的修改操作,并不是直接对行数据进行操作。
比如我们设置 id 为 3 的行的 A 属性为 10,并不是直接修改表中的数据,而是新加一行。
同时数据表其实还有一些隐藏的属性,比如每一行的事务 id,所以每一行数据可能会有多个版本,每一个修改过它的事务都会有一行,并且还会有关联的 undo 日志,表示这个操作原来的数据是什么,可以用它做回滚。
那么为什么要这么做?
因为如果我们直接把数据修改了,那么其他事务就用不了原先的值了,违反了事务的一致性。
那么一个事务读取某一行的数据到底返回什么结果呢?
取决于隔离级别,如果是 Read Committed,那么返回的是最新的事务的提交值,所以未提交的事务修改的值是不会读到的,这就是 Read Committed 实现的原理。
如果是 Read Repeatable 级别,那么只能返回发起时间比当前事务早的事务的提交值,和比当前事务晚的删除事务删除的值。这其实就是 MVCC 方式。
undo log
undo log 中存储的是老版本数据。假设修改表中 id=2 的行数据,把 Name=’B’ 修改为 Name = ‘B2’ ,那么 undo 日志就会用来存放 Name=’B’ 的记录,如果这个修改出现异常,可以使用 undo 日志来实现回滚操作,保证事务的一致性。
当一个旧的事务需要读取数据时,为了能读取到老版本的数据,需要顺着 undo 链找到满足其可见性的记录。当版本链很长时,通常可以认为这是个比较耗时的操作。