MySQL InnoDB存储引擎中的锁机制
1.隔离级别
Read Uncommited(RU):这种隔离级别下,事务间完全不隔离,会产生脏读,可以读取未提交的记录,实际情况下不会使用。
Read Committed (RC):仅能读取到已提交的记录。针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁),存在幻读现象。所谓幻读是指在同一个事务中,多次执行同一个查询,返回的记录不完全相同的现象。幻读产生的根本原因是,在RC隔离级别下,每条语句都会读取已提交事务的更新,若两次查询之间有其他事务提交,则会导致两次查询结果不一致。虽然如此,读提交隔离级别在生产环境中使用很广泛。
Repeatable Read (RR):针对当前读,RR隔离级别保证对读取到的记录加锁 (记录锁),同时保证对读取的范围加锁,新的满足查询条件的记录不能够插入 (间隙锁),不存在幻读现象。mysql的innodb比较特殊,RR即解决了幻读问题,主要通过间隙锁实现。
Serializable:从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。
查看隔离级别:
show variables like 'tx_isolation'; SELECT @@tx_isolation; SELECT @@session.tx_isolation; SELECT @@global.tx_isolation;
修改隔离级别:
用户可以用SET TRANSACTION语句改变单个会话或者所有新进连接的隔离级别。它的语法如下:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
2. 一致性非锁定读和一致性锁定读
一致性非锁定读:
指InnoDB存储引擎通过行多版本控制的方式来读取当前执行时间数据库中行的数据。如果读取的行正在执行delete或update操作,这时读取操作不会因此去等待行上锁的释放。InnoDB存储引擎会去读取行的一个快照数据。快照数据是指该行的之前版本的数据,该实现是通过undo段来完成的。读取快照数据是不需要上锁的,因为没有事务需要对历史数据进行修改操作。在InnoDB存储引擎的默认设置下,这是默认的读取方式,即读取不会占用和等待表上的锁。
一致性锁定读:
在某些情况下,用户需要显式的对数据库读取操作进行加锁,以保证数据逻辑的一致性。InnoDB存储引擎对于select支持两种一致性的锁定读操作。
select...for update 对读取的行记录加一个X锁
select...lock in share mode 对读取的行记录加一个S锁
3. 行锁的三种算法
record lock:记录锁,单个记录上的锁;record lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,则会使用隐式的主键来进行锁定。
gap lock:间隙锁,锁定一个范围,但是不包含记录本身;
next-key lock:gap lock+record lock,锁定一个范围,并锁定记录本身。设计的目的是解决幻读问题。
当查询的索引含有唯一属性时,InnoDB存储引擎会对next-key lock进行优化,将其降级为record lock,即仅锁住索引本身,而不是范围。若是辅助索引,则加的是next-key lock,特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock。
可以通过下面两种方式来显式的关闭gap lock:
(1)将事务的隔离级别设置为Read Committed;
(2)将参数innodb_locks_unsafe_for_binlog设置为1.
4. 查看锁请求的信息
(1)show engine innodb status\G;
(2)show full processlist;
(3)查看information_schema下的表INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS;
5. 锁相关的状态变量
mysql> show status like 'innodb_row_lock%'; +-------------------------------+--------+ | Variable_name | Value | +-------------------------------+--------+ | Innodb_row_lock_current_waits | 0 | | Innodb_row_lock_time | 490578 | | Innodb_row_lock_time_avg | 37736 | | Innodb_row_lock_time_max | 121411 | | Innodb_row_lock_waits | 13 | +-------------------------------+--------+
Innodb 的行级锁定状态变量不仅记录了锁定等待次数,还记录了锁定总时长,每次平均时长,以及最大时长,此外还有一个非累积状态量显示了当前正在等待锁定的等待数量。对各个状态量的说明如下:
- Innodb_row_lock_current_waits:当前正在等待锁定的数量;
- Innodb_row_lock_time:从系统启动到现在锁定总时间长度;
- Innodb_row_lock_time_avg:每次等待所花平均时间;
- Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
- Innodb_row_lock_waits:系统启动后到现在总共等待的次数;
对于这5 个状态变量, 比较重要的主要是Innodb_row_lock_time_avg(等待平均时长) ,Innodb_row_lock_waits(等待总次数)以及Innodb_row_lock_time(等待总时长)这三项。尤其是当等待次数很高,而且每次等待时长也不小的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。
6. 合理使用InnoDB
(1)尽可能让所有的数据检索都通过索引来完成;
(2)合理设计索引,让Innodb 在索引键上面加锁的时候尽可能准确,尽可能的缩小锁定范围,避免造成不必要的锁定而影响其他Query 的执行;
(3)尽可能减少基于范围的数据检索过滤条件,避免因为间隙锁带来的负面影响而锁定了不该锁定的记录;
(4)尽量控制事务的大小,减少锁定的资源量和锁定时间长度;
(5)在业务环境允许的情况下,尽量使用较低级别的事务隔离,以减少MySQL 因为实现事务隔离级别所带来的附加成本;
几篇非常好的博客:
一个最不可思议的MySQL死锁分析:http://hedengcheng.com/?p=844
MySQL加锁处理分析:http://hedengcheng.com/?p=771
Innodb锁系统(1)之如何阅读死锁日志:http://mysqllover.com/?p=411