20.锁问题
锁是计算机协调多个进程或线程并发访问某一资源的机制。
20.1 Mysql锁概述
锁类型分为表级锁、页面锁、行级锁。
表级锁:一个线程对表进行DML时会锁住整张表,其它线程只能读该表,如果要写该表时将产生锁等待。
优点:开销少、加锁快、不会产生死锁。缺点:锁粒度大、容易产生锁等待、并发低。
行级锁:一个线程对表进行DML时会锁住该表影响的行,其它线程可以读该表,也可以DML该表其它的行,如果要DML已被锁定的行时将产生锁等待。
缺点:开销大、加锁慢、会产生死锁。优点:锁粒度小、不容易产生锁等待、并发高。
页面锁:一个线程对表进行DML时会锁住该表影响行的所在页面(dbblock),其它线程可以读该表,也可以DML该表其它的页面(dbblock),如果要DML已被锁定的页面(dbblock)时将产生锁等待。
页面锁介于表级锁和行级锁中间,会产生死锁,并发一般,表只有一个页面(dbblock)时等同于表级锁。
Mysql不同的存储引擎支持不同的锁机制。
MyISAM和MEMORY存储引擎支持表级锁;
BDB存储引擎支持页面锁;
INNODB存储引擎支持行级锁。
20.2 MyISAM表锁
20.2.1 查询表级锁争用情况
状态变量table_locks_immediate:立即加锁的次数
状态变量table_locks_waited:产生锁等待的次数
20.2.2 Mysql表级锁的锁模式
共享读锁lock table 表名 read:一个进程的读操作,不阻断其它进程的读操作,但阻断其它进程的写操作。
独占写锁lock table 表名 write:一个进程的写操作,阻断其它进程的读操作和写操作。
读操作之间的并行的,读写操作之间是串行的,写操作之间是串行的。
释放锁unlock tables:释放所有锁。
20.2.3 如何加表锁
select 语句会自动给涉及的表加读锁;
insertupdatedelete 语句会自动给涉及的表加写锁;
也可通过命令 lock table 表名 read/write 显示的加读/写锁。
lock table 表名 read local; --增加local选项指读操作只锁定表中现有记录,其它进程可以增加新的记录,但不能删除、修改现有记录。
读写操作加锁时会给涉及的所有表加锁,如果其中哪个表已经被其它进程锁定(且非共享读锁)时,将产生锁等待。
Mysql不支持锁升级,使用lock table 表名 read加锁后,进程只能访问被自己加锁的表(不能访问未加锁的表),且必须按已加锁的锁模式来访问(加的读锁时不能进行写操作)。
使用lock table 表名 read加锁后,不能对表起别名。
20.2.4 并发插入(Concurrent Inserts)
系统变量concurrent_insert=[0|1|2] 控制并发插入行为。指表被读锁锁定时,是否允许其它进程在表的末尾进行插入数据。
0 不允许并发插入;1 表没有空洞(中间被删除的行)时允许在表尾进行并发插入;2 允许在表尾进行并发插入。
optimize table命令可以整理空间碎片,回收因删除操作产生的空洞。
20.2.5 MyISAM的锁调度
MyISAM的锁调度策略:读写互斥,读写串行,写锁优先。
问题1:存在大量DML操作时,DQL操作很难获得读锁,长时间被阻塞。
启动参数low-priority-updates,系统层面降低写请求优先级
set low-priority-updates=1 会话层面降低写请求优先级
在insert/update/delete语句后增加low-priority属性 语句层面降低写请求优先级
系统参数max_write_lock_count=x, 当一个表的读锁达到阈值x时,Mysql暂时降低写请求优先级,给读进程一个获得锁的机会。
问题2:耗时长的读进程,也会导致写进程所等待并超时。
应尽量避免耗时长的查询,通过SQL优化或将复杂SQL拆分为多条SQL语句。
20.3 InnoDB锁问题
InnoDB支持事务采用了行级锁。
20.3.1 背景知识
1.事务及其ACID属性
事务是一组SQL语句组成的逻辑处理单元,事务具有原子性(Atomicity)、一致性(Consistent)、隔离性(Isolation)、持久性(Durable)4个特性。
原子性:事务中的一组SQL要么全部执行,要么全部回退,不能部分执行。
一致性:事务开始和完成时数据必须保持一致状态。
隔离性:事务在一个session中运行,不受其它session影响,事务的中间状态对外不可见。
持久性:事务完成后数据的修改是永久性的。
2.并发事务处理带来的问题
更新丢失:多个事务基于同一行的更新操作可能相互覆盖。通过锁机制消除。
脏读:一个事务修改了数据,尚未提交完成时,另一个事务读取了修改后的数据,称为脏读。通过快照提供读一致性。
不可重复读:一个事务中两次读取同一个数据,读取结果不同时,称为不可重复读。
幻读:一个事务中两次读取同一个数据,读取结果数量不同时,称为幻读。
3.事务的隔离级别
脏读、不可重复读、幻读都是读一致性问题,可以通过事务的隔离机制里解决。
数据库实现事务隔离的方式分为:锁和快照。
锁方式事务隔离:在读取数据前对其加锁,防止其它事务对数据进行修改。
快照方式事务隔离:生成一个数据请求时间点的一致性数据快照,使用快照来提供一定级别的读一致性。
隔离性越严格,并发的问题越少,并发量越低。事务隔离实质就是将并发事务串行化执行。
ISO SQL92定义了4个事务隔离级别:
读未提交Read uncommitted:存在脏读、不可重复读、幻读
读已提交Read committed :不存在脏读,存在不可重复读、幻读
可重复读Repeatable read :不存在脏读、不可重复读,存在幻读
可序列化Serializable :不存在脏读、不可重复读、幻读
Mysql提供参数tx_isolation设置事务隔离级别,默认为可重复读Repeatable-read。
20.3.2 获取InnoDB行锁争用情况
状态变量:
InnoDB_row_lock_waits 锁等待次数
InnoDB_row_lock_time 锁等待时间
InnoDB_row_lock_time_avg 锁等待平均时间
InnoDB_row_lock_time_max 锁等待单次最大时间
InnoDB_row_lock_current_waits 当前的锁等待次数
如果锁等待次数和锁等待平均时间的值比较高,说明系统存在严重的锁争用。
可以通过information_schema模式下的innodb_locks和innodb_lock_waits表查看。
也可以创建innodb_monitor表监控发生锁冲突的表和数据行:
create table innodb_monitor(a int)engine=innodb;
打开监控后通过show engine innodb status;命令,会输出详细的锁等待信息,包括:表名、锁类型、锁定的记录等。
打开监控后15秒后系统向.err文件记录监控内容,.err文件会急剧增加,确定问题后需要停止监控,即删除innodb_monitor表。
停止监控时需要drop table innodb_monitor;
20.3.3 InnoDB的行锁模式及加锁方法
InnoDB的行锁模式分为共享锁、排他锁、意向共享锁、意向排他锁。
共享锁(S): 允许一个事务读一行,阻止其它事务写这一行
排他锁(X): 允许一个事务写一行,阻止其它事务读这一行或写这一行
意向共享表锁(IS): 事务在加S锁之前,应先取得表的IS锁
意向排他表锁(IX): 事务在加X锁之前,应先取得表的IX锁
锁兼容:如果一个事务请求的锁模式与当前的锁模式兼容,InnoDB就将请求的锁授予该事务。
锁等待:如果一个事务请求的锁模式与当前的锁模式不兼容,InnoDB就将该请求挂起,等待当前的锁释放,产生锁等待。
IS S IX X
IS ok ok ok no
S ok ok no no
IX ok no ok no
X no no no no
意向锁是InnoDB自动添加的,不需要用户干预。
insert/update/delete/select ... for update 会自动添加X锁。
select 不会添加任何锁。
手动加S锁方法:select * from 表名 where ... lock in share mode;
手动加X锁方法:select * from 表名 where ... for update;
20.3.4 InnoDB行锁实现方式
InnoDB行锁是给索引上的索引项加锁实现的。
如果没有索引,InnoDB将通过隐藏的聚簇索引来对记录加锁。
Record lock:对索引项加锁
Gap lock:对索引项之间的间隙(包括:第一条记录前的间隙和最后一条记录后的间隙)加锁。
Next-key lock:对记录及其前面的间隙加锁。
在不通过索引条件检索数据时,则对表中所有记录加锁。
select * from emp where empno=7788 for update; --empno列有索引,所以只对emp表中7788一行加锁
select * from emp where ename='scott' for update; --ename列没有索引,所以对emp表中所有记录加锁
对唯一索引的某个键值加锁时只锁定一行,对非唯一索引的某个键值加锁时将锁定这个键值对应的多行记录。
当表中有多个索引时,不同的事务可以使用不同索引锁定不同的行,但不能是同一行。无论使用的索引的类型。
即便在SQL的where条件中使用了索引列,但是否使用索引由Mysql执行计划决定,如果索引失效将对表中全部记录加锁。
20.3.5 Next-Key锁
在使用范围条件并请求共享锁或排他锁时,InnoDB会给符合条件的已有记录的索引项加锁,
对于符合条件但并不存在的记录称为“间隙”,InnoDB会给间隙也加锁,这种机制称为Next-Key锁。
Next-Key锁的作用在于防止幻读,并满足Mysql恢复和复制的需要。
业务中应尽量使用等值条件,避免使用范围条件,以降低Next-Key锁等待。
在等值条件中请求给一个不存在的记录加锁,InnoDB也会使用Next-Key锁。
20.3.6 恢复和复制的需要,对InnoDB锁机制的影响
Mysql通过binlog记录执行成功的DML操作,并由此实现Mysql的数据库恢复和主从复制。
binlog日志格式分为:基于语句的日志格式SBL、基于行的日志格式RBL和混合格式。
Mysql 4中复制模式:基于SQL语句的复制SBR、基行数据的复制RBR、混合复制模式和使用全局事务ID(GTIDs)的复制。
基于SQL语句的复制SBR要求:一个事务未提交前,其它并发事务不能插入满足其锁定条件的任何记录,即不允许出现幻读。
即使在其它隔离级别下,InnoDB均会使用Next-Key锁保证复制的串行化。
对复制语句create table tab_name select ...和insert into table_name select ... Mysql为保证恢复和复制的正确性增加了共享锁。
系统参数innodb_locks_unsafe_for_binlog=[off|on] 默认未off。
当设置未on时,给复制语句不加锁,但有可能导致复制不一致。
20.3.7 InnoDB在不同隔离级别下的一致性读及锁的差异
锁和快照是Innodb实现一致性读和SQL92隔离级别的方法,
不同隔离级别下Innodb处理SQL时采用的一致性读策略和需要的锁是不同的。
读未提交 读已提交 可重复度 可序列化
select 相等 None locks None locks None locks None locks
select 范围 None locks None locks None locks S Next-Key
insert X X X X
update 相等 X X X X
update 范围 X Next-Key X Next-Key X Next-Key X Next-Key
delete 相等 X X X X
delete 范围 X Next-Key X Next-Key X Next-Key X Next-Key
replace 无键冲突 X X X X
replace 键冲突 X Next-Key X Next-Key X Next-Key X Next-Key
lock in S 相等 S S S S
lock in S 范围 S S S Next-Key S Next-Key
for update 相等 X X X X
for update 范围 X S(X) X Next-Key X Next-Key
复制数据 off S Next-Key S Next-Key S Next-Key S Next-Key
复制数据 on None locks None locks None locks S Next-Key
复制表 off S Next-Key S Next-Key S Next-Key S Next-Key
复制表 on None locks None locks None locks S Next-Key
说明:
lock in S 指 手动添加S锁 select * from 表名 where ... lock in share mode;
for update 指 手动添加X锁 select * from 表名 where ... for update;
复制数据 指 insert into 表名 select ...
复制表 指 create table 表名 select ...
on 指 系统参数innodb_locks_unsafe_for_binlog=on
off 指 系统参数innodb_locks_unsafe_for_binlog=off
None locks 指 没有加任何锁
S 指 Share lock 行级共享锁
X 指 Exclusive lock 行级排他锁
S Next-Key 指 Share Next-Key 行级共享间隙锁
X Next-Key 指 Exclusive Next-Key 行级排他间隙锁
相等 指 条件中使用主键列的等值比较,即只处理一条记录
范围 指 条件中使用主键列的范围比较或非主键列比较或未使用索引,即处理多条记录
注意:隔离级别越高,加锁越严格,锁冲突的可能性就越高,并发性越低。
设置隔离级别的方法:
1.修改my.cnf文件中tx_isolation系统参数
tx_isolation=READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE;
2.动态修改参数
set [session|gloabl] tx_isolation=READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE;
3.在程序中执行
set session transactionn isolation READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE;
20.3.8 什么时候使用表锁
事务和行锁是Innodb被选择的原因,个别特殊事务中可以考虑使用表级锁。
场景1:事务需要更新大部分或全部数据(大量数据)时,使用表级锁可以提升性能。
场景2:事务涉及多个表且逻辑复杂,很可能一起死锁而造成大量事务回滚时,使用表级锁可以避免死锁,减少回滚。
使用表级的方法:
set autocommit=0; -- 关闭自动提交功能,否则Mysql不会给表加锁
lock tables 表名1 write|read,...; -- 添加表级锁,可以一次给多个表添加
要处理的事务SQL
commit; -- 提交或回退事务,commit或rollback不能释放表级锁
unlock tables; -- 释放全部表级锁,unlock tables可以隐式提交事务
说明:
表锁不是InnoDB存储引擎管理的,而是由Mysql Server管理,
仅当autocommit=0(默认为1)、innodb_table_locks=1(默认为1)时,
InnoDB引擎才能感知Mysql server是否添加了表锁,Mysql server才能感知InnoDB引擎是否添加了行锁。
否则,InnoDB将无法自动检测和处理因Mysql server添加了表锁而与已有行锁的冲突以及死锁。
20.3.9 关于死锁
MyIASM引擎只有表级锁,且需要一次获取所有的锁,要么全部满足,要么等待,不会产生死锁。
InnoDB引擎是行级锁,锁是由事务中的单条SQL逐步获取的,所以可能会产生死锁。
死锁是指AB两个事务各有一个排他锁,且需要获得对方已占有排他锁,才能结束自身的事务,此时产生的相互的锁等待称为死锁。
InnoDB可以自动检测死锁,并回退其中一个事务,释放该事务占有的锁,使另一个事务获取要请求的锁,继续完成该事务。
在涉及外部锁和表锁时,InnoDB并不能检测到死锁,而是通过锁等待超时参数innodb_lock_wait_timeout来释放等待超时的事务的锁来解决死锁问题。
大量事务的锁等待会占用大量计算机资源,引起性能问题,所以要设置合适的锁等待超时参数innodb_lock_wait_timeout阈值。
死锁一般都是应用设计的问题,可以通过调整业务流、数据库对象、事务大小、以及SQl语句来解决,从而从根本上避免死锁。
减少死锁方法:
1.程序并发存取多个表时,应尽量约定以相同的顺序来访问表。
2.在批量处理数据时,应对数据排序,以固定的顺序来处理记录。
3.在事务中应该直接申请足够级别的锁,避免逐步的锁升级。
4.将默认隔离级别由REPEATABLE READ降低至READ COMMITTED。
5.事务报错后应使用rollback语句释放已获得的排他锁。
6.可以通过show innodb status;命令分析最后一个死锁产生的原因,
包括:引发死锁的SQL语句,事务已获得的锁,正在等待的锁,以及被回滚的事务等。
依此分析死锁产生的原因和改进措施。
20.4 小结
减少锁等待和死锁次数的措施:
1.尽量使用较低的隔离级别;
2.精心设计索引,并尽量使用索引访问数据,使加锁更加准确,从而减少锁冲突的机会;
3.选择合理的事务大小,小事务发生锁冲突的几率更小;
4.给记录集显示加锁时,最好一次性请求足够级别的锁,而不是逐步进行锁升级;
5.不同程序访问一组表时,应尽量约定以相同的顺序访问各个表,对一个表而言,尽可能以固定的顺序存取表中的行;
6.尽量使用相等条件访问数据,避免Next-Key锁的影响;
7.不要申请超过实际需要的锁级别,除非必须,否则查询时不要显示加锁;
8.对特定事务,可以使用表锁来提高处理速度或减少发生死锁的几率。