一:事务的隔离级别
事务隔离级别是为了解决并发带来的问题,脏读,不可重复读(针对update/delete操作),幻读(针对insert操作)。 为了保证一个事务中的读一致性问题,需要对其控制。
事务的隔离级别我们都知道可以解决脏读,不可重复读,幻读的问题。但是Mysql通过什么机制来实现读一致性问题呢?
这就牵涉到MVCC(多版本并发控制)。 MVCC:多版本并发控制,对于事物的隔离级别的控制。只有RC,RR隔离级别才会用到这种技术。
MVCC(Mutil-Version Concurrency Control),就是多版本并发控制。MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。
在Mysql的InnoDB引擎中就是指在已提交读(READ COMMITTD)和可重复读(REPEATABLE READ)这两种隔离级别下的事务对于SELECT操作会访问版本链中的记录的过程。
这就使得别的事务可以修改这条记录,反正每次修改都会在版本链中记录。SELECT可以去版本链中拿记录,这就实现了读-写,写-读的并发执行,提升了系统的性能。
我们来具体看看是如何实现的。
版本链
我们先来理解一下版本链的概念。在InnoDB引擎表中,它的聚簇索引记录中有两个必要的隐藏列:
trx_id这个id用来存储的每次对某条聚簇索引记录进行修改的时候的事务id。
roll_pointer每次对哪条聚簇索引记录有修改的时候,都会把老版本写入undo日志中。这个roll_pointer就是存了一个指针,它指向这条聚簇索引记录的上一个版本的位置,通过它来获得上一个版本的记录信息。(注意插入操作的undo日志没有这个属性,因为它没有老版本)
比如现在有个事务id是60的执行的这条记录的修改语句
此时在undo日志中就存在版本链
ReadView
说了版本链我们再来看看ReadView。已提交读和可重复读的区别就在于它们生成ReadView的策略不同。
ReadView中主要就是有个列表来存储我们系统中当前活跃着的读写事务,也就是begin了还未提交的事务。通过这个列表来判断记录的某个版本是否对当前事务可见。假设当前列表里的事务id为[80,100]。
如果你要访问的记录版本的事务id为50,比当前列表最小的id80小,那说明这个事务在之前就提交了,所以对当前活动的事务来说是可访问的。如果你要访问的记录版本的事务id为70,发现此事务在列表id最大值和最小值之间,那就再判断一下是否在列表内,如果在那就说明此事务还未提交,所以版本不能被访问。如果不在那说明事务已经提交,所以版本可以被访问。如果你要访问的记录版本的事务id为110,那比事务列表最大id100都大,那说明这个版本是在ReadView生成之后才发生的,所以不能被访问。这些记录都是去版本链里面找的,先找最近记录,如果最近这一条记录事务id不符合条件,不可见的话,再去找上一个版本再比较当前事务的id和这个版本事务id看能不能访问,以此类推直到返回可见的版本或者结束。
举个例子 ,在已提交读隔离级别下:
比如此时有一个事务id为100的事务,修改了name,使得的name等于小明2,但是事务还没提交。则此时的版本链是
那此时另一个事务发起了select 语句要查询id为1的记录,那此时生成的ReadView 列表只有[100]。那就去版本链去找了,首先肯定找最近的一条,发现trx_id是100,也就是name为小明2的那条记录,发现在列表内,所以不能访问。
这时候就通过指针继续找下一条,name为小明1的记录,发现trx_id是60,小于列表中的最小id,所以可以访问,直接访问结果为小明1。
那这时候我们把事务id为100的事务提交了,并且新建了一个事务id为110也修改id为1的记录,并且不提交事务
这时候版本链就是
这时候之前那个select事务又执行了一次查询,要查询id为1的记录。
这个时候关键的地方来了
如果你是已提交读隔离级别,这时候你会重新一个ReadView,那你的活动事务列表中的值就变了,变成了[110]。
按照上的说法,你去版本链通过trx_id对比查找到合适的结果就是小明2。
如果你是可重复读隔离级别,这时候你的ReadView还是第一次select时候生成的ReadView,也就是列表的值还是[100]。所以select的结果是小明1。所以第二次select结果和第一次一样,所以叫可重复读!
也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。
二:锁:
另外一种保证一致性读的技术就是加锁。解决资源竞争。
1:锁的分类:
1)从性能上分为乐观锁(用版本比对)和悲观锁
2)从对数据库的操作类型上分,分为读锁和写锁(都属于悲观锁)
读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会相互影响。
手动加上共享锁:select *from t_xx lock in share mode;
写锁(排它锁):当前写操作没有完成前,它会阻塞其它写锁和读锁。
自动加上排它锁:insert /update/delete
手动加上排它锁:select *from t_xx for update;
3)从对数据库操作的粒度分,分为表锁,行锁,页面锁。
表锁:开销小,加锁快,不会出现死锁,锁定粒度大,发生锁冲突的概率也最高,并发度最低。
行级锁:开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
页面锁: 开销和加锁时间介于表锁和行锁之间,会出现死锁。
4)为了允许表锁和行锁共存,实现多粒度的锁控制,InnoDB还有两种内部使用的意向锁,用户无法使用,这两种意向锁都是表锁。
意向共享锁:(IS)事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排它锁:(IX)事务在给一个数据行加排它锁之前必须先取得该表的IX锁。
作用:可以理解为一个标志,更加快速的判断这个表有没有锁,这样别的事务可以不用通过全表扫描才能知道该表有锁没有。可以提高加锁的效率。
死锁:多个进程互相等待对方锁的释放。
锁冲突:一个进程等待另外一个进程释放需要的锁。
2:数据库中的锁,到底锁的是什么呢?
InnoDB行锁是通过给索引上的索引项加锁来实现的,如果没有索引,InnoDB将通过隐藏的聚簇索引(rowid)来对记录加锁。InnoDB分三种情况:
Record lock:对索引项枷锁
Gap lock: 对索引项之间的“间隙枷锁”,第一条记录的“间隙”或最后一条记录后的“间隙”枷锁
Next-key lock:前两种的组合,对记录以及前面的间隙加锁。
InnoDB这种锁机制意味着,如果不通过索引条件检索数据,那么将会对表中的所有记录加锁,实际和表锁一样。
1)在不通过索引条件查询时,InnoDB将对全部记录加锁
2)由于Mysql的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键(即索引字段是可重复的),是会出现锁冲突的。需要特别注意!
3)当表中有多个索引时,不同的事务可以使用不同的索引锁定不同的行(也可以用不同的索引锁定相同的行),无论是使用主键索引,唯一索引,还是普通索引,InnoDB都会使用行锁来对数据加锁。
4)即便在条件中使用了索引字段,但是否使用索引来检索数据是有Mysql通过判断不同的执行计划代价来决定的,如果Mysql认为全表扫描效率更高,比如一些很小的表,它就不会使用索引,这种情况下也会对表进行加锁。
因此在分析锁冲突的时候,要检查执行计划,确认是否真正使用了索引。
上面的几种情况虽然没有举例说明,但是通过两个窗口自测都是可以得出结论的。
3:Next-key锁
当我们用范围条件而不是相等条件检索数据,并请求共享或排它锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙”(GAP),InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的Next-key锁。
距离来说,假如emp表中只有101条记录,其empid分别是 1,2,3............101,下面的sql:
select *from emp where empid>100 for update;
是一个范围条件的检索,InnoDB不仅会对符合条件的empid=101的记录加锁,也会对empid大于101(虽然记录并不存在)的“间隙”加锁。
如果别的session插入大于101的数据就会有锁冲突。
InnoDB使用Next-key锁的目的,一方面是为了解决幻读(具体可以参考:幻读和间隙锁),以满足隔离级别的要求,对于上面的例子,要是不使用间隙锁,如果其它事务插入empid大于101的数据,那么本事务再次执行上面的语句就会发生幻读;另一个方面是为了满足恢复和复制的需要,这个后面再论。
很显然这种范围内的锁定会出现严重的锁等待,在实际生产中尤其是并发插入比较多的应用中,我们要尽量优化业务逻辑,尽量使用等值条件访问更新数据,避免使用范围条件。
还有需要说明的是InnoDB除了通过范围条件加锁时使用Next-key锁外,如果使用相等条件给一个不存在的记录加锁,InnoDB也会使用Next-key锁。
参考《深入浅出Mysql》