系列文章目录
一、MySQL数据结构选择
二、MySQL性能优化explain关键字详解
三、MySQL索引优化
四、MySQL事务
五、MySQL锁机制
六、MySQL多版本并发(MVCC)机制
文章目录
一、MySQL事务特性
1.1、什么是事务
简单来说,事务是指对于一组操作的集合,这些操作要么全部执行,要么全部不执行,确保数据的一致性和完整性。事务的核心目标是保证在多个操作过程中,即使发生故障,数据也能保持一致的状态。
事务并非是数据库特有的概念,操作系统
中也有事务的概念,比如文件系统中的原子操作,要么执行完毕,要么不执行。以及在在分布式系统
中,事务用于保证跨多个系统或服务的操作的一致性。
1.2、事务的特性
在数据库中,事务是为了保证数据在多用户环境下的可靠性、完整性和一致性。数据库事务通常有以下四个特性,简称 ACID 特性:
- 原子性(Atomicity):事务是一个“原子”操作,要么全都成功,要么全都失败,不会有中间状态。即使系统发生崩溃或其他错误,事务中的所有操作也不会被部分提交。
- 一致性(Consistency):事务执行前后,数据库的状态是一致的。事务的执行必须保持数据库的一致性约束(例如数据的完整性和规则)。
- 隔离性(Isolation):事务的执行应该是隔离的,即使多个事务并发执行,每个事务执行时都不应受到其他事务影响,其他事务的数据变动对当前事务是不可见的,直到当前事务完成。
- 持久性(Durability):事务一旦提交,其修改的数据将永久保存在数据库中,即使系统发生崩溃或停机,事务的结果也不会丢失。
1.3、MySQL如何保证事务特性
对于原子性,MySQL通过undolog
实现,undolog
中记录的是操作的历史,比如在A事务中,首先执行了一条insert语句,对应地在undolog
中,就会记录对应的delete语句(反向sql),将来事务需要回滚时,就执行undolog
中的该条sql。
对于一致性,MySQL每次事务执行前后会检查数据库约束(如唯一性约束、外键约束等)是否成立。如果执行的操作违反了数据库的约束,MySQL 会回滚该事务。
对于隔离性,MySQL通过MVCC
机制,锁
,以及隔离级别
进行控制。
对于持久性,MySQL通过Redo Log
来保证数据的持久性。每当事务提交时,InnoDB 会将事务的修改记录到Redo Log
文件中。即使发生系统崩溃,在恢复过程中,InnoDB 可以通过重做日志恢复事务提交的操作。
图片来源:图灵学院
二、MySQL隔离级别
MySQL隔离级别有以下四种,按从低到高的顺序,隔离级别越高,粒度越细,相应的并发度越低。
- Read Uncommitted(读未提交):一个事务可以读取其他事务未提交的数据,可能会导致“脏读”。
- Read Committed(读已提交-不可重复读):一个事务只能读取其他事务已提交的数据,避免了“脏读”。
- Repeatable Read(可重复读):在事务开始时,读取的数据在整个事务期间都不会变化,防止了“不可重复读”。
- Serializable(串行化):最严格的隔离级别,事务之间完全串行执行,避免了所有并发问题,但会导致性能下降。
下面用一个案例演示一下四种隔离级别:
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('lilei', '450');
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('hanmei', '16000');
INSERT INTO `test`.`account` (`name`, `balance`) VALUES ('lucy', '2400');
表的初始状态:
2.1、读未提交
将事务A和B的隔离级别设置为read‐uncommitted
,注意,在MySQL 5.7版本以上,用SET transaction_isolation = 'READ-UNCOMMITTED';
A和B都开启一个事务,在A中对表中的数据进行查询,不提交事务:
在B中对id为1的数据进行修改,也不提交事务:
回到A中再次执行查询语句,会发现读取到了B事务没有提交的数据
如果此时B进行了回滚:
A对于balance - 50 :
然后进行查询,发现最终的结果是400,而不是上一步A查询到的4500-50 = 4450。
而在应用程序中,A是不知道B有没有回滚的。
2.2、读已提交-不可重复读
将A和B的隔离级别设置为'READ-COMMITTED'
:
同样地A和B各自开启事务,然后A先查询余额:
B对余额增加50,不提交事务:
A查询余额,
读取不到B事务未提交的数据
:
B提交事务:
A再次进行查询:
发现两次读取到的数据不一致。即产生了不可重复读
的问题,但是同时也解决了脏读
的问题。
2.3、可重复读
将事务A和事务B的级别设置为'REPEATABLE-READ'
:
A和B开启事务,A进行查询,不提交事务:
B执行更新语句,并且提交事务:
回到A去查询,在读已提交-不可重复读
的隔离级别下,当B提交了事务后,A读取到的应该是B提交的最新的结果,但是在可重复读
的隔离级别下,A事务没提交之前,读取到的依旧是A第一次查询时
的结果,而不是其他事务更新并提交
后的结果:
如果此时再开另一个C事务,执行完更新语句然后提交:
A读取到的依旧是最初的值:
这里使用到了
MVCC
机制,select
操作是快照读(历史版本);insert、update和delete
是当前读(当前版本)。
如果此时B事务再次开启,并且插入一条新的记录
,提交事务:
在其他的事务中是能查到新增的记录的,同时看到了
id为1的balance已经加到了600
:
但是在A中依旧查询不到,解决了读未提交的问题。
在这个时候,在A中对于id为1的数据进行
修改
操作,这时候再去查询,该条数据就会查询到其他事务的提交结果 + A事务修改的结果:
小结:上面三种隔离级别的区别:
在读未提交中,A事务可以查询到其他事务未提交的修改。(脏读,幻读)
在读已提交中,A事务只能查询到其他事务已经提交的修改。(不可重复读)
在可重复读中,无论其他事务提交了多少次修改,A事务只会查询到第一次读取的数据(解决了脏读,幻读,不可重复读)。在A事务对该数据进行修改后,方可读取到其他事务的提交结果 + A事务修改的结果。(select
操作是快照读(历史版本);insert、update和delete
是当前读(当前版本))
2.4、串行化
将事务A和事务B的级别设置为'SERIALIZABLE'
:
A和B开启事务,A进行查询操作,不提交事务:
此时B进行更新操作,却发现处于
阻塞状态
:(如果B进行读取操作,则不会阻塞,涉及到读写锁
互斥的概念)
只有A提交了事务后,B才会解除阻塞。
总结
总结一下几个概念,以及四种隔离级别的关系,解决的问题:
脏读
是指一个事务读取了另一个事务尚未提交的数据。如果另一个事务回滚了,这个“脏”数据就不再有效,但第一个事务已经读取了它,导致数据的不一致。幻读
是指在一个事务中,两次查询结果不一致,即另一个事务在第一个查询后插入、删除或修改了符合查询条件的数据,导致第二次查询结果与第一次不相同。
隔离级别 | 允许的现象 | 描述 |
---|---|---|
读未提交(Read Uncommitted) | 脏读、不可重复读、幻读 | 事务可以读取其他事务未提交的数据,这导致可能会出现脏读。最低的隔离级别,性能较好,但可能会产生数据不一致的情况。 |
读已提交(Read Committed) | 不允许脏读,允许不可重复读、幻读 | 事务只能读取其他事务已经提交的数据,避免了脏读,但仍然允许不可重复读和幻读。 |
可重复读(Repeatable Read) | 不允许脏读、不允许不可重复读,允许幻读 | 事务在整个过程中读取到的数据不会改变,避免了脏读和不可重复读,但仍然可能会发生幻读。 |
串行化(Serializable) | 不允许脏读、不可重复读、幻读 | 最严格的隔离级别,事务按照串行顺序执行,避免了所有的并发问题,但性能最差。 |