一、什么是锁
锁机制用于管理对共享资源的并发访问。数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性。
二、lock与latch的区别
在数据库中,lock和latch都可以被称为锁,但是两者有截然不同的含义。
latch一般称为闩锁(轻量级的锁),因为其要求锁定的时间必须非常短,若持续的时间长,会造成应用的性能非常差,在innodb存储引擎中,latch可以分为mutex(互斥量)和relock(读写锁),其目的是用来保证并发线程操作临界资源的正确性,并且通常没有死锁检测机制。
lock的对象是事务,用来锁定的是数据库中的对象,如表、页、行,并且一般lock的对象仅在事务commit或rollback后进行释放(不同事务隔离级别释放的时间可能不同),此外,lock锁有死锁检测机制。
| lock | latch | |
|---|---|---|
| 对象 | 事务 | 线程 |
| 保护 | 数据库内容 | 内存数据结构 |
| 持续时间 | 整个事务的过程 | 临界资源 |
| 模式 | 行锁、表锁、意向锁 | 读写锁、互斥量 |
| 死锁 | 通过 waits-for graph、 time out等机制进行无死锁检测与处理机制。 | 无死锁检测与处理机制。仅通过应用程序加锁的顺序( lock leveling)保证无死锁的情况发生 |
| 存在于 | Lock Manager的哈希表中 | 每个数据结构的对象中 |
三、Lock锁简介
innodb存储引擎支持多粒度锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在;如果将上锁的对象看做一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象上锁,则需要先获取对应粗粒度对象的锁。例如:如果需要对页记录上的记录R进行上排它锁,那么就需要对数据库A、表、页上意向锁排他锁,最后对记录R上排它锁。若其中任何一部分导致等待,那么该操作需要等待粗粒度多的完成。
四、锁请求信息
在innodb1.0版本之前,用户只能通过命令show full processlist,show engine innodb status来查看当前数据库中锁的请求,然后在判断事务锁的情况。在innodb1.0之后,在information_schema架构下添加了表innodb_trx、innodb_locks、innodb_lock_waits。通过这三张表,用户可以更简单的监控当前事务并分析可能存在的锁问题。
4.1、表innodb_trx 事务与锁的关联关系表
| 字段名 | 说明 |
|---|---|
| trx_id | innodb存储引擎内部唯一的事务id |
| trx_state | 当前事务的状态 |
| trx_started | 事务的开始时间 |
| trx_requested_lock_id | 等待事务的锁ID,如trx_state的状态为lock_wait,那么该值代表当前的事务等待之前事务占用锁资源的id,若trx_state不是lock_wait,则该值为null |
| trx_wait_started | 事务等待开始的时间 |
| trx_weight | 事务的权重,反映了一个事务修改和锁住的行数,在innodb存储引擎中,当发生死锁需要回滚时,innodb存储引擎会选择该值最小的进行回滚 |
| trx_mysql_thread_id | mysql中的线id |
| trx_query | 事务运行的sql |
4.2、表innodb_locks 事务持有的锁信息
| 字段名 | 说明 |
|---|---|
| lock_id | 锁的id |
| lock_trx_id | 事务id |
| lock_mode | 锁的模式 |
| lock_type | 锁的类型,表锁还是行锁 |
| lock_table | 要加锁的表 |
| lock_index | 锁住的索引 |
| lock_space | 锁对象的space id |
| lock_page | 事务锁定页的数量,若是表锁,则该值为null |
| lock_rec | 事务锁定行的数量,若是表锁,则该值为null |
| lock_data | 事务锁定记录的主键值,若是表锁,则该值为null |
4.3、表innodb_lock_waits
| 字段名 | 说明 |
|---|---|
| requesting_trx_id | 申请锁资源的事务id |
| requesting_lock_id | 申请的锁的id |
| blocking_trx_id | 阻塞的事务id |
| blocking_lock_id | 阻塞的锁的id |
五、innodb存储引擎锁分类
innodb是mysql最常用的存储引擎,在高并发场景下,有着非常优秀的性能,之所以如此,跟innodb锁机制密切相关,总的来说,innodb共有七种锁类型。
- 共享锁/排他锁(Shared and Exclusive Locks)
- 意向锁(Intention Locks)
- 记录锁(Record Locks)
- 间隙锁(Gap Locks)
- 临键锁(Next-key Locks)
- 自增锁(Auto-inc Locks)
-
5.1、共享锁/排他锁(Shared and Exclusive Locks)
innodb存储引擎实现了如下两种标准的行级锁
共享锁(S Lock):允许事务读一行数据
- 排它锁(X Lock):允许事务删除或更新一行数据
|
X | S | | | —- | —- | —- | | X | 不兼容 | 不兼容 | | S | 不兼容 | 兼容 |
5.2、意向锁
innodb存储引擎的意向锁设计的比较简单,意向锁即为表级别的锁,设计的主要目的是为了在一个事务中揭示下一行将被请求的锁类型,共支持两种意向锁
- 意向共享锁(IS Lock):事务想要获得一张表中某几行的共享锁
- 意向排他锁(IX Lock):事务想要获得一张表中某几行的排它锁
5.2.1、意向锁协议
- 一个事务必须先持有该表上的 IS 或者更强的锁才能持有该表中某行的 S 锁
Before a transaction can acquire shared lock on a row in a table, it must first acquire an IS lock or stronger on the table.
- 一个事务必须先持有该表上的 IX 锁才能持有该表中某行的 X 锁
Before a transaction can acquire an exclusive lock on a row in a table, it must first acquire an IX lock on the table.
5.2.2、意向锁作用
意向锁的目的是为了表明某个事务正在锁定一行或者将要锁定一行。
当再向一个表添加表级X锁的时候
- 如果没有意向锁的话,则需要遍历所有整个表判断是否有行锁的存在,以免发生冲突
- 如果有了意向锁,只需要判断该意向锁与即将添加的表级锁是否兼容即可。因为意向锁的存在代表了,有行级锁的存在或者即将有行级锁的存在。因而无需遍历整个表,即可获取结果
5.2.3、兼容性
意向锁是比较弱的锁,他们之间相互兼容
| IX | IS | |
|---|---|---|
| IX | 兼容 | 兼容 |
| IS | 兼容 | 兼容 |
意向锁与表级的S锁与X锁的兼容性
| S | X | IS | IX | |
|---|---|---|---|---|
| S | 兼容 | 不兼容 | 兼容 | 不兼容 |
| X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
| IS | 兼容 | 不兼容 | 兼容 | 兼容 |
| IX | 不兼容 | 不兼容 | 兼容 | 兼容 |
5.3、记录锁、间隙锁、临键锁
记录锁(record locks):单个行记录上的锁,锁定索引行
间隙锁(gap locks):锁定一个范围,但不包含记录本身,主要作用是防止其它事务在索引记录中的间隔内部插入数据,而引起的幻读。
临键锁(next-key lock): 记录锁 + 间隙锁,锁定一个范围,并锁定记录本身。主要作用同样是为了防止其它事务在索引记录中的间隔内部插入数据,而引起的幻读
5.4、插入意向锁
mysql官网对插入意向锁的解释如下:
An insert intention lock is a type of gap lock set by INSERT operations prior to row insertion. This lock signals the intent to insert in such away that multiple transactions inserting into the same index gap need not wait for each other if they are not inserting at the same positionwithin the gap. Suppose that there are index records with values of 4 and 7. Separate transactions that attempt to insert values of 5 and 6,respectively, each lock the gap between 4 and 7 with insert intention locks prior to obtaining the exclusive lock on the inserted row, but do notblock each other because the rows are nonconflicting.
插入意向锁是一种在插入行之前由insert操作产生的间隙锁,此锁表示插入的意图,即如果插入到同一索引间隙中的多个事务未插入到间隙内的同一位置,则它们无需相互等待。 假设有值为 4 和 7 的索引记录。分别尝试插入值 5 和 6 的单独事务,在获得插入行的排他锁之前,每个事务都会锁定 4 和 7 之间的间隙, 但不要相互阻塞,因为行是不冲突的。
- 插入意向锁是一种特殊的间隙锁,是行级别的锁,与意向锁不同,意向锁是表级别的锁
- 插入意向锁之间互不排斥,所以多个事务在同一个区间同时插入多个记录,只要记录本身(主键or唯一索引)不冲突,那么事务之间不会出现互相等待。
5.4、自增锁
自增锁是一种比较特殊的表级锁,专门针对事务插入AUTO_INCREMENT类型的列。实现列的自增长算法有三种,通过参数innodb_autoinc_lock_mode来控制
- innodb_autoinc_lock_mode = 0,传统模式(auto-inc locking模式,自增锁模式)
- innodb_autoinc_lock_mode = 1,连续模式,默认模式
- innodb_autoinc_lock_mode = 2,交叉模式
可通过show variables like ‘%innodb_autoinc_lock_mode%’; 查看值。
5.4.1、传统模式(auto-inc locking模式,自增锁模式)
在innodb存储引擎的内存结构中,对每个含有自增长值的表都有一个自增长计数器(auto-increment counter),当对含有自增长计数器的表进行插入操作时,这个计数器会被初始化,执行如下语句:
select MAX(auto_inc_col) from t for update
插入操作会依据这个自增长计数器获得的值加1在赋值给自增长列,这个实现方式被称作(auto-inc locking),为了提高插入的性能,锁不是在一个事务完成后才释放,而是在完成对自增长值插入的sql语句后立即释放。示例如下user表
| id(自增) | name | age |
|---|---|---|
| 1 | zs | 10 |
| 2 | ls | 20 |

auto-inc locking的工作模式如下图:一定程度上提高了并发写入的性能,但是性能上还是存在问题,一个事务的写入必须等待另一个事务sql语句写入的完成,其次,对于insert…..select的大数据量的插入性能并不理想,因为另一个事务的插入会被阻塞。
5.4.2、连续模式
由于传统模式存在性能的弊端,所以mysql5.1.22版本后,innodb存储引擎提供了一种轻量级互斥量的自增长实现模式,这种模式可大大提供自增长写入的性能。通过互斥量对内存中的计数器进行累加操作。
mysql默认采用连续模式innodb_autoinc_lock_mode = 1,生成自增主键。在这种模式下生成自增主键有两种策略
- 如果insert语句能够提前确定插入的条数,如insert、insert batch,则可以不需要获取自增锁,可通过互斥量(mutex锁)的模式获取自增主键。
如果insert语句不能提前确认插入数据的条数,如insert…select,则依然采用传统模式(自增锁模式)。
5.4.3、交叉模式
在交叉模式下,所有的insert语句都不会使用自增锁(auto-inc),而是使用轻量级的mutex锁,这样多条insert语句就可以并发的执行。
优点:性能高;
缺点:单个insert语句的获取的自增值可能不连续(一条insert语句需要插入多行的情况下)
- 主从一致性无法保证,mysql主从复制基于binlog,binlog有三种格式,分别为statement、row、mixedstatement:基于语句复制,master只记录对数据做了修改的sql语句,slave重放sql语句row:基于行的赋值,master的记录sql修改后的数据,slave直接对数据进行回放,无需执行sql语句mix:混合模式,基于语句和行的混合赋值模式。默认采用基于语句的复制,一旦发现基于语句的无法精确的复制时,就会采用基于行的复制。
如果mysql采用statement模式进行主从复制,那么mysql主从同步的是一条条的sql语句,由于采用交叉模式,那么在并发条件下,则无法保障sql语句执行的顺序,就会造成同一条数据在master和slave上的主键不同。
5.4.3、交叉模式
在交叉模式下,所有的insert语句都不会使用自增锁(auto-inc),而是使用轻量级的mutex锁,这样多条insert语句就可以并发的执行。
优点:性能高;
缺点:
- 单个insert语句的获取的自增值可能不连续(一条insert语句需要插入多行的情况下)
- 主从一致性无法保证,mysql主从复制基于binlog,binlog有三种格式,分别为statement、row、mixedstatement:基于语句复制,master只记录对数据做了修改的sql语句,slave重放sql语句row:基于行的赋值,master的记录sql修改后的数据,slave直接对数据进行回放,无需执行sql语句mix:混合模式,基于语句和行的混合赋值模式。默认采用基于语句的复制,一旦发现基于语句的无法精确的复制时,就会采用基于行的复制。
如果mysql采用statement模式进行主从复制,那么mysql主从同步的是一条条的sql语句,由于采用交叉模式,那么在并发条件下,则无法保障sql语句执行的顺序,就会造成同一条数据在master和slave上的主键不同。
六、加锁协议
6.1、两阶段(2PL)加锁协议
mysql的innodb存储引擎采用两阶段加锁协议(2PL)实现事务的隔离性和一致性,两阶段锁是指在一个事务中,分为加锁(lock)和解锁(unlock)两个阶段,也就是说所有的加锁操作会在所有的解锁操作之前。mysql对于两阶段锁的应用如下图:
从上图看,mysql的加锁时间是从begin开启事务时,但是这并不严谨,真正的加锁起始时间是开启事务后,执行第一条需要加锁的sql时;解锁时间是事务提交或者回滚(commit or rollback)时。
6.2、两阶段加锁协议对性能的影响
方案一
begin;
update product set count = count - 1 where id = 2 and count > 1;
select * from user_account where account_id = 10 for update;
insert into order values();
commit;
方案二
begin;
select * from user_account where account_id = 10 for update;
insert into order values();
update product set count = count - 1 where id = 2 and count > 1;
commit;
上述方案一、方案二的sql由于在同一个事务内,并且sql语句相同,所以对数据库的操作是等价的,但是在两阶段加锁情况下,性能是有比较大的差距,时序图如下:
由于商品库存是热点,存在多线程并发访问同一个商品的问题,所以这种热点数据通常会成为系统的性能瓶颈,采用方案一库存会被锁住3rt的时间,采用方案二会锁住1rt的时间;所以我们在写代码的时候对于热点数据的加锁尽量放在事务的后面,这样可提供吞吐量。
