概述
为解决数据库的多事务并发问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,旨在用这一整套机制来解决多事务并发可能出现的脏读、脏写、不可重复读、幻读等问题。下面就事务的隔离机制进行总结。
事务的定义及其特性
-
定义:事务是由一组SQL语句组成的逻辑处理单元,具有ACID四大特性。
-
ACID特性
- 原子性(Atomicity):事务是一个原子操作单元,其对数据的修改操作,要么都执行,要么都不执行;
- 一致性(Consistent):在事务开始和完成时,数据都必须保持一直状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性。
- 隔离性(Isolation):数据库系统提供一定的隔离机制,保证单个事务不受外部并发操作影响的“单独”环境中执行。这意味着事务处理过程中的中间状态对外部是不可见的。
- 持久性(Durable):事务完成之后,它对于数据的修改是永久性的,即使系统出现故障也能够保持。
-
并发事务处理可能出现的问题
- 脏写:当两个以上的事务对同一行的数据进行更新时,由于每个事务都不知道其他事务的存在,就会发生某一个更新操作丢失的问题,即最后更新的操作覆盖了其他事务所做的更新操作。
- 脏读:一个事务正在对一条记录进行修改,在该事务提交之前,当前记录的状态是不一致的,此时如果有另一个事务也来读取同一条记录,第二个事务便读取了这些“脏”数据,并根据此前读到的数据进行下一步操作,第二个事务便会与未提交事务的数据产生依赖关系。这种现象就叫做脏读。即事务A读取了事务B已经修改但未提交的数据,还在此基础上进行了其他操作,如果此时事务B进行回滚,那么事务A所读取的事务即为无效,不符合一致性的要求。
- 不可重复读:一个事务在读取某些数据后的某个时间,再次读取了以前读过的数据,却发现其读出的数据已经发生了改变,或者这些数据已经被删除了。这种现象就叫做不可重复读。即事务A内部的相同查询语句在不同时间读出了不一样的结果,不符合隔离性。
- 幻读:一个事务按照相同的查询条件重新读取以前读过的数据,却发现其他事务插入了满足查询条件的新数据,这种现象就称为幻读。即事务A读取到了事务B提交的新增数据,不符合隔离性。
事务隔离级别
脏读、不可重复读、幻读,这三种现象其实都是数据库读一致性的问题,要通过数据库的事务隔离机制来解决。事务的隔离级别有四种,分别是:读未提交,读已提交,可重复读,串行化。
- 数据库的事务隔离机制越严格,“串行化”程度就会越高,也就是说在减小并发的副作用的同时,应对并发的能力也随之下降。
- 查看当前数据库的事务隔离级别:
- (5.x版本)show variables like 'tx_isolation'; 、
- (8.x版本)show variables like '%transaction_isolation%';
- 设置事务隔离级别:
- (5.x版本)set tx_isolation='REPEATABLE-READ';
- (8.x版本)set session transaction isolation level read uncommitted;
事务隔离级别案例分析
CREATE TABLE `goods` (
`id` bigint NOT NULL,
`name` varchar(64) DEFAULT NULL,
`price` int DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `goods` VALUES (1, 'apple', 100);
INSERT INTO `goods` VALUES (2, 'banana', 200);
INSERT INTO `goods` VALUES (3, 'orange', 300);
读未提交
- 打开客户端A,设置当前的事务隔离级别为read uncommitted(读未提交),开启一个事务,查询goods表的初始值:
2.打开另一个客户端B,在客户端A的事务提交之前,客户端B开启一个事务,更新goods表的数据:
3.此时客户端B的事务还没提交,但是客户端A就可以查询到客户端B已经更新的数据:
4.如果此时客户端B的事务进行回滚,那么在事务的所有操作都会撤销,则客户端A查询到的数据即为脏数据,此时则出现了脏读;
5.在客户端A执行更新语句update godos set price = price-10 where id=1,apple的price的结果却是90,并不是80。这是因为update语句是当数据库当前值100去执行减10的操作,结果当然就是90了。但是,如果这是在应用程序中,犹豫并不知道其他事务进行了回滚,我们用回90-10=80。要想避免这个问题,就需要把隔离级别提升为读已提交。
读已提交
1.打开客户端A,设置当前的事务隔离级别为read committed(读已提交),开启一个事务,查询goods表的初始值:
2.在客户端A提交之前,打开另一个客户端B,更新goods表:
3.此时,客户端B的事务还没提交,客户端A查询不到客户端B已经更新的数据,解决了脏读的问题。
4.这时候再把客户端B的事务提交,数据库的记录会根据客户端B操作进行更新。
5.再看客户端A同样的查询语句读取到的数据,为客户端B提交事务后的数据,两次查询得到的数据是不一样的,出现了不可重复读的问题。要想解决不可重复读的问题,需要把隔离级别提升为可重复读。
可重复读
1.打开客户端A,设置当前的事务隔离级别为repeatable read(可重复读),开启一个事务,查询goods表的初始值:
2.在客户端A提交之前,打开另一个客户端B,更新goods表并提交:
3.在客户端A再次用相同的查询语句查看goods表的数据,与步骤1的结果是一致的,没有出现不可重复读的问题:
4.在客户端A,执行update goods set price = price-10 where id=1;price没有编程80,apple的price的值是按步骤2执行之后的结果来算的,所以是80-10=70,数据的一致性没有被破坏。
5.重新打开客户端B,开启事务,插入一条数据并提交:
6.在客户端A中查询goods表的所有记录,没有查询出新增的数据,没有出现幻读
7.验证幻读:在客户端A中执行update goods set price=999 where id=4,再次查询可以查到客户端B新增的数据
串行化(并发性极地,很少会用到)
1.打开客户端A,设置当前的事务隔离级别为serializable(串行化),开启一个事务,查询goods表id=1的初始值:
2.打开客户端B,设置当前的事务隔离级别为serializable(串行化),更新id=1的记录会被阻塞等待,更新其他id的记录则可以成功,这说明串行化隔离级别下,Innodb的查询操作也会加上行锁。如果查询的是一个范围,那该范围的所有行以及间隙区间范围都会被加锁,此种锁为间隙锁。在客户端B在被锁范围内进行写操作都会被阻塞,从而避免出现了幻读。
结语
Mysql事务的隔离机制是为了解决多事务并发场景下可能出现的脏读、不可重复读、幻读问题的,为适应不同程度的并发场景以及保证数据一致性的前提下,设置了四个隔离级别。隔离级别越严格,数据一致性的可靠性就越大,但其应对并发的能力就越小,在使用数据库的时候可根据业务场景的偏向性以及并发量来选择隔离级别。最后,补充一点,Mysql的事务隔离是通过锁机制以及MVCC机制来实现的,本文暂不展开叙述,这也是Innodb执行引擎很重要的部分,建议感兴趣的朋友去深入了解。