多事务并发的问题
对 innodb引擎执行流程 和 buffer pool 足够了解的话,那一定知道mysql系统在初始化的时候bufferpool会将内存分为多个缓存页,此时的free链表都是空的;在对数据做操作的时候,就会将磁盘的数据页加载到内存的缓存页中去,此时这个缓存的描述信息就会从free链表中移除、同时将数据页信息放入lru冷链中和flush链表中;然后我们开始操作数据后,会记录undolog事务日志、redolog和binlog,最后刷盘。如果是commit之前宕机、可以通过redolog恢复之前的操作;如果是commit后出现问题,则会通过undolog来回滚。一个事务的执行流程大抵如此。
那多个事务并发运行的时候呢?n个用户同时触发一系列操作又会出现什么问题?比如:
* 多个事务并发对缓存页里的一行数据进行更新,这个冲突怎么处理?是否要加锁?
* 有事务在对一行数据做更新,其它事务在查询这行数据,这里的冲突怎么处理?
脏读、写
数据库里面有一个数据值为null,此时第一个线程将他修改为了A,这时候第二个事务来了,在内存页将它修改成了B。此时A突然回滚了,值成了最开始log里面记录的null,第二个线程丢改的值丢失了,这就叫脏写。
数据库里面有一个数据值为null,此时第一个线程将他修改为了A,这时候第二个事务来了,在内存页查出来了这个A值。此时A突然回滚了,这个时候第二个线程的业务需要再查询一次,得到的却是null值,就很有可能影响之前的业务逻辑,这就是脏读。
无论是脏写还是脏读,都是因为一个事务去更新或者查询了另外一个还没提交的事务更新过的数据。但由于另外一个事务还没提交,所以他随时可能会反悔会回滚,那么必然导致你更新(查询)的数据就没了。
不可重复读
emmm .... 不可重复读这个东西吧,你说他有问题他也有问题,你说他没问题他也没问题,不过mysql设计了一套机制来避免不可重复度的问题。上面的幻读幻写的本质就是操作了其他事务没提交的写数据。而不可重复读呢,举个例子:事务A在执行过程中查询一个数据是null,此时另一个事务修改为B提交后,事务A再次查出来就是值B,现在又一个线程修改为C提交后,事务A再次查出来就是值C,每次查出来的数据都不一样。那这到底是有问题还是没问题呢,仁者见仁智者见智吧!等会我会讲下mysql是如何规避这个问题的,mysql设计的比较高明。
幻读
幻读特指的是你查询到了之前查询没看到过的数据!比如查询月薪高于5000的人有10个,其他事务插入了几条数据后,此时查出了12条。那就会自我怀疑为啥一样的语句,前后两次查出的数据不一样了。
事务隔离级别
这里说的SQL标准的事务隔离级别,并不是MySQL的事务隔离级别,MySQL在具体实现事务隔离级别的时候会有点差别。SQL标准中规定了4种事务隔离级别,就是说多个事务并发运行的时候,不同的隔离级别是可以避免不同的事务并发问题的这4种级别包括了:read uncommitted(读未提交),read committed(读已提交),repeatable read(可重复读),serializable(串行化)。
read uncommitted:其他事务写操作没提交时,你只能读不能写。(这样不会脏写,可是会有脏读啊,一般没人用这个)
read committed:其他事务没提交时,你无法读或写。(俗称:RC,杜绝了脏读脏写,但还会幻读和不可重复读)
repeatable read:不会发生脏写、脏读和不可重复读,但还有幻读。(俗称:RR)
serializable:多个事务操作同一份数据时,会串行化,避免了上述所有问题。(这速度,想想都知道没人用)
MySQL默认设置的事务隔离级别,都是RR级别的,不过MySQL的RR级别SQL标准的RR级别不同,MySQL是可以避免幻读发生的。也就是说,MySQL里执行的事务,默认情况下不会发生脏写、脏读、不可重复读和幻读的问题。@Transactional(isolation=Isolation.DEFAULT),其实默认的就是DEFAULT值,这个就是MySQL默认支持什么隔离级别就是什么隔离级别。
MVCC
MVCC,全称Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的方法,在InnoDB中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。也就是说脏写、脏读、不可重复读、幻读,都不会发生,每个事务执行的时候,跟别的事务压根儿就没关系,甭管你别的事务怎么更新和插入,我查到的值都是不变的,是一致的!可能说比较抽象,那在讲mvcc之前,不得不提一下undolog版本链这个东西了。
undolog版本链
简单来说,其实我们的每条数据都有两个隐藏字段:一个是trx_id,一个是roll_pointer。这个trx_id就是最近一次更新这条数据的事务id,roll_pointer就是指向你了你更新这个事务之前生成的undo log文件。多个事务执行的时候,每个人修改了一行数据,都会更新隐藏字段txr_id和roll_pointer,多个数据快照对应的undo log,会通过roll_pinter指针串联起来,形成一个重要的版本链!下面演示下它的原理:
1. 比如现在有一个事务A(id=10),插入了一条数据。入的值是A,因为事务id=10,所以trxid=10;roll_pointer指向一个空的undo log,因为之前这条数据是没有的。
2. 此时事务B(id=20)跑来修改这条数据。就把值修改为B了,同时trxid=20;然后roll_pointer指向一个新undo log,里面记录了之前的值,也就是A。
3. 然后又来了一个事务C(id=25)过来修改。那现在值就成了C,trxid=20,roll_pointer指向的undo log值为B。
readview
现在再分析下基于undo log多版本链条实现的ReadView机制,就理解MVCC多版本并发控制机制了。这个ReadView呢,简单来说,就是你执行一个事务的时候给你生成一个ReadView,里面比较关键的东西有4个:
m_ids,这个就是说此时有哪些事务在MySQL里执行还没提交的
min_trx_id,就是m_ids里最小的值
max_trx_id,这是说mysql下一个要生成的事务id,就是最大事务id。max_trx_id并不是m_ids中的最大值。
creator_trx_id,就是你这个事务的id
读写并发
mysql的mvcc处理读写并发,正是基于undo log多版本链条+ReadView机制实现的。ReadView默认是RR级别的,现在模拟下它的流程:
1. 假设原来数据库里存在着一行数据,他的值就是初始值,事务id是100
2. 然后两个事务并发过来执行了,一个是事务A(id=200)过来查询的,一个是事务B(id=300)过来修改的
3. 现在事务A开启了一个ReadView,此时m_ids包含事务A和事务B的两个id,min_trx_id就是200,max_trx_id就是301,creator_trx_id是自己。事务A第一次查询这个数据会判断,当前数据的txr_id是否小于m_ids的最小id,发现100<200,则说明这个数据是在A开启事务之前就存在的,所以能读的到这个值。
4. B开始修改了,然后这行数据的txr_id就成了自己的id、也就是300,然后提交事务。
5. 这个时候A又来查询了,发现数据行txr_id是300,要大于m_ids的最小值200,那么说明更新数据的事务是和自己一起或者之后开启的,所以这个修改后的值是查不到的,接着它会roll_pointer顺着undo log日志链条一直往下找,直到找到trx_id是100,100是小于200的,说明这个undo log版本必然是在事务A开启之前就执行且提交的。那么就读到了这个数据。
而RC隔离级别,实际上意思就是说你事务运行期间,只要别的事务修改数据还提交了,你就是可以读到人家修改的数据的,所以是会发生不可重复读的问题,包括幻读的问题,都会有的。就是因为他每次发起查询,都重新生成一个ReadView。
。