likes
comments
collection
share

MySQL中的锁

作者站长头像
站长
· 阅读数 17

解决并发问题的两种方式

隔离级别并发事务引发的问题
读未提交脏读 不可重复读 幻读
读已提交不可重复读 幻读
可重复读幻读
串行化--

在事务隔离级别为可重复读时,实际上MySQL也能通过其他手段解决幻读问题,一般通过如下两种方式:

  • 第一:多版本控制。在事务开启时生成一个Read View,Read View中有当前事务id、活跃事务id的最小值和应该给下一个事务的 id 值,通过Read View 可以判断当前事务对一条的记录的可见性,当前事务查询语句只能读到在生成 ReadView 之前已提交事务所做的更改,在生成 ReadView 之前未提交的事务或者之后才开启的事务所做的更改是看不到的,这样当前事务多次读取的数据就能保持一致,很好地解决了幻读问题。
  • 第二:加锁。在执行select ... for update时加Next-key lock,将当前记录和查询到的上一条记录之间锁定,不允许其他事务进行插入删除,这样当前事务没有释放锁之前,多次读取到数据记录数能够保持一致,避免了幻读问题。

今天我们主要研究MySQL中锁,根据加锁的范围,MySQL里面的锁大致可以分成全局锁、表级锁和行锁三类下面我们一起看看这些锁吧!

MySQL中的锁

全局锁

所谓全局锁就是对整个数据库加锁,加锁之后,对数据的增删改操作和表的创建、修改都会阻塞。

MySQL中的锁

怎么使用全局锁

Flush tables with read lock; 使整个数据库处于只读状态

MySQL中的锁 unlock tables; 释放全局锁

MySQL中的锁

应用场景

全局锁最经典的应用场景就是全库逻辑备份,这样就不会出现备份数据和实际业务数据不符的情况。

举个例子,假如在全库逻辑备份期间不加全局锁,有两个表用户余额表和商品信息表,当用户购买一件商品时,一般会先更新用户的余额再更新商品的信息,在不加锁的情况,就有可能出现下面这种情况:

  1. 先备份了用户余额
  2. 用户购买一件商品
  3. 备份商品信息

这样就出现了备份数据和实际业务数据不一致的情况,全库逻辑备份期间,用户余额信息发生了变更但是却没有备份下来,而商品信息的变更被备份了。

加锁带来的问题

加锁势必会降低性能,全局逻辑备份期间,如果加全局锁,意味着整个数据库都处于只读状态,那么业务就不能正常进行,造成业务停滞。

解决方法

当然也可以选择业务量少的时间,比如深夜,但这样还是不够方便智能。

官方提供了mysqlldump工具,使用时加上–single-transaction参数。具体原理就是:在可重复读隔离级别下,备份数据库时开启一个备份事务,创建一个Read View,在整个备份事务期间都使用这个Read View,只能看见创建事务前已经提交的事务,而正在进行的业务可以正常的对数据库的数据进行更新,只不过这部分数据此时不会被备份,在下一次全局逻辑备份时才会进行备份。

表级锁

MySQL 里面表级别的锁有这几种:

  • 表锁;
  • 元数据锁(MDL);
  • 意向锁;
  • AUTO-INC 锁;

表锁

表锁即对一张表加锁,有如下特点:

  • 加锁过程的开销小,加锁的速度快;
  • 不会出现死锁的情况;
  • 锁定的粒度大,发生锁冲突的几率大,并发度低;

应用场景:

  • 一般在执行DDL语句时会对整个表进行加锁,比如说 ALTER TABLE 等操作;
  • 如果对InnoDB的表使用行锁,被锁定字段不是主键,也没有针对它建立索引的话,那么将会锁整张表;
  • 表级锁更适合于以查询为主,并发用户少,只有少量按索引条件更新数据的应用,如Web 应用。

加锁命令:

//表级别的共享锁,也就是读锁;
lock tables 表名 read;

//表级别的独占锁,也就是写锁;
lock tables 表名 write;

需要注意的是,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。比如某个线程A中执行lock tables t1 read,t2 wirte;这个语句,则其他线程写t1、读写t2的语句都会被阻塞。同时,线程A在执行unlock tables之前,也只能执行读t1、读写t2的操作,不能对t1加写锁和写t1。

释放锁可以使用下面这条命令,会释放当前会话的所有表锁:

unlock tables

另外,当会话退出后,也会释放所有表锁。

元数据锁

在MySQL5.5版本引入了MDL,MDL不需要显式使用,在访问一个表的时候会被自动加上

  • 当对一个表做增删改查操作的时候,加MDL读锁;
  • 当要对表做结构变更操作的时候,加MDL写锁

引入MDL锁主要是为了保证用户对表进行CRUD操作时,其他线程不会修改表结构。

  • 当有线程执行CRUD语句,对表加MDL读锁,之后所有的对表做变更的线程都会被阻塞,直至线程释放MDL读锁。
  • 当有线程对表做变更,对表加MDL写锁,之后申请MDL读锁和写锁的线程都会被阻塞,直至线程释放MDL写锁。

加MDL锁会产生表不可读的情况,当有一个长事务申请到MDL读锁之后一直不释放,后续如果是其他线程申请MDL读锁还没问题,读读共享可以同时进行持有MDL读锁,但是如果是MDL写锁,就会阻塞,同时也会导致后面申请MDL读锁的线程也阻塞,这是因为申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。如果此时有大量该表的 select 语句的请求到来,就会有大量的线程被阻塞住,这时数据库的线程很快就会爆满了,这个表也就变的不可读。

MySQL中的锁 所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。

意向锁

意向锁,表达的是加锁的一个意向

  • 当想对一些记录加共享锁之前,需要先对表加意向共享锁
  • 当相对一些记录加独占锁之前,需要先对表加意向独占锁

意向共享锁和独占锁是表级锁,不会和行级的锁发生冲突,只会和共享表锁和独占锁发生冲突。意向锁的主要作用是判断表里是否有记录被加锁,也就是表里是否有行级锁。当需要加一个表级锁时,需要判断表里是否有独占锁,如果没有意向锁就需要遍历表里所有记录,效率很低;现在有了意向锁,只需要查看是否有表级意向独占锁,如果有则意味着表里已将有了独占锁这样就不用遍历整个表了。

MySQL中的锁

AUTO-INC锁

自增锁与表中字段声明AUTO_INCREMENT有关,主要是为了保证插入数据时字段不会重复,当一个事务持有AUTO-INC锁时,其他事务中的插入语句都会被阻塞,从而使得被AUTO_INCREMENT修饰的字段是连续递增 。

自增锁不是在事务提交后才释放,而是执行完插入语句后就会立即释放,虽然这样的锁粒度已经很小了,但当大量数据进行插入时,还是会很影响性能,因此在MySQL5.1.22版本开始,InnoDB提供了一种轻量级锁实现自增,这种锁粒度更小,只要将自增字段插入完成就会释放,不需要等待整个插入语句执行完。

InnoDB通过innodb_autoinc_lock_mode系统变量控制使用AUTO-INC锁还是轻量级的锁:

  • innodb_autoinc_lock_mode=0,使用AUTO-INC锁
  • innodb_autoinc_lock_mode=2,采用轻量级锁
  • innodb_autoinc_lock_mode=1,普通 insert 语句,自增锁在申请之后就马上释放; 类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;

innodb_autoinc_lock_mode=2时,系统性能最高,但是当binlog日志格式为statement时,会出现主从复制场景中主从数据不一致的情况。因为日志格式为statement时,binlog 日志只会记录sql语句也就是逻辑操作,在主库中数据的插入可能是多个事务交叉运行插入语句,两个事务插入数据的自增id也就是相互交叉的,事务A和事务B的插入数据自增id不连续,但是记录在binlog中时,要么先记录事务A的插入,要么就先记录事务B的插入。从库根据binlog 日志执行时,是按照顺序执行的,事务A和B的插入数据的自增id就变成连续的了,和主库数据的自增id数据不一致。

MySQL中的锁

解决方法就是:innodb_autoinc_lock_mode = 2 时,并且 binlog_format = row,将binlog日志格式设为row,日志中保存记录最终被修改后数据,既能提升并发性,又不会出现数据一致性问题。但也会出现binlog日志文件过大的问题,所以还是需要综合考虑,选择适合的方案。

行级锁

行级锁的类型主要有三类:

  • Record Lock,记录锁,对一条记录加锁;
  • Gap Lock,间隙锁,锁定一个范围,但是不包含记录本身;
  • Next-Key Lock:Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

Record Lock

记录锁,也分有S锁(共享锁)和X锁(独占锁):

  • 当事务对记录加了S型记录锁后,允许其他事务对该记录加S型记录锁,但是不可以加X型记录锁。
  • 当事务对记录加了X型记录锁后,不允许其他事务加S型和X型记录锁

Gap Lock

间隙锁是一种开区间的范围锁,只存在于可重复读隔离级别,主要作用是解决可重复读隔离级别下幻读的问题。

MySQL中的锁 如图所示,假设一个事务A查询时加了(3,5)的间隙锁,那么后面事务B就无法插入id为4的数据,当事务A再次查询时仍然还是两条记录,不会出现幻读现象。

Next-Key锁

Next-Key Lock 称为临键锁,左开右闭,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

MySQL中的锁 假设,表中有一个范围 id 为(3,5] 的 next-key lock,那么其他事务即不能插入 id = 4 记录,也不能修改 id = 5 这条记录。所以,next-key lock 既能保护该记录,又能阻止其他事务将新记录插入到被保护记录前面的间隙中。

插入意向锁

插入意向锁是在执行插入时判断插入位置是否已被其他事务加了间隙锁,如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻),在此期间会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态,锁结构如下图所示:

MySQL中的锁 MySQL 加锁时,是先生成锁结构,然后设置锁的状态,如果锁状态是等待状态,并不是意味着事务成功获取到了锁,只有当锁状态为正常状态时,才代表事务成功获取到了锁。

参考 xiaolincoding.com/mysql/ 小林code 《MySQL是怎样运行的:从根儿上理解MySQL》