重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
mysql 为并发事务同时对一条记录进行读写时,提出了两种解决方案:
创新互联公司成立与2013年,先为孟津等服务建站,孟津等地企业,进行企业商务咨询服务。为孟津企业网站制作PC+手机+微官网三网同步一站式服务解决您的所有建站问题。
1)使用 mvcc 的方法,实现多事务的并发读写,但是这种读只是“快照读”,一般读的是历史版本数据,还有一种是“当前读”,一般加锁实现“当前读”,或者 insert、update、delete 也是当前读。
2)使用加锁的方法,锁分为共享锁(读锁),排他锁(写锁)
快照读:就是select
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,处理的都是当前的数据,需要加锁。
mysql 在 RR 级别怎么处理幻读的呢?一般来说,RR 级别通过 mvcc 机制,保证读到低于后面事务的数据。但是 select for update 不会触发 mvcc,它是当前读。如果后面事务插入数据并提交,那么在 RR 级别就会读到插入的数据。所以,mysql 使用 行锁 + gap 锁(简称 next-key 锁)来防止当前读的时候插入。
Gap Lock在InnoDB的唯一作用就是防止其他事务的插入操作,以此防止幻读的发生。
Innodb自动使用间隙锁的条件:
锁是计算机协调多个进程或线程并发访问某一资源的机制,在数据库中,除传统的计算资源(CPU、RAM、I/O)争用外,数据也是一种供许多用户共享的资源,如何保证数据并发访问的一致性,有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素,从这个角度来说,锁对数据库而言是尤其重要,也更加复杂。MySQL中的锁,按照锁的粒度分为:1、全局锁,就锁定数据库中的所有表。2、表级锁,每次操作锁住整张表。3、行级锁,每次操作锁住对应的行数据。
全局锁就是对整个数据库实例加锁,加锁后整个实例就处于只读状态,后续的DML的写语句,DDL语句,已经更新操作的事务提交语句都将阻塞。其典型的使用场景就是做全库的逻辑备份,对所有的表进行锁定,从而获取一致性视图,保证数据的完整性。但是对数据库加全局锁是有弊端的,如在主库上备份,那么在备份期间都不能执行更新,业务会受影响,第二如果是在从库上备份,那么在备份期间从库不能执行主库同步过来的二进制日志,会导致主从延迟。
解决办法是在innodb引擎中,备份时加上--single-transaction参数来完成不加锁的一致性数据备份。
添加全局锁: flush tables with read lock; 解锁 unlock tables。
表级锁,每次操作会锁住整张表.锁定粒度大,发送锁冲突的概率最高,并发读最低,应用在myisam、innodb、BOB等存储引擎中。表级锁分为: 表锁、元数据锁(meta data lock, MDL)和意向锁。
表锁又分为: 表共享读锁 read lock、表独占写锁write lock
语法: 1、加锁 lock tables 表名 ... read/write
2、释放锁 unlock tables 或者关闭客户端连接
注意: 读锁不会阻塞其它客户端的读,但是会阻塞其它客户端的写,写锁既会阻塞其它客户端的读,又会阻塞其它客户端的写。大家可以拿一张表来测试看看。
元数据锁,在加锁过程中是系统自动控制的,无需显示使用,在访问一张表的时候会自动加上,MDL锁主要作用是维护表元数据的数据一致性,在表上有活动事务的时候,不可以对元数据进行写入操作。为了避免DML和DDL冲突,保证读写的正确性。
在MySQL5.5中引入了MDL,当对一张表进行增删改查的时候,加MDL读锁(共享);当对表结构进行变更操作时,加MDL写锁(排他).
查看元数据锁:
select object_type,object_schema,object_name,lock_type,lock_duration from performance_schema_metadata_locks;
意向锁,为了避免DML在执行时,加的行锁与表锁的冲突,在innodb中引入了意向锁,使得表锁不用检查每行数据是否加锁,使用意向锁来减少表锁的检查。意向锁分为,意向共享锁is由语句select ... lock in share mode添加。意向排他锁ix,由insert,update,delete,select。。。for update 添加。
select object_schema,object_name,index_name,lock_type,lock_mode,lock_data from performance_schema.data_lock;
行级锁,每次操作锁住对应的行数据,锁定粒度最小,发生锁冲突的概率最高,并发读最高,应用在innodb存储引擎中。
innodb的数据是基于索引组织的,行锁是通过对索引上的索引项加锁来实现的,而不是对记录加的锁,对于行级锁,主要分为以下三类:
1、行锁或者叫record lock记录锁,锁定单个行记录的锁,防止其他事物对次行进行update和delete操作,在RC,RR隔离级别下都支持。
2、间隙锁Gap lock,锁定索引记录间隙(不含该记录),确保索引记录间隙不变,防止其他事物在这个间隙进行insert操作,产生幻读,在RR隔离级别下都支持。
3、临键锁Next-key-lock,行锁和间隙锁组合,同时锁住数据,并锁住数据前面的间隙Gap,在RR隔离级别下支持。
innodb实现了以下两种类型的行锁
1、共享锁 S: 允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。
2、排他锁 X: 允许获取排他锁的事务更新数据,阻止其他事务获得相同数据集的共享锁和排他锁。
insert 语句 排他锁 自动添加的
update语句 排他锁 自动添加
delete 语句 排他锁 自动添加
select 正常查询语句 不加锁 。。。
select 。。。lock in share mode 共享锁 需要手动在select 之后加lock in share mode
select 。。。for update 排他锁 需要手动在select之后添加for update
默认情况下,innodb在repeatable read事务隔离级别运行,innodb使用next-key锁进行搜索和索引扫描,以防止幻读。
间隙锁唯一目的是防止其它事务插入间隙,间隙锁可以共存,一个事务采用的间隙锁不会阻止另一个事务在同一间隙上采用的间隙锁。
对表的增删改查,都需要MDL锁,无所不在
MDL读锁之间不互斥,但MDL读写锁互斥
#举个栗子
假设t是一张大表
session1对t执行一个查询(SR)
session2对t执行一个DDL(SU,可能升级到X)
session3对t执行一个查询(SR)
可知session1持有t表的MDL读锁(SR),session1的查询还没有结束的时候,去执行session2的DDL(SU),此时session2需要MDL写锁(SU升级到X,需要X锁),由于MDL读写锁互斥,因此session2需要等待session1释放MDL读锁(SR阻塞X);同时session2对后面的所有MDL读锁互斥(X阻塞SR),因此session2又继续阻塞了session3...
#注释:一开始的DDL能看到的状态是SU,但如果SU的某个阶段被阻塞,会被升级到X,从而引发SR阻塞X,达到实验的效果。但实际测试中,DDL是分阶段的,如果没有满足一定的要求,就不会引发阻塞,看到的结果就是SR和SU并没有互相阻塞。这个过程需要具体的去查看源码,此处不展开。
事务中的MDL锁在语句开始时申请,但并不会在语句结束后就马上释放,而是会等到事务结束时才进行释放
忙时对大表DDL会产生的灾难性的结果就是:如果后续对该表有查询操作,而且web端又有重试机制的话,那么会有一个新的session再次发起读请求,反复如此,线程池就会在短时间内爆炸
在线执行DDL的时候,需要检查一下information_schema.innodb_trx表中有没有当前操作表对应的事务,此外还可以使用ALTER TABLE tbl_name NOWAIT...进行操作(MySQL8.0新特性)
eg.
session1
select * from cpf where payid'xxx'
union
select * from cpf where payid'xxx'
union (union重复50次,确保查询时间几十秒以上)
session2
alter table cpf modify payer_userid varchar(500);
session3
select * from cpf where payer_userid='18051512003600300034';
#执行结果
session1执行了31秒,当session1完成的时候session2和session3相继完成
在session4中执行show processlist,结果如下
#变种1
如果session1在执行select之前,添加一句start transaction
会发现session1什么时候执行完commit,sesssion2和session3什么时候完成
也就是证实了在事务中的MDL锁,在语句查询完之后并不会释放,而是会随着事务的释放而释放
#变种2
session1和session3在执行select之前,添加一句start transaction,然后session1,2,3依次按顺序执行
会发现session1阻塞了session2,而session3在执行完start transaction之后就被阻塞,根本没有办法去执行后面的select
当session1执行commit释放之后,session2仍然处于阻塞状态,session3亦是如此
直到session2或者session3当中任意一个执行了停止(navicat客户端操作,类似于rollback)后,另一个才能完成执行
单纯从变种2的结果来看,MDL锁并没有按照执行时间的先后来进行分配,当session1的锁释放之后,session3先获得了读锁
MySQL是server-engine结构,MDL锁是server层的锁
通过show processlist可以发现waiting for table metadata lock,但这还远远不够,需要在performance_schema库中进行设置(MySQL8.0默认开启)
5.7临时开启
UPDATE performance_schema.setup_instruments SET ENABLED='YES', TIMED='YES' WHERE NAME='wait/lock/metadata/sql/mdl';
5.7永久开启(修改cnf配置)
[mysqld]
performance-schema-instrument = 'wait/lock/metadata/sql/mdl=ON'
global:全局级(FTWRL)
schema:库级(drop database)
table:表级(lock table read/write)
commit:提交级
关于global对象,主要作用是防止DDL和写操作的过程中,执行set golbal_read_only = on或flush tables with read lock。
关于commit对象锁,主要作用是执行flush tables with read lock后,防止已经开始在执行的写事务提交。insert/update/delete在提交时都会上(COMMIT,MDL_EXPLICIT,MDL_INTENTION_EXCLUSIVE)锁
DML和DDL在执行之前都会申请IX锁,DML会在global级别上加,而DDL会在global和schema这2个级别上都加IX(也就是2把锁)
IX与大部分锁都是兼容的,除了S,当然了X肯定是不兼容的;但IX与IX之间是兼容的,比如下图
flush table with read lock会持有这个锁(在global级别和commit级别)
FTWRL在全局级和事务级上分别加上了S锁
IX与S是不兼容的
所以DML和DDL都会与FTWRL产生阻塞
逻辑备份第一句:flush table with read lock(S锁)
大表DML(IX锁)
先执行的阻塞后执行的,逻辑备份之前需要检查是否有在线DDL(X锁)以及DML(IX锁),否则逻辑备份产生等待;尽量不要在忙时进行逻辑备份,否则阻碍忙时DML
如下图,前面2行是FTWRL持有的S锁,第3行是一个update语句,IX直接被阻塞,处于pending的锁等待状态;同时由于S锁的持有时间为EXPLICIT,表明FTWRL需要一个显示的释放(unlock tables)
DML并不是只有IX锁,DML和select .. for update在执行中持有的锁实际是SW锁(DML需要找一个大一点的表来验证,目前只验证了select .. for update),IX只是DML初期需要获得的锁
如下图是一个select for update语句,start transaction对应的是第2行的SR锁,而语句本身对应的是SW锁
如果在此时执行一个FTWRL,我们会发现2个会话并不会相互阻塞(因为S锁与SR和SW都是兼容的),如下图
但如果我们是先执行的FTWRL再执行的select for update,那么画风就不是像上图那样了
如下图所示,在先执行FTWRL的情况下,select for update压根没有获得SW锁,而是在获取IX锁的过程中就受挫了,一直处于pending状态。(如果这个S锁不释放,那么后面的IX会一直等待,直到超时)
S锁除了逻辑备份时的FTWRL以外,createa table as也会持有这个锁
目前已知的是desc操作会持有这个SH锁
SH锁与绝大部分锁都兼容,除开X锁
也就是说在做rename一类的操作的时候,你是无法去执行desc的
前面提到的start transaction,以及所有的非当前读都需要持有这个锁
非当前读的意思就是快照读,也就是普通的select
与SR锁有冲突的有2个,一个是X,另一个是SNRW
研发有时候会很困惑的问我,“我这个表只有几十行数据,select查不出来???” 这时候就需要检查MDL锁了
当前读需要持有此锁,常见的DML和select for update都对应此锁,但不包括DDL
与SW锁有冲突的有4个,SU,SRO,SNRW,X
看到一种说法是这个锁仅对MyISAM引擎生效,冲突范围与SW锁类似
部分alter语句会持有该锁。该锁可能会升级成SNW,SNRW,X;而X锁也有可能逐步降级到SU锁
SU锁和SU,SNW,SNRW,X锁互斥
表面看起来DML的SW锁和SU锁不互斥(DML和DDL),但实际上因为SU锁存在升级的属性,SU锁会升级到SNW锁,从而和SW产生互斥
如下图,SU并没有被SW锁阻塞,但升级到SNW之后,SNW被SW阻塞,一直处于pending状态
SU锁的兼容性如下
查看改过源码的例子,在执行alter的时候,SU会升级到X,之后X降级到SU,然后SU再升级到X
先SU,再SW,SW被SU阻塞
先SW,再SU,SU并未被SW阻塞,但是SU向上升级的过程中产生的SNW被SW阻塞;于是将SW的会话commit,之后SNW向下降级成SU,并成功获得锁;
所以虽然看起来SW和SU不是一个双向阻塞,但实际效果就是双向阻塞,无论DML和DDL谁在前面,都必然会发生相互的阻塞
不兼容的有点多,先贴一个兼容性
SU升级X的过程中会升级成SNW
SU升级成X的过程中,有一个copy的过程,这个过程就是SNW,在这个copy的过程中,允许DML但是不允许select(SR)
copy是一个非常耗时的过程
lock tables read的语句会持有这个锁
SRO阻塞SW,SNRW,X
兼容性如图
lock tables write的语句会持有这个锁
阻塞的锁非常多,除开SH和S以外,其他的都阻塞,连SR都阻塞了
兼容性如下
换句话说flush tables with read lock; (S)会堵塞lock table write; (SNRW)
但是flush tables with read lock;(S)却不会堵塞lock table read (SRO)
阻塞一切
各种DDL均属于这个范畴
create,drop,rename (alter table add column也属于这个范畴)
SW锁阻塞X锁,(X锁是为了去执行一个drop)
X锁阻塞SH
thread104在做一个create table as的表复制操作,在表里面并没有发现X锁的信息,在thread95上对新表做一个desc操作,可以看到SH锁处于等待状态,然而这里阻碍SH的并不是X锁
只有1行的select被堵住
thread95做一个start transaction之后不提交,thread107对95的表做出一个rename操作,X锁被前面的SR锁阻塞,这时候thread108对该表发起一个limit仅仅为1的查询,但被X锁阻塞。由于lock_wait_timeout这个参数通常是1年,所以一连串查询被堵死
alter开头的几个SQL,无论是modify还是add,查询出来都是SU锁,但DDL是一个过程,其中的有一部分如果发生了阻塞,可能会发现是X锁阻塞;拿SR阻塞X锁的实验来说,SR阻塞X的过程非常短暂,如果没有刚好卡到那个点,看到的结果可能就是SR和SU互不干涉,但如果卡到那个点,就会观测到X被SR所阻塞。具体的需要读源码,这里不展开
SELECT
locked_schema,
locked_table,
locked_type,
waiting_processlist_id,
waiting_age,
waiting_query,
waiting_state,
blocking_processlist_id,
blocking_age,
substring_index(sql_text,"transaction_begin;" ,-1)ASblocking_query,
sql_kill_blocking_connection
FROM
(
SELECT
b.OWNER_THREAD_IDASgranted_thread_id,
a.OBJECT_SCHEMAASlocked_schema,
a.OBJECT_NAMEASlocked_table,
"Metadata Lock"ASlocked_type,
c.PROCESSLIST_IDASwaiting_processlist_id,
c.PROCESSLIST_TIMEASwaiting_age,
c.PROCESSLIST_INFOASwaiting_query,
c.PROCESSLIST_STATEASwaiting_state,
d.PROCESSLIST_IDASblocking_processlist_id,
d.PROCESSLIST_TIMEASblocking_age,
d.PROCESSLIST_INFOASblocking_query,
concat('KILL', d.PROCESSLIST_ID)ASsql_kill_blocking_connection
FROM
performance_schema.metadata_locks a
JOINperformance_schema.metadata_locks bONa.OBJECT_SCHEMA=b.OBJECT_SCHEMA
ANDa.OBJECT_NAME=b.OBJECT_NAME
ANDa.lock_status='PENDING'
ANDb.lock_status='GRANTED'
ANDa.OWNER_THREAD_IDb.OWNER_THREAD_ID
ANDa.lock_type='EXCLUSIVE'
JOINperformance_schema.threads cONa.OWNER_THREAD_ID=c.THREAD_ID
JOINperformance_schema.threads dONb.OWNER_THREAD_ID=d.THREAD_ID
) t1,
(
SELECT
thread_id,
group_concat(CASEWHENEVENT_NAME='statement/sql/begin'THEN"transaction_begin"ELSEsql_textENDORDERBYevent_id SEPARATOR ";" )ASsql_text
FROM
performance_schema.events_statements_history
GROUPBYthread_id
) t2
WHERE
t1.granted_thread_id=t2.thread_id
MDL锁处理
MDL元数据锁
快速处理MDL锁