前面说了为了解决脏读,幻读,不可重复读,mysql设置了四种隔离级别,read committed和read uncommitted会发生幻读和不可重复读,repeatable read会发生不可重复读,seriliztable,mysql默认是repeatable read,用mvcc解决不可重复读。设置隔离级别set global|session transaction isolation level …。当global时候,代表执行完之后其他所有session都可以使用当前设置的事务,如果是session则代表之后当前session才可以执行当前设置的事务,如果什么都没加,则是默认下一条事务提交完毕,就恢复之前的事务。Mvcc用他的readView链表控制解决这不可重复读,每次执行修改,都会吧修改的数据放入readView链表,链表有一个参数是trx_id,链表的头部第一条数据显示的是页面数据,后面的都是undo数据。里面有m_ids,min_trx_id,max_trx_id,creator_trx_id,主要在里面遍历,判断是否满足数据在当前事务可见性,比如creator_trx_id等于当前事务id,意味着该版本可以在当前事务查看,如果当前事务id大于mix_trx_id,表明该版本链在事务后才生成,则不可见,如果当前事务id小于min_trx_id,则表示该版链已提交,可以见。如果在这两个之间,则看是否事务id在m_ids里面,是就代表是活跃事务,不可见。这就保证了事务的可重复读。
事务隔离级别与MVCC (1)—mysql进阶(六十七)
解决并发事务带来问题的两种基本方式
前面说了事务并发可能带来各种问题,并发事务访问相同记录大致分为3种:
- 读-读 的情况:并发事务相继读取相同记录
因为读取并不会改变记录,所以并不会引起问题。
- 写-写 的情况:并发事务相继对相同的记录做出改动
我们前面说过这种情况就是脏写,脏写是在mysql四种隔离级别情况下都是不允许发生的。那么怎么解决脏写的呢,于是在多个事务并发写同一条数据的时候,让他们排队,这个排队的过程就是通过锁来实现的。这个锁其实是内存结构,一开始并没有锁和记录相关联。
当一个事务准备改动事务里的记录时,首先看看他有没有锁结构,当没有的时候吗,会在内存中生成一个锁结构。比方说t1要对这个记录做修改,要生成一个锁结构与之关联:
比方说修改t1里的数据,trx信息是t1,is_waiting:false。
锁结构两个重要的属性,一个就是trx信息,一个就是is_waiting。
一个代表是哪个事务的锁结构,一个代表当前事务是否在等待。
当t1需改这个数据,就生成了一个锁结构,因为他可以直接修改不需要等待,所以锁结构里的is_waiting是false。
这时候t2需要修改这个数据,因为检测到其他事务正在修改这个数据,所以这时候t2需要等待,他的is_waiting就是true。
当t1吧数据修改完毕,吧锁释放之后,会查看其他事务是否在等待,于是就把t2的is_waiting改为false,唤醒当前锁,让他执行。
综上:
不加锁:意思不需要在内存加锁结构,可以直接执行。
获取锁成功,加锁成功:意思是内存中生成了对应锁结构,而锁结构是false,表示可以执行。
获取锁失败,加锁失败:意思是内存中已经存在对应的锁结构,需要等待其他事务提交,当前事务才可以继续执行,表示true,需要等待执行。
读-写,写-读 的情况:也就是一个事务读,一个事务改动。
这种情况下可能出现脏读,幻读,不可重复读等问题。
Mysql在repeatable read已经解决了幻读问题,那么他是怎么解决脏读,幻读,不可重复读的问题呢?其实本质就是解决事务并发执行的问题,解决方案有两种:
方案一:读操作利用多版本并发控制,mvcc,写操作进行加锁
前面说过mvcc会生成一个版本链readView,由修改的数据和页面真实数据组成,readView第一条数据就是页面存在的数据,后面的数据就是undo 修改的数据。因为里面有creator_trx_id和min_trx_id,max_trx_id,m_ids,当前事务d与creator_trx_id相同,则可见,若大于max_trx_id,readView刚生成,不可见,若小于min_trx_id,则表示已经提交,可见,若在这之间,则判断当前事务是否在m_ids里,存在则代表当前事务活跃,不可见。而写入数据则是采用加锁的方式,这样保证读写不冲突。Read commtted 和 repeatable read是不同的,前者是每次selct都会创建一个readView版本链,而后者只有第一次select才创建,之后都是复用之前的readView版本链,解决了不可重复度和幻读。
方案二:读、写操作都用加锁来完成
我们在有些业务需求下,不允许用读取旧的事务,必须每次去读取最新的记录,比如银行里的业务,需要把账户余额读取出来,再加上本次金额,咋写入数据库。在操作当前数据的 时候,不想让其他事务访问本次数据,直到这次事务结束。这样的操作就需要在读和写都加锁。
(注意:读,写都加锁的话可以解决脏读,因为脏读是读取了另一个事务未提交的一条事务,如果给这个记录加了锁,那么就不可以读。不可重复读也可以解决,不可重复读是在事务里,两次读取的同一条值不同,这时候加锁了,就不会出现。但是解决幻读有点麻烦,幻读是因为读取了更多的数据,并不知道给那个多的数据怎么上锁,后面会介绍)
很显然,采用mvcc读写性能更高,一般情况下都是采用mvcc。但如果在特殊的业务场景下,会采用加锁的方式来解决业务问题。
一致性读(Consistent reads)
事务利用mvcc进行的读取操作称为一致性读,或者 一致性无锁读,有的地方称为 快照读。所有select语句在read committed 、 repeatable read隔离级别下都算一致性读。如:
Select * from t.
Select * from t left join t2.
一致性读并不会加锁,其他事务可以任意对表中的记录做自由修改。
锁定读(locking read)
共享锁和独占锁
前面说过并发的情况下 读-读 不会有问题,不过对于 写-写,读-写,写-读这些情况可能引引起一些问题,需要使用mvcc或者加锁的方式来解决。在使用加锁的方式解决问题时候,mysql设计了两个锁的分类:
共享锁:shared locks,简称s锁。事务读取一条记录时候,必须先获取该记录的锁。Lock in share mode 开启后另一个事务只可以读不可以修改。
独占锁:也叫排它锁,Exclusive locks,简称x锁。事务改动一条记录时候,先要获取这个锁。For update 开启后另一个事务不可以读也不可以修改。
写操作
平常用的写操作无非就是delete,insert,update这三种:
Delete:
对一条记录做delete操作,无非就是在b 树中定位到这条记录的位子,然后获取这条记录的排它锁,然后在执行delete mark操作。我们也可以吧这个定位待删除记录在b 树中位子过程看成是一个获取排它锁的锁定读。
Update:
如果对一条记录修改操作时候分为三种情况:
如果未修改这个主键值,并且存储空间没有发生变化,则直接就地修改,在b 树定位这条记录的位子,然后在获取这个记录的排他锁,最后在原记录的位子就行修改操作。其实也可以吧这个定位修改记录在b 树中位子过程看做是一个获取x锁的锁定读。
如果未修改这个记录的主键值,但存储空间发生了变化,则会先定位b 树的位子,获取排它锁,把这条记录放入垃圾链表,然后插入一条新的数据。定位修改记录的b 树位子可以看做排它锁的锁定读,insert操作提供隐式锁进行保护。
如果修改了该记录的主键,相当于在原纪录的基础上,先delete,在insert,加锁只需要按照delete和insert的规则就好。
Insert:
一般来说,新插入的数据不需要加锁,mysql提供一种隐式锁来保护这条新数据在事务提交之前,不被其他事物来访问。
多粒度锁
我们前面提到的锁针对的记录,可以说是行级锁,或者行锁,对一条记录加锁影响也只是这条记录而已,可以理解这个锁的颗粒度比较细。其实一个事务也可以在表级别进行加锁,自然称为表级锁或者表锁,对表加锁我们可以说这个锁的颗粒度比较粗,给表加锁分为共享锁和排它锁:
1、给表加s锁:
如果一个事务给表加s锁,那么,
别的事务可以继续获得该表的s锁。
别的事务可以继续获取该表某些记录的s锁。
别的事务不可以获取该表的排它锁。
别的事务不可以获取该表一些记录的排它锁。
2、给表加排它锁:
如果一个事务给表加排它锁(意味着独占这个表),那么:
别的事务不可以继续获得该表的s锁。
别的事务不可以继续获取该表某些记录的s锁。
别的事务不可以获取该表的排它锁。
别的事务不可以获取该表一些记录的排它锁。
我们在实际生活中举个例子:
教室一般是公用的,当有学生进入的时候,会在门口加个s锁,这时候如果很多学生进去,则很多s锁。
如果有维修人员来修理天花板或者空调或者电路等,这时候就不能让学生进去,这时候上个x锁。
当有特殊需求:
当学校领导来参观的时候,给教室上个表级别的s锁,因为学生可以正常进入学习,但是维修人员不可以进入,要等s锁解除,才会上个x锁。
当学校占用教学楼考试:
此时会给教室上个x锁,维修人员和学生都不可以进入,类似表级别的x锁。
但这里面有两个问题:
如果想对整栋楼上s锁,首先确保楼里没有正在维修的教室,否则要等他维修完毕才上s锁。
如果想对整栋楼上x锁,首先需要确保没有考试的教室和维修的教室,如果有的话,需要等他们全部锁释放,才可以整栋楼上x锁。
这时候我们怎么知道整栋楼里有没有教室上锁呢,难道一次遍历,那太慢了,于是innoDB有个意向锁,Intention locks:
意向共享锁:intention shared locks,is锁,当事务给某行记录上s锁的时候,还会上个is锁。
意向排它锁:intention exclusive locks,ix锁,当事务给某行记录上行x锁的时候,还会上个ix锁。
所以不需要遍历,只需要在表锁的时候判断一下is和ix是否锁了就行。