事务版本号:事务开始时的系统版本号。
在MySQL中,会在表中每一条数据后面添加两个字段:
创建版本号:创建一行数据时,将当前系统版本号作为创建版本号赋值
删除版本号:删除一行数据时,将当前系统版本号作为删除版本号赋值
SELECT
select时读取数据的规则为:创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。
创建版本号<=当前事务版本号保证取出的数据不会有后启动的事物中创建的数据。这也是为什么在开始的示例中我们不会查出后来添加的数据的原因
删除版本号为空或>当前事务版本号保证了至少在该事物开启之前数据没有被删除,是应该被查出来的数据。
INSERT
insert时将当前的系统版本号赋值给创建版本号字段。
UPDATE
插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行,实际上这里的更新是通过delete和insert实现的。
DELETE
删除时将当前的系统版本号赋值给删除版本号字段,标识该行数据在那一个事物中会被删除,即使实际上在位commit时该数据没有被删除。根据select的规则后开启懂数据也不会查询到该数据。
MVCC真的解决了幻读?
从最开始我们的测试示例和上面的理论支持来看貌似在MySQL中通过MVCC就解决了幻读的问题,那既然这样串行化读貌似就没啥意义了,带着疑问继续测试。
测试前数据:
事物 1 | 事物 2 |
---|---|
begin | begin |
select * from dept | |
- | insert into dept(name) values("研发部") |
- | commit |
update dept set name="财务部"(工作中如果不想被辞退一定要写where条件) | |
commit |
根据上面的结果我们期望的结果是这样的:
id name
1 财务部
2 研发部
但是实际上我们的经过是:
本来我们希望得到的结果只是第一条数据的部门改为财务,但是结果确实两条数据都被修改了。这种结果告诉我们其实在MySQL可重复读的隔离级别中并不是完全解决了幻读的问题,而是解决了读数据情况下的幻读问题。而对于修改的操作依旧存在幻读问题,就是说MVCC对于幻读的解决时不彻底的。
快照读和当前读
出现了上面的情况我们需要知道为什么会出现这种情况。在查阅了一些资料后发现在RR级别中,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,不是数据库最新的数据。这种读取历史数据的方式,我们叫它快照读 (snapshot read),而读取数据库最新版本数据的方式,叫当前读 (current read)。
select 快照读
当执行select操作是innodb默认会执行快照读,会记录下这次select后的结果,之后select 的时候就会返回这次快照的数据,即使其他事务提交了不会影响当前select的数据,这就实现了可重复读了。快照的生成当在第一次执行select的时候,也就是说假设当A开启了事务,然后没有执行任何操作,这时候B insert了一条数据然后commit,这时候A执行 select,那么返回的数据中就会有B添加的那条数据。之后无论再有其他事务commit都没有关系,因为快照已经生成了,后面的select都是根据快照来的。
当前读
对于会对数据修改的操作(update、insert、delete)都是采用当前读的模式。在执行这几个操作时会读取最新的记录,即使是别的事务提交的数据也可以查询到。假设要update一条记录,但是在另一个事务中已经delete掉这条数据并且commit了,如果update就会产生冲突,所以在update的时候需要知道最新的数据。也正是因为这样所以才导致上面我们测试的那种情况。
select的当前读需要手动的加锁:
select * from table where ? lock in share mode; select * from table where ? for update;
有个问题说明下
在测试过程中最开始我以为使用begin语句就是开始一个事物了,所以在上面第二次测试中因为先开始的事物1,结果在事物1中却查到了事物2新增的数据,当时认为这和前面MVCC中的select的规则不一致了,所以做了如下测试:
SELECT * FROM information_schema.INNODB_TRX //用于查询当前正在执行中的事物
可以看到如果只是执行begin语句实际上并没有开启一个事物。
下面在begin后添加一条select语句:
所以要明白实际上是对数据进行了增删改查等操作后才开启了一个事物。
如何解决幻读
很明显可重复读的隔离级别没有办法彻底的解决幻读的问题,如果我们的项目中需要解决幻读的话也有两个办法:
实际上很多的项目中是不会使用到上面的两种方法的,串行化读的性能太差,而且其实幻读很多时候是我们完全可以接受的。
总结