InnoDB引擎与其他mysql引擎最大的区别是其引入了事务的概念,使用事务可以保证
原子性,持久性,隔离性的目的也是为了保障数据的一致性!
**脏读:**读到其他事务未提交的数据。
不可重复读: 读到其他事务已经提交的事务,导致一次事务中,两次查询同一条记录不一致。
幻读: 幻读,并不是说两次读取获取的结果集不同,幻读侧重的方面是某一次的 select 操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些:select 某记录是否存在,不存在,准备插入此记录,但执行 insert 时发现此记录已存在,无法插入,此时就发生了幻读。
如果看过笔者的《mysql专栏》undo log页 会知道InnoDB使用undo log来实现事务,当每一个事务操作数据的时候 会在undo log的链表中生成修改之前的数据记录,除了记录数据,其会记录两个隐藏列
DB_ROW_ID
:包含一个行ID,该ID在插入新行时会单调增。下面以多个事务更新同一条数据为例,看看undo log是如何工作的
//插入并提交事务
INSERT INTO USER VALUES (1, 'tony',20);
//事务A更新数据
UPDATE user SET name='jack' WHERE id=1;
//事务B更新数据
UPDATE user SET name='tom' WHERE id=1;
//事务C更新数据
UPDATE user SET name='blob' WHERE id=1;
上述三个事务执行后 undo log的链表如下:
InnoDB支持MVCC多版本,其中RC(Read Committed)和RR(Repeatable Read)隔离级别是利用consistent read view(一致读视图)方式支持的。 所谓consistent read view就是在某一时刻给事务系统trx_sys打snapshot(快照)。ReadView 其实就是一个保存事务ID的list列表,其中包含如下主要部分:
前三个属性将事务列表分成如下三个区间
通过比较当前每次readView时候创建的事务creator_trx_id 在当前区间的位置,则可以判断其可见性。
1、trx_id < m_ids列表中最小的事务id
表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问。
2、trx_id > m_ids列表中最大的事务id
表明生成该版本的事务在生成ReadView 后才生成,所以该版本不可以被当前事务访问。
3、m_ids列表中最小的事务id < trx_id < m_ids列表中最大的事务id
此处比如m_ids为[5,6,7,9,10]
①、若trx_id不在m_ids中,比如是8:说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。
②、若trx_id在m_ids中,比如是6,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问。不可访问后需要获取undo log链的上一个节点中的trx_id继续上述可见性判断。
其实了解了InnoDB的undo log链和readVieew快照后,innoDB的事务隔离级别产生的问题也就清楚了,主要体现不同隔离级别下产生的readView 快照时机不同,导致其获取到的undo log 节点不同。
-- 创建表
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(50) DEFAULT NULL COMMENT '姓名',
`age` int(3) DEFAULT NULL COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1;
-- 插入数据
insert into `user`(`id`,`name`,`age`) values (1,'tony',20);
-- 查看innoDB的自动提交
SHOW VARIABLES LIKE 'autocommit';
-- 关闭innoDB的自动提交
SET autocommit = 0
-- 查看全局隔离级别:
SELECT @@global.tx_isolation;
-- 设置事务隔离级别为 读未提交
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
事务A | 事务B |
---|---|
BEGIN; | BEGIN; |
INSERT INTO user (id , name , age ) VALUES(‘2’,‘blob’,‘25’); |
|
SELECT * FROM USER; //脏读产生,查询到事务A未提交的数据 | |
COMMIT; | COMMIT; |
如上所示,脏读现象发生了,是因为 InnoDB在 READ UNCOMMITTED(读未提交)是不会产生readView快照的,所以会获取到undo log链的最新数据,所以无论是新增、修改等等都会被立马获取到,没有可见性的判断。
-- 设置全局事务隔离级别为 读已提交
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
事务A | 事务B | 节 点 |
---|---|---|
START TRANSACTION; | START TRANSACTION; | ① |
UPDATE user SET age =30 WHERE id =1; |
② | |
SELECT * FROM USER WHERE id = 1; //此时查询不到事务A未提交的数脏读避免了。 | ③ | |
COMMIT; | ** | ④ |
SELECT * FROM USER WHERE id = 1; //不可重复读产生,事务A提交后,再次查询到了修改的数据,同一个事务里两次查询数据不一致 | ⑤ | |
COMMIT; |
下面我们来分析一下事务B两次读获取id=1的数据不同原因(主要的分析依据上述章节的2.undo log和3.readView),
不同于read uncommited(读未提交),read commited隔离级别下,每一次读取数据时候都会生成一个readView快照
① 事务A和事务B开启,假定当前活跃事务从5开始,则活跃的事务列表中m_ids[5(事务A), 7(事务B), 10(其他事务) ]
② undo log链条添加一条id=1的元素修改之前的数据
③ 针对此次查询事务B生成一次快照readView 包含数据如下
此时通过可见性判断,当前事务id=7 属于 m_ids列表中,最新的undo log链查找属于本事务id的undo log数据如果没有,则此事务在活跃中不可访问,需要从undo log连中查找上一个4(此时undo log只有一条事务Aid=5的链条)。4是已经提交的事务则可以直接访问初始数据。则不会查找事务A未提交的数据,避免了脏读
④ 事务A 提交 则活跃事务列表变成了 m_ids[7(事务B), 10(其他事务) ]
⑤ 同3 事务B再次生成快照readView,m_ids:[7(事务B), 10(其他事务)],min_trx_id: 7(事务B),max_trx_id: 11,可见性获取事务id=5已提交,则事务B再次获取到了id=1的新数据,两次相同查找条件获取到了不同的结果,出现了不可重复读。
-- 设置全局事务隔离级别为 读已提交
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
还是5中的例子,则可以看到事务B多次查询(事务A 提交前后)查询结果一致,这是因为在REPEATABLE READ(可重复读)隔离级别下实在每次事务开始的时候生成一次readView快照。
幻读并不是说事务中多次读取获取的结果集不同,而是一次查询后数据不存在,在插入该数据,出现数据重复的错误,
幻读: 某次的 select 操作得到的结果集所表征的数据状态无法支撑后续的业务操作
。幻读也好理解其实就是不可重复读只看事务开始的时候生成readView,不管其他事务是否提交了数据,导致自己插入重复数据。
事务A | 事务B |
---|---|
START TRANSACTION; | START TRANSACTION; |
SELECT * FROM user where id =6;// 此时查询不到id=6的记录 |
|
INSERT INTO user (id , name , age ) VALUES(‘2’,‘blob’,‘25’) |
|
COMMIT; | |
SELECT * FROM user where id =6;// 此时查询不到id=6的记录 |
|
INSERT INTO user (id , name , age ) VALUES(‘2’,‘blob’,‘25’) //幻读产生出现数据重复key错误 |
|
COMMIT; |
解决幻读问题就是通过加锁来解决,但是innoDB只有
对此InnoDB提供了一个间隙锁:主键/唯一索引查询只有锁住多条记录或者一条不存在的记录的时候,才会产生间隙锁。
查询表现形式为
select * from 【table】 where 【condition】 for update;
~~假设user表存在 id 为[0,5,15,20]
查询id=3或者查询唯一索引u=3,会产生间隙锁(0,5)
查询id>3 and id <8,会产生间隙锁(0,5],(5,10]
上述幻读例子中 查询修改为: 此时事务
-- 此时6不存在 此时会产间隙锁5~6的间隙锁
SELECT * FROM `user` where id =6 for update;
事务A | 事务B |
---|---|
START TRANSACTION; | START TRANSACTION; |
SELECT * FROM user where id =6 FOR UPDATE;// 产生间隙锁 锁住了id 【5,6】的间隙锁 |
|
INSERT INTO user (id , name , age ) VALUES(‘2’,‘blob’,‘25’) //此时会阻塞直到事务B提交 |
|
COMMIT; | |
SELECT * FROM user where id =6;// 此时查询不到id=6的记录 |
|
INSERT INTO user (id , name , age ) VALUES(‘2’,‘blob’,‘25’) //正常插入避免幻读 |
|
COMMIT; |