案例:
假设我有一张表
| id | name |
|---|---|
| 1 | 张三 |
| 2 | 李四 |
此时有三个线程,做了几件事情。
t1时刻:
A线程:
begin;
select *** where name = '张三' for update;
update *** set name = '王五' where name = '张三';
此时id为1的记录已经被锁住了。 查到的数据是【id:1,name:张三】。 并且将name从张三改成王五。当前事务未commit。
t2时刻:
B线程:
begin;
update *** set name = '张三' where id=2;
commit;
t3时刻:
A线程:
select *** where name = '张三' for update;
查到的数据是【id:2,name:张三】,出现了不可重复读现象了。
t4时刻:
C线程:
begin;
insert into *** value (3,'张三');
commit;
t5时刻:
A线程:
select *** where name = '张三' for update;
commit;
查到的数据是【id:2,name:张三】,【id:3,name:张三】出现了不可重复读、幻读现象了。
问题分析:
上面最终导致数据是这样的:
| id | name |
|---|---|
| 1 | 王五 |
| 2 | 张三 |
| 3 | 张三 |
提交事务的时刻分别是:
t2时刻,B线程update *** set name = '张三' where id=2;
t4时刻C线程insert into *** value (3,'张三');
t5时刻A线程update *** set name = '王五' where name = '张三';
可以看出如果从库同步binlog会产生的数据是:
| id | name |
|---|---|
| 1 | 王五 |
| 2 | 王五 |
| 3 | 王五 |
| 最终导致主库和从库数据不一致问题。 |
数据库支持四种隔离级别来解决这些问题:
1.读取未提交 一个事务还没有被提交时,他做的变更就能够被其他事务看到。
2.读取已提交 一个事务在被提交后,他做的变更才能够被其他事务看到。
3.可重复读 在同一个事务中多次读取同一个数据的结果是一样的。
4.可串行化 强制事务串行执行,后访问的事务必须等前一个事务完成才能执行。
InnoDB引擎怎么实现的?
存在两种读,快照读和当前读。
快照读:每一次修改数据都会在undo log里写快照记录,读取的是undo log里的某一版本快照。
读取已提交和可重复读级别下的普通select都是快照读。
当前读:读取数据最新版本,除了快照读都是当前读,如:select...lock in share mode/
select...for update/update、delete、insert
当前读解决问题:
比如上面的线程A我不锁id=1这一条记录了,我把id=2这一条记录也锁上,
线程B想修改id=2的数据只能被阻塞住,但是这样线程C还是可以新增数据,
这个锁不住因为表中之前也没有id=3的数据。InnoDB他实现了个间隙锁,
这个id=1,id=2两条记录不是会产生三个间隙嘛,(-无穷大,1),(1,2),(2,+无穷大),
我把这三个缝隙也锁上,任何人都不能在这三个区间新增了,这样线程C想新增id=3,
正好属于(2,+无穷大)这个区间被阻塞住。
快照读解决问题:
InnoDB实现了一套不用加锁解决读写冲突的机制来解决,这就是MVVC机制。
他是这么一回事,每条记录都有个隐藏字段事务id,并且你修改都会将之前记录写入undo log里面,
他是通过引用将多个记录串成一个记录版本链条,然后生成readview来按照一定规则来判断你应该读取哪一个版本。
readview里面有四个重要的属性:
1.未提交事务id集合,简单记为txIds
2.当前readview里事务id集合中最小的事务id,简单记为minTxId
3.系统分配给下一个事务id,简单记为nextTxId
4.创建当前readview的事务id,简单记为curTxId
读取版本链规则:
从最新的版本依次按下面规则匹配,如果匹配成功则读取,如果匹配失败,则继续匹配上一个版本,一直到匹配结束。
1.curTxId是否和版本链中记录的事务id相同,是则成功,不是则失败
2.版本链中记录的事务id是否小于minTxId,如果小于说明这条记录已经提交,可读取成功,不是则失败
3.版本链中记录的事务id大于等于nextTxId,失败
4.版本链中记录的事务id大于minTxId,小于nextTxId,如果版本链记录的事务id在txIds中则失败,否则成功。
并且在不同隔离级别下readview的生成还不一样:
读已提交:一个事务中每一个查询都会生成一个新的readview
可重复读:一个事务中第一次查询会生成一个readview,后面的查询不会创建readview,会使用第一次的readview
案例演示:
假如事务100插入一条数据(1,'张三'),并提交了事务。 以后又有事务101、102、103对这一条数据进行读写请求。
t1时刻:
事务101:
begin;
查询id=1;
t2时刻:
事务102:
begin;
查询id=1;
t3时刻:
事务103:
begin;
查询id=1;
t4时刻:
事务102:
设置id=1的name为李四;
t5时刻:
事务101:
查询id=1;
t6时刻:
事务102:
查询id=1;
commit;
t7时刻:
事务103:
查询id=1;
设置id=1的name为王五;
commit;
t8时刻:
事务101:
查询id=1;
commit;
读已提交隔离级别:
t1时刻:
事务101查询生成readview:【txIds:101,minTxId:101,nextTxId:102,curTxId:101】,
当前时间点版本链只有一个快照(事务id=100,name:张三,id:1),按规则匹配符合第二条,成功。
t2时刻:
事务102查询生成readview:【txIds:101,102,minTxId:101,nextTxId:103,curTxId:102】,
当前时间点版本链只有一个快照(事务id=100,name:张三,id:1),按规则匹配符合第二条,成功。
t3时刻:
事务103查询生成readview:【txIds:101,102,103,minTxId:101,nextTxId:104,curTxId:103】,
当前时间点版本链只有一个快照(事务id=100,name:张三,id:1),按规则匹配符合第二条,成功。
t5时刻:
事务101查询生成readview:【txIds:101,102,103,minTxId:101,nextTxId:104,curTxId:101】,
当前时间点版本链有两个快照(事务id=102,name:李四,id:1),(事务id=100,name:张三,id:1),
按规则从第一个版本匹配失败,匹配第二个版本成功。
t6时刻:
事务102查询生成readview:【txIds:101,102,103,minTxId:101,nextTxId:104,curTxId:102】,
当前时间点版本链有两个快照(事务id=102,name:李四,id:1),(事务id=100,name:张三,id:1),
按规则从第一个版本匹配,符合第一条成功。
t7时刻:
事务103查询生成readview:【txIds:101,103,minTxId:101,nextTxId:104,curTxId:103】,
当前时间点版本链有两个快照(事务id=102,name:李四,id:1),(事务id=100,name:张三,id:1),
按规则从第一个版本匹配符合第四条,成功。
t8时刻:
事务101查询生成readview:【txIds:101,minTxId:101,nextTxId:104,curTxId:101】,
当前时间点版本链有三个快照(事务id=103,name:王五,id:1),
(事务id=102,name:李四,id:1),(事务id=100,name:张三,id:1),
按规则从第一个版本匹配,符合第四条成功。
可重复读隔离级别:
和读已提交级别类似,只是t5、t8时刻使用的是t1时刻的readview,t6时刻使用的是t2时刻的readview,t7时刻使用的是t3时刻的readview。