今天简单记录下关于数据库事务的理解,因为最近被面试高频问道,所以想着吃透它。
什么是事务
要去完全认识数据库事务,得先了解什么是事务
事务(Transaction)指一个操作,由多个步骤组成,要么全部成功,要么全部失败。
举一个简单的例子:比如转账功能,A账户需要给B账户转账,需要进行下面两步:
从 A 账户扣钱。
往 B 账户加入等量的钱。
因为是独立的两个操作,所以可能有一个成功,一个失败的情况。但是因为在这种场景下,不能存在从 A 账户扣钱成功,往 B 账户加入等量钱失败这种情况,要么同时成功,要么同时失败(一个失败需要回滚),即必须要保证事务。
数据库事务与四大特性
什么是数据库事务?
数据库事务(Database Transaction)是指对数据库的一系列操作组成的逻辑工作单元。
并非任意的数据库操作序列都是数据库事务。数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(Transaction)正确可靠,必须具备四个特性:ACID,也就是我们面试经常被问到的ACID,下面详细展开说下:
-
原子性(Atomicity)
事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。 -
一致性(Consistency)
一致性确保事务将数据库从一种一致的状态转换为另一种一致的状态。换句话说,事务在执行前后,数据库必须满足一些预定义的一致性规则,如约束、触发器、级联等。如果事务执行后数据库不满足这些规则,整个事务将被回滚。 -
隔离性(Isolation)
多个事务并发执行时,一个事务的执行不影响其他事务的执行。 -
持久性(Durability)
已被提交的事务对数据库的修改应该永久保存在数据库中。
还是用我们在第一个事务的举例来解释一下这四大特性吧,A账户要往B账户进行转账,我们可以分步进行分析,需要以下六个步骤:
1 从 A 账户中把余额读出来(500)
2 对 A 账户做减法操作(500-100)
3 把结果写回 A 账号中(400)
4 从 B账户中把余额读出来(500)
5 对 B 账户做加法操作(500+100)
6 把结果写回 B 账户中(600)
-
原子性:保证1-6所有过程要么都执行,要么都不执行。一旦在执行某一步骤的过程中发生问题,就需要执行回滚操作。 假如执行到第五步的时候,B账户突然不可用(比如被注销),那么之前的所有操作都应该回滚到执行事务之前的状态。
-
一致性:在转账之前,A和B的账户中共有500+500=1000元钱。在转账之后,A和B的账户中共有400+600=1000元。也就是说,数据的状态在执行该事务操作之后从一个状态改变到了另外一个状态,两个状态数据总额是一致的,不能凭空变多或变少。
-
隔离性:在 A 向 B 转账的整个过程中,只要事务还没有提交(commit),查询 A 账户和 B 账户的时候,两个账户里面的钱的数量都不会有变化。如果在 A 给 B 转账的同时,有另外一个事务执行了 C 给 B 转账的操作,那么当两个事务都结束的时候,B 账户里面的钱应该是 A 转给 B 的钱加上 C 转给 B 的钱再加上自己原有的钱。
-
持久性:一旦转账成功(事务提交),两个账户的里面的钱就会真的发生变化(会把数据写入数据库做持久化保存)。
我们会发现事务能够解决在这种诸如转账的类似场景,但是在实际业务中,很难保证事务,或者说去保证事务需要付出很大的一个成本,这时候就需要我们去设计出低成本又符合实际应用场景的方案。
那么MySql使用了什么技术去保证这个事务的四大特性呢?
持久性是通过 redo log (重做日志)来保证。
原子性是通过 undo log(回滚日志) 来保证。
隔离性是通过MVCC(多版本并发控制) 和锁机制来保证。
一致性则是通过持久性+原子性+隔离性来保证。
高并发下的事务
数据库如果被多个线程同时进行了访问,那么就会出现以下三个问题:
- 脏读(Dirty Read)
- 不可重复读(Non-repeatable Read)
- 幻读(Phantom Read)
这三个问题按严重级别来排序是这样的:
脏读>不可重复读>幻读
下面解释下这三个问题:
-
脏读
读取未提交数据。事务 A 读取事务 B 尚未提交的数据,此时如果事务 B 发生错误并回滚,那么事务 A 读取到的数据就是脏数据。 -
不可重复读
前后多次读取,数据内容不一致。事务 A 在事务 B 开始前读和事务 B 结束后读的数据不一样,因为数据被事务 B 修改了。 -
幻读
当同一个查询在不同时间产生不同的结果集时,称之为幻读。比如事务 A 在读取某个范围内的记录时,事务 B 在该范围内插入了新记录或删除了旧记录,事务 A 再次读取该范围内的记录时,前后获取的结果集不同,产生了幻读。
幻读比不可重复读取更难防范,因为锁定第一个查询结果集中的所有行并不能阻止导致幻像出现的更改。
这里的不可重复读和幻读我第一次接触的时候非常相似,感觉难以分清,我直接问了gpt:
也就是说不可重复读更关注修改,幻读关注条数的增加和删除
那为了解决这三个问题,顺势的就出现了事物的隔离级别,继续往下讲
事务的隔离级别
事务隔离有多个级别,每个隔离级别都有不同的特点和能力,以解决并发访问数据库时可能出现的不同问题。
SQL标准定义了四个隔离级别及其解决的问题,隔离级别越高,性能效率越低:
读未提交(Read Uncommitted)
读已提交(Read Committed)
可重复读(Repeatable Read)
串行化(Serializable)
隔离级别的高低是这样的:读未提交>读已提交>可重复读>串行化
下面我煎蛋画个图来解释下不同隔离级别解决了什么问题:
继续往下举例子解释四个隔离级别,不然太干了:
- 读未提交
允许脏读、不可重复读和幻读。
最低的隔离级别,事务可以读取其他事务尚未提交的数据,虽然拥有超高的并发处理能力及很低的系统开销,但很少用于实际应用,因为可能导致数据不一致性。
时间顺序 | 转账的事务 | 取款的事务 |
---|---|---|
1 | 开始事务 | |
2 | 开始事务 | |
3 | 查询目前银行卡余额是2000元 | |
4 | 取走1000元,把余额改为1000元(2000-1000) | |
5 | 查询到余额为1000元(这里其实就是脏数据,因为另一个取款事务还没有提交) | |
6 | 此时银行取款业务崩溃,事务回滚,余额回到2000 | |
7 | 往银行卡转入2000元,此时余额为3000元(1000+2000) | |
8 | 提交事务 | |
备注 | 我们其实知道这边按照正常逻辑,银行卡余额应该还剩4000元,就是因为脏读所导致的数据余额不正确。 |
- 读已提交
允许不可重复读和幻读。事务只能读取已经提交的数据,避免了脏读问题,但可能导致不可重复读和幻读。
这是大多数数据库系统的默认隔离级别(如 Oracle 和 SQL Server),但不是 MySQL 的默认
时间顺序 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | |
2 | 查询小王的年龄是20 | |
3 | 开始事务 | |
4 | 做一些其他的操作 | |
事务B把小王的年龄修改成30岁 | ||
6 | 提交事务 | |
7 | 再次查询,发现小王的年龄是30 | |
备注 | 我们这里按照正常的逻辑小王的年龄应该还是20,两次读到了不一样的数据。 |
- 可重复读
这个只允许幻读。事务在整个事务期间保持一致的快照,其他事务的修改不会影响正在运行的事务,从而防止不可重复读问题。
这是 MySQL InnoDB 默认的事务隔离级别。
时间顺序 | 事务A | 事务B |
---|---|---|
1 | 开始事务 | |
2 | 查询数据总数,查出来一百条 | |
3 | 开始事务 | |
4 | 做一些其他的操作 | |
5 | 新增一百条数据 | |
6 | 提交事务 | |
7 | 再次查询,发现数据的总量变成了两百条 | |
备注 | 我们这里按照正常的逻辑数据的总量还是一百条,此时出现了幻读 |
- 串行化
解决所有事务并发问题。
最高的隔离级别,通过强制事务排序,使之不可能相互冲突,从而防止所有并发问题。
虽然这个隔离级别可以解决上面提到的所有并发问题,由于事务是串行执行,所以效率会大大下降,应用程序的性能会急剧降低。最直观的体现就是,当数据库隔离级别设置为串行化后,A 事务在未提交之前,B 事务对 A 事务数据的操作都会被阻塞。通常不会使用这个隔离级别,我们需要其他机制来解决这些问题:比如乐观锁和悲观锁。
下面列一个表格简单总结下:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | √ | √ | √ |
读已提交 | √ | √ | |
可重复读 | √ | ||
串行化 |
每个隔离级别都在一定程度上解决了事务并发问题,但隔离级别越高,并发性能越低,因为更高级别的隔离通常需要更多的锁和资源开销。因此,在选择隔离级别时,需要根据应用场景平衡一致性和性能,选择合适的隔离级别。
篇幅有限,下一篇展开记录MVCC