13、mysql锁
mysql锁
事务的隔离性是通过锁来实现的。为保证数据的一致性,需要锁对并发事务操作进行控制。同时锁机制也为实现MySQL的各个隔离级别提供了保证。
mysql并发事务访问相同的记录会出现什么问题(在没有锁的情况下)
1.1读-读情况
读-读情况:即并发事务相继读取相同的记录。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生。
1.2写-写情况
写-写情况:即并发事务相继对相同的记录做出改动。在这种情况下会出现脏写的问题。任何一种隔离级别都不允许脏写这种问题的发生,所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过锁来实现的。
1.3读-写或写-读情况
写-读情况:当一个事务执行修改操作,另一个事务执行读取操作。这种情况下会出现脏读的问题。
读-写情况:当一个事务执行读取操作,另一个事务执行修改操作。这种情况下会出现不可重复读和幻读的问题。
1.4针对以上并发问题的解决方案
解决脏写、脏读、不可重复读、幻读,有两种方案:
方案一:读操作利用MVCC多版本并发控制(快照读),写操作进行加排它锁(当前读)。
- 所谓的MVCC,就是生成一个ReadView,通过ReadView找到符合条件的记录版本(历史版本由undo日志构建)。查询语句只能读到在生成Readview之前已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突。
普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。
在 READ COMMITTED 隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改,也就是避免了脏读现象;
在 REPEATABLE READ 隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView,之后的SELECT操作都复用这个ReadView,这样也就避免了不可重复读和幻读的问题。
方案二:读操作和写操作都使用加锁的方式
如果我们一些业务场景不允许读取记录的旧版本,而需要每次都必须去读取记录的最新版本,这时读操作就需要采取加锁的方式解决。
- 脏读的产生是因为当前事务读取了另一个未提交事务写的一条记录
如果另一个事务在写记录的时候就给这条记录加排它记录锁,那么当前事务就无法继续读取该记录了,所以也就不会有脏读问题的产生了。
- 不可重复读的产生是因为当前事务先读取一条记录,另外一个事务对该记录做了改动之后并提交之后,当前事务再次读取时会获得不同的值,
如果在当前事务读取记录时就给该记录加排它记录锁,那么另一个事务就无法修改该记录,自然也不会发生不可重复读了。
- 幻读问题的产生是因为当前事务读取了一个范围的记录,然后另外的事务向该范围内插入了新记录,当前事务再次读取该范围的记录时发现了新插入的新记录。
如果在当前事务读取记录时就给该范围的记录加上排它间隙锁,那么另一个事务就无法插入新的记录,自然也不会发生幻读了
两种方案的对比:
- 方案一:采用MVCC的方式,读-写操作彼此并不冲突,并发执行效果好,性能高。
- 方案二:采用加锁的方式,读-写操作彼此需要排队执行,影响性能。
锁的不同分类
2.1从数据操作的类型划分:读锁、写锁
对于写-写、读-写或写-读这些情况可能会引起一些问题,需要使用MVCC或者加锁的方式来解决它们。在使用加锁的方式解决问题时,由于既要允许读-读情况不受影响,又要使写-写、读-写或写-读情况中的操作相互阻塞,所以MySQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为共享锁(Shared Lock,SLock)和排他锁(Exclusive Lock,XLock),也叫读锁((readlock)和写锁(write lock)。
- 读锁
1、针对读-读操作,读锁视为共享锁(用S表示)。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻塞的。
2、针对读-写操作,读锁视为排他锁(用X表示)。针对同一份数据,一个事务在进行读操作的时候,另一个事务不能进行读操作或者写操作
- 写锁
针对写-写、写-读操作,写锁视为排他锁(用X表示)。当前事务的写操作没有完成前,其他事务的写操作或读操作都不可以执行。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。
对于InnoDB引擎来说,共享锁和排他锁可以加在表上(加在表上是表锁),也可以加在行上(加载行上是行锁)。
针对共享锁和排他锁的兼容性(兼容是指对同一张表或者同一个记录是共享的,不互相制约)
2.1.1锁定读
采用加锁的方式解决脏读、不可重复读、幻读的问题时,读取一条记录时可以对该记录加共享锁(S锁)或者排他锁(X锁)
- 对读取的记录加共享锁(S锁):
select .....lock in share mode
或者select .....for share(mysql8.0的语法)
- 对读取的记录加排他锁(X锁):
select ..... for update
2.1.2写操作
当使用DML中的delete、update、insert操作修改数据时,会对该记录加排他锁(X锁)。
- delete
- update
- insert(隐式锁相当于是排他锁)
2.2从数据操作的粒度划分:表级锁、页级锁、行级锁
2.2.1表级锁
该锁会锁定整张表,它是MysQL中最基本的锁策略,并不依赖于存储引擎(不管你是MysQL的什么存储引擎,对于表锁的策略都是一样的),并且表锁是开销最小的策略(因为粒度比较大)。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题。当然,锁的粒度大所带来最大的负面影响就是出现锁资源争用的概率也会最高,导致并发率低。
一、表级别的S锁、X锁
1.1表级别的S锁和X锁的使用场景
1、在系统崩溃恢复的时候使用,在系统变量autocommit=0,innodb_table_locks=1时,手动获取InnoDB存储引擎提供的表t的S锁或者X锁
- LOCK TABLES t READ:InnoDB存储引擎会对表t加表级别的共享锁(S锁)
- LOCK TABLES t WRITE:InnoDB存储引擎会对表t加表级别的排他锁(X锁)
2、查看表上加过的锁
show open tables
:主要看In_use大于0的数据。
1.2mysql的表级锁有两种模式
- 表共享锁(表读锁)
- 表排他锁(表写锁)
二、意向锁
InnoDB支持多粒度锁(multiple granularity locking),它允许行级锁与表级锁共存,而意向锁是一种不与行级共享或排他锁冲突的表级锁,但是意向锁会与表级共享或排他锁发生冲突。意向锁是由存储引擎自己维护的,用户无法手动操作意向锁。
2.1意向锁分类:
- 意向共享锁(intention shared lock, IS):事务有意向对表中的某些行加共享锁(S锁)
- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
SELECT column FROM table ... LOCK IN SHARE MODE;
- 意向排他锁(intention exclusive lock, IX):事务有意向对表中的某些行加排他锁(X锁)
-- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
SELECT column FROM table ... FOR UPDATE;
2.2意向锁生成和要解决的问题
意向锁的生成:
- 如果我们给某一行数据加上了排他锁,数据库会自动给数据页或数据表加上意向排他锁,该意向锁会告诉其它事务这个数据表中的某些数据已经有事务上过排它锁了。
意向锁要解决的问题:
- 现在有两个事务,分别是T1和T2,其中T2试图在该表级别上应用共享或排它锁,如果T1没有生成意向锁,那么T2就需要去检查各个页或行是否存在锁;如果T1生成意向锁,那么此时就会受到由T1控制的表级别意向锁的阻塞。T2在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁,如果表级共享或排他锁与意向锁发生不兼容,该表级共享或排他锁就会阻塞。
意向锁和意向锁之间是兼容的
意向锁与表级共享或排他锁是不兼容的(意向锁与行级共享或排他锁是兼容的)
三、自增锁
3.1自增锁的介绍
- AUTO-INC锁是当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁
- 一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞
- 当我们向一个有AUTO_INCREMENT关键字的主键插入值的时候,每条语句都要对这个表锁进行竞争,这样的并发潜力其实是很低下的,所以innodb通过innodb_autoinc_lock_mode的不同取值来提供不同的锁定机制,来显著提高sql语句的可伸缩性和性能。
3.2插入数据的方式
- “Simple inserts” (简单插入)
可以预先确定要插入的行数(当语句被初始处理时)的语句。包括没有嵌套子查询的单行、多行没有指定自增值INSERT...VALUES() 和 REPLACE 语句。
- “Bulk inserts” (批量插入)
事先不知道要插入的行数 (和所需自动递增值的数量)的语句。比如 INSERT ... SELECT,REPLACE... SELECT 和 LOAD DATA 语句,但不包括纯INSERT。
- “Mixed-mode inserts” (混合模式插入)
这些是“Simple inserts”语句但是指定部分新行的自动递增值。例如 INSERT INTO teacher (id,name)VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d'); 只是指定了部分id的值。另一种类型的“混合模式插入”是 INSERT ... ON DUPLICATE KEY UPDATE 。
四、元数据锁
4.1元数据锁的介绍
- 在对某个表执行一些诸如ALTER TABLE、DROP TABLE这类的DDL语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL语句也会发生阻塞。这个过程其实是通过在server层使用一种称之为元数据锁 (英文名: Metadata Locks ,简称 MDL )结构来实现的。
- 当对一个表做增删改查操作的时候,加MDL读锁;当要对表做结构变更操作的时候,加MDL写锁。
- 元数据锁不需要显示使用,在访问一个表的时候会自动加上。MDL读锁与MDL读锁之间兼容、MDL读锁与MDL写锁之间不兼容、MDL写锁与MDL写锁之间不兼容。
4.2查看元数据锁
使用命令:show processlist
元数据锁可能带来的问题
2.2.2行级锁
行级锁并发效率高、开销大、有可能产生死锁。
一、记录锁(Record Locks)
- 记录锁也就是仅仅把一条记录锁上,官方的类型名称为: LOCK_REC_NOT_GAP 。比如我们把id值为8的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响。
- 记录锁解决了脏写、脏读、不可重复读
- 使用记录锁的举例
- 记录锁的分类:共享型记录锁(S型记录锁)和排他型记录锁(X型记录锁)
1、当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
2、当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
二、间隙锁(Gap Locks)
-
MySQL在REPEATABLE READ隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC方案解决,也可以采用加锁方案解决。
-
但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取id为5操作时,该幻影记录尚不存在,我们无法给这些幻影记录加上记录锁,那就需要id为8的值加一个间隙锁,意味着不允许别的事务在id值为8的记录前边的间隙插入新记录,其实就是id列的值(3, 8)这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为4的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3, 8)中的新记录才可以被插入。
-
当两个不同的事务同时使用共享间隙锁或者排他间隙锁对同一条记录进行加锁,是不会有冲突的,因为它们的间隙锁作用在间隙上,保护这个间隙,不允许插入值。
-
间隙锁可能出现死锁
三、临键锁(Next-Key Locks)
- Next-Key Locks是在存储引擎innodb、事务级别在可重复读的情况下使用的数据库锁,innodb默认的锁就是Next-Key locks。
- 临键锁是记录锁和间隙锁的合体,锁住的是某条记录和该记录前的间隙。
- 加临键锁语句
select * from student where id <=8 and id > 3 for update;
四、插入意向锁
- 一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了gap锁(next-key锁也包含 gap锁),如果有gap锁的情况下,插入操作就需要等待,同时对该insert插入操作生成一个插入意向锁(Insert Intention Locks)
- 插入意向锁是一种间隙锁,不是意向锁。
- 插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。
2.2.3页锁
- 页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。
锁升级
每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的 。当某个层级的锁数量 超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如 InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。
2.3从对待锁的态度划分:乐观锁、悲观锁
乐观锁和悲观锁并不是锁,而是锁的设计思想。
2.3.1悲观锁(Pessimistic Locking)
- 通过数据库自身的锁机制来实现,从而保证数据操作的排它性。
- 悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。Java中 synchronized 和 ReentrantLock 等独占锁就是悲观锁思想的实现。
select .. for update语句是悲观锁
,在执行过程中所有扫描的行都会被锁上,因此在MySQL中用悲观锁必须确定使用了索引,而不是全表扫描,否则将会把整个表锁住。
2.3.2乐观锁(Optimistic Locking)
- 不采用数据库自身的锁机制,而是通过程序来实现(版本号机制或CAS机制)
- 乐观锁不用每次都对数据上锁,但是在更新的时候使用版本号机制或CAS机制来判断一下在此期间别人有没有去更新这个数据。
2.3.4悲观锁和乐观锁的适用场景
- 乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
- 悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止写-读和写-写的冲突。
2.4按加锁的方式划分:显式锁、隐式锁
2.4.1显示锁
通过特定的语句进行加锁,我们一般称之为显示加锁,例如:
- 显示加共享锁
select .... lock in share mode
- 显示加排他锁
select .... for update
2.4.2隐式锁
- 一般情况下一个事务对新插入的记录不加锁,当其他的事务在对这条记录加S锁或者X锁时,会根据trx_id的状态为该新插入的记录生成一个隐式锁(排他锁),然后其他事务自己再生成一个锁结构后进入等待状态。
- 隐式锁是一种延迟加锁的机制,从而来减少加锁的数量。
- 只有其他事务在锁等待的时候,隐式锁才会转为显示锁。下面的命令是查看锁
2.5其他锁
2.5.1全局锁
- 全局锁就是对整个数据库实例加锁。当你需要让整个库处于只读状态的时候,可以使用这个命令,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。
- 全局锁的典型使用场景是全库逻辑备份 。
- 全局锁的命令:
Flush tables with read lock
2.5.2死锁
1、死锁的产生
- 两个或多个事务都持有对方需要的锁,但是都不释放,就会导致死锁。
- 事务1在等待事务2释放id=2的行锁,而事务2在等待事务1释放id=1的行锁。事务1和事务2在互相等待对方的资源释放,就是进入了死锁状态。
2、死锁的处理
- 直接进入等待,直到超时。这个超时时间可以通过参数innodb_lock_wait_timeout 来设置。
- 发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务(将持有最少行级排他锁的事务进行回滚),让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为on,表示开启这个逻辑。
3、如何避免死锁
- 合理设计索引,使业务SQL尽可能通过索引定位更少的行,减少锁竞争。
- 调整业务逻辑sQL执行顺序,避免update/delete长时间持有锁的SQL在事务前面。
- 避免大事务,尽量将大事务拆成多个小事务来处理,小事务缩短锁定资源的时间,发生锁冲突的几率也更小。
- 在并发比较高的系统中,不要显式加锁,特别是是在事务里显式加锁。如select ... for update语句,如果是在事务里运行了start transaction或设置了autocommit等于o,那么就会锁定所查找到的记录。
- 降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
锁的内存结构
锁监控
1、使用命令:show status like 'innodb_row_lock%';
- Innodb_row_lock_current_waits:当前正在等待锁定的数量;
- Innodb_row_lock_time :从系统启动到现在锁定总时间长度;(等待总时长)
- Innodb_row_lock_time_avg :每次等待所花平均时间;(等待平均时长)
- Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
- Innodb_row_lock_waits :系统启动后到现在总共等待的次数;(等待总次数)
2、其他监控方法
mysql5.7及之前:
- information_schema.INNODB_LOCKS:只能看到阻塞事务的锁
- information_schema.INNODB_LOCK_WAITS:查看等待的锁
mysql8.0 - performance_schema.data_locks:不但可以看到阻塞该事务的锁,还可以看到该事务所持有的锁。
- performance_schema.data_lock_waits:查看等待的锁