数据库的事务以及事务隔离级别
事务是什么
数据库事务是数据库管理系统执行过程中的一个逻辑单位,包含了一个有限的数据库操作序列。事务不是天然存在的东西,它是被人为创造出来,目的是简化应用层的编程模型。数据库事务有两个目的:
- 定义一个逻辑单位,以便于当数据库从故障中恢复的时候能正确地恢复数据
- 提供一种隔离方式,以便于解决多个客户端并发操作的时候出现的奇怪问题
ACID是什么
数据库系统在实现事务的时候必须满足4个条件:
简称 | 中文 | 说明 |
---|---|---|
Atomic | 原子性 | 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行 |
Consistent | 一致性 | 事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束 |
Isolated | 隔离性 | 多个事务并发执行时,一个事务的执行不应影响其他事务的执行 |
Durable | 持久性 | 已被提交的事务对数据库的修改应该永久保存在数据库中 |
这4个条件构成了常说的ACID模型。数据库系统在实现事务的时候要遵照ACID模型,也就是说数据库事务必须是:
- 原子的(它必须完整地完成或不起作用)
- 一致的(它必须符合数据库中现有的约束)
- 隔离的(它不能影响其他事务)
- 持久的(必须将其写入持久性存储)
事务的隔离级别是什么
在数据库系统中,隔离度决定了其他用户和系统如何看到事务完整性。较低的隔离级别可提高并发能力,但同时也会导致出现更多的并发问题(例如脏读或丢失更新);较高的隔离级别可以减少并发问题,但需要更多的系统资源,也会降低系统的并发能力。最高级别的隔离会导致性能低下,无法应对业务需求,因此也很少使用。
事务的隔离在实现的时候需要兼顾性能和应用层实际需求。针对四种可能出现的问题,定义了四种隔离级别:
Isolation Level | 隔离级别 | 脏读 | 更新丢失 | 不可重复读 | 幻读 |
---|---|---|---|---|---|
Read Uncommitted | 读未提交 | 可能发生 | 可能发生 | 可能发生 | 可能发生 |
Read Committed | 读已提交 | 不会发生 | 可能发生 | 可能发生 | 可能发生 |
Repeatable Read | 可重复读 | 不会发生 | 不会发生 | 不会发生 | 可能发生 |
Serializable | 可串行化 | 不会发生 | 不会发生 | 不会发生 | 不会发生 |
在四种隔离级别中, Serializable的级别最高, Read Uncommited级别最低。
- 大多数数据库的默认隔离级别为: Read Commited,如Sql Server, Oracle。
- 少数数据库默认的隔离级别为Repeatable Read, 如MySQL InnoDB存储引擎。
脏读
如下图所示,有 emails 和 mailboxes 两张表,emails 表中 unread_flag 字段表示是否已读,mailboxes 表中 unread 字段则是未读的email数量。
当用户1执行 insert 操作和 update 操作中间,用户2进行了读取,这时会获得一个矛盾的结果,明明有1封未读的 email,但是 unread 计数器却是0。
防止保证:读数据库的时候,只能看到已成功提交的数据。
更新丢失
如下图所示,用户1和用户2都分别尝试获取 counter,然后对 counter 进行加1并保存,理论结果应该是44,但是实际上结果为43。
防止保证:通过原子化写操作、加锁等方式。
脏写
Alice 和 Bob 同时购买一辆车,买车同时也会更新发票,下图的场景中会出现, Bob 买到了车,但是 Alice 收到了发票的情况。
注意,这种情况和 丢失更新 不一样,丢失更新中第二次写入是在第一个事务提交后才执行的。
防止保证:写数据库的时候,只能覆盖已经成功提交的数据。
不可重复读(读倾斜)
Alice有两个子账户,分别有500,一共有1000,转账者从账户2转了100到账户1,转账有两个SQL操作,两个操作在同一个事务中。如果 Alice 在同一个事务中读取了多次账户1,最初读取到了账户1是500,而后面会读取到600,两次读取数据不一致。
防止保证:读数据库的时候,只能看到已成功提交的数据。
幻读(写倾斜)
下图中的表是一个 oncall 排班系统,如果有两个医生 oncall 的情况下,允许一个医生退出 oncall。Alice 和 Bob 分别读取了 oncall 的人数,判断当前 oncall 的人数多于两人,然后分别退出了 oncall,导致这个时间段无人 oncall。
第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时 (此时第一事务还未提交) ,第二个事务向表中插入一行新数据。这时第一个事务再去读取表时,发现表中还有没有修改的数据行,就好象发生了幻觉一样。
防止保证:如果只有在事务完全提交后才能读取数据,则可以避免该问题。
事务隔离是如何实现的
不同的隔离级别采用不同的锁类开来实现。
可序列化的
有两种方式:
- 使用基于锁的并发控制实现,可串行性要求在事务结束时释放读写锁(在选定数据上获取)。此外,当SELECT查询使用带范围的WHERE子句时,必须获取范围锁,尤其是要避免幻像读取现象。
- 当使用基于非锁的并发控制时,不会获取任何锁。但是,如果系统在多个并发事务中检测到写冲突,则仅允许其中之一提交。这种方式称之为快照隔离。
重复读取
基于锁的并发控制实现将保持读写锁(在选定数据上获取),直到事务结束为止。但是,范围锁不受管理,因此可能发生幻像读取。
读已提交
基于锁的并发控制实现将写锁(在选定数据上获取)保持到事务结束,但是一旦执行SELECT操作,就会释放读锁(因此出现不可重复的读现象)。与重复读取一样,不管理范围锁。
阅读未提交
这是最低的隔离级别。在此级别,允许进行脏读取,因此一个事务可能会看到其他事务尚未提交的更改。
基于锁的并发控制是什么
锁分为两类:
- 悲观锁:先取锁再访问
- 乐观锁:多版本控制就是一种乐观锁实现
多版本控制详见【MySQL】MVCC(多版本并发控制)
MySQL里面的事务控制
关闭自动提交(session级别), 每次执行单个逻辑工作单元后,手动commit
提交/rollback
回滚
1 | set autocommit=0; |
使用begin
(或者start transaction
)显示开启一个事务,然后使用commit
提交,或者rollback
回滚
1 | begin; |