什么是事务
事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。
事务的结束有两种,当事务中的所以步骤全部成功执行时,事务提交。如果其中一个步骤失败,将发生回滚操作,撤消撤消之前到事务开始时的所以操作。
事务的 ACID
1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏。比如 A 向 B 转账,不可能 A 扣了钱,B 却没收到。
3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,并发执行不同的事务之间彼此没有任何干扰。比如 A 正在从一张银行卡中取钱,在 A 取钱的过程结束前,B 不能向这张卡转账。
4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,其它操作或故障不应该对其执行结果有任何影响,不能回滚。
事务的并发问题
1、脏读:事务 A 读取了事务 B 更新的数据,然后 B 回滚操作,那么 A 读取到的数据是脏数据
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务 A 多次读取的过程中,对数据作了更新并提交,导致事务 A 多次读取同一数据时,结果 不一致。
3、幻读:系统管理员 A 将数据库中所有学生的成绩从具体分数改为 ABCDE 等级,但是系统管理员 B 就在这个时候插入了一条具体分数的记录,当系统管理员 A 改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
事务隔离级别
1、Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
2、Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是 MySQL 默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的 commit,所以同一 select 可能返回不同结果。
3、Repeatable Read(可重读)
这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读(Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影”行。InnoDB 和 Falcon 存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
4、Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
mysql 默认的事务隔离级别为 repeatable-read
各个隔离级别的情况
CREATE TABLE `account` (`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(11) DEFAULT '',
`balance` decimal(10,2) DEFAULT NULL ,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
1、读未提交
(1) 打开一个客户端 A,并设置当前事务模式为 read uncommitted(未提交读),查询表 account 的初始值:
(2)在客户端 A 的事务提交之前,打开另一个客户端 B,更新表 account:
(3)这时,虽然客户端 B 的事务还没提交,但是客户端 A 就可以查询到 B 已经更新的数据:
(4)一旦客户端 B 的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端 A 查询到的数据其实就是脏数据:
(5)在客户端 A 执行更新语句 update account set balance = balance – 50 where id =4,Lucy 的 balance 没有变成 200,居然是 250,是不是很奇怪,数据不一致啊,如果你这么想就太天真了,在应用程序中,我们会用 250-50=200,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别
2、读已提交
(1) 打开一个客户端 A,并设置当前事务模式为 read committed(已提交读),查询表 account 的所有记录:
(2)在客户端 A 的事务提交之前,打开另一个客户端 B,更新表 account:
(3)这时,客户端 B 的事务还没提交,客户端 A 不能查询到 B 已经更新的数据,解决了脏读问题:
(4)客户端 B 的事务提交
(5) 客户端 A 执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题
3、可重复读
(1) 打开一个客户端 A,并设置当前事务模式为 repeatable read,查询表 account 的所有记录
(2) 在客户端 A 的事务提交之前,打开另一个客户端 B,更新表 account 并提交
(3) 在客户端 A 查询表 account 的所有记录,与步骤(1)查询结果一致,没有出现不可重复读的问题
(4) 在客户端 A,接着执行 update balance = balance – 50 where id = 1,balance 没有变成 350-50=300,lucy 的 balance 值用的是步骤(2)中的 300 来算的,所以是 250,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了 MVCC 机制,select 操作不会更新版本号,是快照读(历史版本);insert、update 和 delete 会更新版本号,是当前读(当前版本)。
(5)重新打开客户端 B,插入一条新数据后提交
(6) 在客户端 A 查询表 account 的所有记录,没有 查出 新增数据,所以没有出现幻读
4、串行化
(1) 打开一个客户端 A,并设置当前事务模式为 serializable,查询表 account 的初始值:
(2)打开一个客户端 B,并设置当前事务模式为 serializable,插入一条记录报错,表被锁了插入失败,mysql 中事务隔离级别为 serializable 时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,开发中很少会用到。
https://www.cnblogs.com/huano…
https://www.fangzhipeng.com/j…