0%

MySql-InnoDB锁

前言

在了解分布式锁的过程中看到了MySql锁的相关知识,所以针对MySql-InnoDB锁的相关内容做一个梳理。

InnoDB锁

相对于MyISAM,InnoDB的不同之处在于引入了事务,并且主要使用的是行级锁而不是表级锁。

InnoDB事务

并发事务的问题
  1. 脏读:事务A的未提交的写操作导致的数据修改被另一个事务读到
  2. 不可重复读:一个事务内对同一行数据的前后两次查询结果不一样,由其他事务的update,delete操作影响
  3. 幻读:一个事务内对同一批数据的前后两次查询结果不一样,由其他事务的insert操作影响
事务隔离级别

InnoDB的锁模式

InnoDB行级锁是针对索引的索引项加锁,也叫记录锁。如果某个sql没有用到索引,那么就会使用聚集索引。
在聚集索引的所有索引项上加锁(类似表级锁),MySQL之后会进行优化,释放掉不符合条件的索引项的锁。
但这个加锁和释放的过程仍然执行了,所以要保证sql尽量用到索引。

共享锁与排他锁
共享锁(s):读锁,允许一个事务读一行,并阻止其他事务获取相同数据集的排他锁
排他锁(x):写锁,允许一个事务读/写一行,并阻止其他事务获取相同数据集的排他锁和共享锁
updat,insert,delete操作会自动加排他锁,而select默认不加锁,在串行读下会加共享锁
意向锁
意向锁属于表级锁,由数据库完成其申请操作,分为意向共享锁和意向排他锁。
其解决的问题是:事务A先获取了表table的一个行级写锁,之后事务B想获取table的表级写锁,为了避免冲突
逐行检查有没有冲突的行级锁,这样明显很浪费时间。

而意向锁的作用就是:在事务申请一个行级锁之前,数据库会自动先申请一个意向锁。如果已经有事务持有该表
的表级锁就会等待锁释放,而申请成功后,如果再有事务申请该表的表级锁,也会等待锁释放。这就避免了逐行
检查的开销。
上图是意向锁和表级共享锁,排他锁的兼容关系(绿色表示兼容,红色表示冲突),可以看到,意向锁之间是不
冲突的。从这里就可以看到,意向锁的主要目的还是解决表级锁和行级锁的冲突。有了意向锁作为一种前置判断
可以很方便的进行检查防止冲突。
间隙锁与临键锁

前面提到,InnoDB的行级锁是针对索引的索引项的,可以设想这样一种情况:

对上图的表数据,age字段存在非唯一索引,那么执行以下sql:
事务A:select name,age where age > 4;
      update set name = 'xx' where age > 4;
      select name,age where age > 4;
事务B:insert into table(id,name,age) values(5,'bb',7);

按照前文对行级锁的理解,事务A的update操作会通过age的索引锁定主键索引4并加排他锁,事务B的操作不会
因为锁阻塞。那么这时事务A的第二个查询就会和第一个查询的结果不一样,莫名多了一条数据。

为解决这种问题,就使用了间隙锁(GAP-LOCK)
间隙锁(GAP-LOCK):是指对表中不存在的数据上加锁,一般处于两个数据行的间隙,所以叫间隙锁。用来防止
                 其他事务的新增操作导致幻读,通常会在条件是一个范围的时候使用,而如果条件索引是唯
                 一索引或者结果是唯一值(可以理解为不会因为其他事务的新增操作影响结果),会放弃间隙
                 锁而只使用记录锁
临键锁(Next-Key):临键锁比较好理解,他等于记录锁+间隙锁

MVCC(多版本并发控制)

MVCC通过对每一行数据增加两个隐藏值:事务ID,回滚指针,来实现并发控制。
不同事务对一行数据的写操作会产生多个版本,通过回滚指针形成一个链表的结构。
读操作不用加锁,可以减少锁的使用和等待等开销。

快照读和当前读

根据MVCC的版本记录,就产生了两种读取方式:快照读和当前读
快照读:读取记录的可见版本,默认的select就是快照读,不用加锁
当前读:读取记录的最新版本并加锁,防止其他事务的操作,属于当前读的操作有
1. select …… from …… where …… lock in share mode; 加共享锁
2. select …… from …… where …… for update; 加排他锁
3. insert,update,delete

ReadView

ReadView用来控制快照读的可见版本,其操作原理是在生成ReadView时会保存一个当前活跃事务
ID的列表,在进行select操作时,会将版本链中数据的事务ID与ReadView中的ID列表比对,判断版本是否可见。

例如:如果版本的事务ID大于ReadView事务ID列表的最大值,那么说明该版本数据是在生成ReadView之后提交的,自然对当前事务不可见,通过回滚指针遍历之前的版本继续比对。

不同事务隔离级别的区别

  1. 未提交读:读不加锁,只读取最新版本数据,可以读到未提交的记录
  2. 已提交读:使用MVCC,select使用快照读,写操作只用记录锁
    每一次select都会生成一个新的ReadView,所以可以读取提交的数据版本,导致不可重复读
  3. 可重复读:mysql默认隔离级别
    使用MVCC,select使用快照读,写操作会使用记录锁,间隙锁和临键锁
    在第一次select时生成一个ReadView,之后不再改变,所以不会读到之后提交的数据版本,
    保证前后读取的数据一致性(所以这里也可以防止幻读)
  4. 串行读:select加共享锁,读写互斥

死锁

产生死锁的原因主要就是加锁的顺序不一样,因为MySql的加锁是对符合条件的数据逐行加锁。
这就会导致两个事务持有对方需要的锁,导致互相等待,形成死锁

小结

  关于MySql-InnoDB锁的相关知识点,对于基本的行锁的概念和作用其实很好理解,包括事务隔离级别及其避
免的并发问题,不过在我阅读相关博客时,对一些细节始终没有找到准确的答案,比如ReadView的具体实现方
式,间隙锁的具体情况下的加锁操作。这些之后需要找些相关书籍确定答案,这里就还是做一下小结:
1.InnoDB的默认事务隔离级别是可重复读,写操作会涉及锁的加载和释放。所以合理的sql操作及索引配置能
  够有效减小开销及死锁的风险。这也是sql的优化的一个方向。(所以索引对于sql优化很重要)
2.MVCC的版本控制思想是解决并发问题的常用办法,例如:CAS解决aba问题就是添加版本号的方式
3.使用select……for update 实现分布式锁就是利用他给行数据添加了排他锁的原理,这样就解决了并发下
  对同数据集的操作,不过不想使用锁的话就可以借用版本控制的方法,增加一个版本号的字段进行判断