0%

MySQL锁机制

MySQL锁机制

锁机制是数据库为了保证数据的一致性而使各种共享资源在被并发访问变得有序所设计的一种规则,锁的作用主要是管理共享资源的并发访问,事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁

由于Mysql中存在多种存储引擎,而每种存储引擎所应对的场景不同,所以各存储引擎的锁机制也有较大区别,总的来说MySQL中的锁按照类型上分为共享锁(读锁)、独占锁(写锁)、悲观锁(for update)、乐观锁(使用version,来进行CAS操作);按照锁的粒度分为表锁、行锁和页锁

按照算法可以分为Record Lock(单行记录)、Gap Lock(间隙锁,锁定一个范围,不包括锁定记录,间隙锁主要用于范围查询的时候,锁住查询的范围)、Next-Key Lock(Record Lock+Gap Lock,锁定一个范围,包括记录本身)

按照锁类型

表锁(偏读)

表锁是MySQL各存储引擎中最大粒度的锁定机制,实现逻辑简单,带来的系统负面影响最小,所以获取锁和释放锁的速度很快,且由于表锁一次会将整个表锁定,所以可以很好的避免死锁,但是由于锁粒度大导致锁定资源争用的概率最高,致使并发度很低

总结下来就是:表锁开销小,加锁快;无死锁;锁的粒度大,并发小

使用表锁的存储引擎一般是非事务性存储引擎,如MyISAM、Memory、CSV等存储引擎

表锁的底层实现主要是通过四个队列来维护读锁和写锁的

  • Current read-lock queue 当前持有的读锁
  • Pending read-lock queue 等待中的读锁
  • Current write-lock queue 当前持有的写锁
  • Pending write-lock queue 等待中的写锁

为表加锁

1
2
# 为user表加上读锁,good表加上写锁
lock table user read,good write;

解锁

1
unlock tables;

查看当前哪些表在使用中

1
2
3
4
show open tables;
show open tables where in_use > 0;
+--------------------+------------------------------------------------------+--------+-------------+
| Database | Table | In_use | Name_locked |

分析表锁定

1
2
3
4
5
6
7
8
9
10
 show status like 'table_lock%';
+-----------------------+-------+
| Variable_name | Value |
+-----------------------+-------+
| Table_locks_immediate | 122 |
| Table_locks_waited | 0 |
+-----------------------+-------+

Table_locks_immediate 产生表级锁的次数
Table_locks_waited 出现由于表级锁导致等待的次数

加锁和解锁的操作都是MySQL底层隐式执行的,MyISAM存储引擎中,虽然读写操作是串行化的,但是它也支持并发插入,这个需要设置内部变量concurrent_insert的值

1
show VARIABLES like '%concurrent_insert%'

concurrent_insert有三种值

值为NEVER (or 0)表示不支持并发插入;

值为AUTO(或者1)表示在MyISAM表中没有被删除的行(数据文件中间不存在空闲空间),可以从文件尾部插入数据;

值为ALWAYS (or 2)表示不管是否有删除的行,都允许在文件尾部插入数据

读锁

一个新的客户端申请获取读锁时,需要满足两个条件

  • 请求锁定的资源当前没有写锁定
  • 写锁定等待队列(Pending write-lock queue) 中没有更高优先级的写锁定等待

如果在整张表上加了读锁,当前会话不可以修改该表数据,不可以查询/修改其他表数据,只能读取该表数据;其他会话可以查询该表数据,可以查询/修改其他表数据,但是如果修改该表数据,会进行阻塞,直到解锁为止

写锁

如果在整张表加了写锁,当前会话可以查询/修改该表数据,但是不可以对其他表进行操作;其他会话在对该表进行查询/修改时会进行阻塞,直到解锁为止,可以查询/修改其他表

行锁(偏写)

行锁最大的特点是锁定对象的粒度很小,所以发生锁定资源争用的概率也最小,能够给予应用程序尽可能大的并发处理能力而提高一些需要高并发应用系统的整体性能;但是由于粒度很小,所以每次获取锁和释放锁需要做的事情也更多,带来的消耗更大,而且最容易发生死锁

总结下来就是:行锁开销大,加锁慢;会出现死锁;锁粒度小,并发高

使用行锁的存储引擎主要是Innodb和NDB Cluster存储引擎

行锁中分为了共享锁和排他锁,而且为了让行锁和表锁共存,Innodb中又加入了意向锁的概念,所以还有意向共享锁和意向排他锁

当一个事务需要给自己需要的某个资源加锁时,如果遇到一个共享锁正锁定着自己所需要的资源时,自己可以再加一个共享锁,不能加排他锁;如果遇到一个排他锁正锁定着自己所需要的资源时,则只能等待该锁释放之后才能添加自己的锁

而意向锁的作用就是当一个事务需要获取资源锁的时候,如果遇到自己需要的资源已经被排他锁占用,则该事务可以在所需要锁定行的表上添加一个合适的意向锁

innodb的行锁是通过在指向数据记录的第一个索引键之前和最后一个索引键之后的空域空间上标记锁定信息而实现的,而这种锁定方式称为Next-key locking,也就是间隙锁

  • [ ] 这是在5.1中,不知道后续版本有没有将行锁和间隙锁分开
间隙锁

当我们使用范围条件查询而不是等值条件查询的时候,InnoDB就会给符合条件的范围索引加锁,在条件范围内并不存的记录就叫做间隙(GAP)

间隙锁是在RR(Repeatable Read可重复读)隔离级别下使用的

MySQL 把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key锁

Mysql中的不可重复是已经解决了某种情况下的幻读问题,它通过引入Next-key Lock的实现来解决幻读,通过给符合条件的间隙加锁,防止再次查询的时候出现新数据产生幻读的问题

1
2
3
4
// 给查询sql显示添加读锁,共享锁
select ... lock in share mode;
// 给查询sql显示添加写锁,执行非索引条件查询执行的是表锁,排他锁
select ... for update;

可以设置锁的超时时间

1
2
3
4
5
-- 设置锁的超时时间(单位为秒)
set innodb_lock_wait_timeout=100;

-- 查询锁的超时时间
show variables like '%innodb_lock_wait_timeout%';

查看innodb中锁的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- 已获取的锁的信息 mysql8之后使用`performance_schema`.data_locks

select * from information_schema.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
---

-- 当前运行事务的信息
select * from information_schema.innodb_trx;
---
trx_id 事务id
trx_state 当前事务状态
trx_started 事务的开始时间
trx_requested_lock_id 等待事务的锁id,只有trx_state为lock wait状态时
trx_wait_started 事务开始等待的时间
trx_weight 事务的权重
trx_mysql_thread_id 线程id
trx_query 事务运行的sql语句
---

-- 等待的锁的信息 mysql8之后使用`performance_schema`.data_lock_waits
select * from information_schema.innodb_lock_waits;
---
requesting_trx_id 申请锁资源的事务id
requested_lock_id 申请的锁的id
blocking_trx_id 持有锁的事务id
blocking_lock_id 阻塞的锁的id
---

分析行锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
show status like 'innodb_row_lock%';
+-------------------------------+-------+
| Variable_name | Value |
+-------------------------------+-------+
| Innodb_row_lock_current_waits | 0 |
| Innodb_row_lock_time | 0 |
| Innodb_row_lock_time_avg | 0 |
| Innodb_row_lock_time_max | 0 |
| Innodb_row_lock_waits | 0 |
+-------------------------------+-------+


Innodb_row_lock_current_waits 当前正在等待锁定的数量
Innodb_row_lock_time 锁定的总时间总时间长度
Innodb_row_lock_time_avg 每次等待所花平均长度
Innodb_row_lock_time_max 等待的最长时间
Innodb_row_lock_waits 等待的次数

页锁

页锁是MySQL中比较独特的一种锁,锁定的粒度介于行锁和表锁之间,所以锁定所需要的资源开销,以及所能提供的并发处理能力也同样介于两者之间,且会发生死锁

总结下来就是:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

使用页锁的存储引擎主要是BerkeleyDB存储引擎

悲观锁和乐观锁

悲观锁

悲观锁特点:先获取锁,再进行业务操作

即“悲观”的认为获取锁是非常有可能失败的,因此要先确保获取锁成功再进行业务操作。通常所说的“一锁二查三更新”即指的是使用悲观锁。通常来讲在数据库上的悲观锁需要数据库本身提供支持,即通过常用的select … for update操作来实现悲观锁

当数据库执行select for update时会获取被select中的数据行的行锁,因此其他并发执行的sql语句如果试图选中同一行则会发生排斥(需要等待行锁被释放),因此达到锁的效果。select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用

MySQL中select for update语句执行中所有扫描过的行都会被锁上,这一点很容易造成问题。因此如果在MySQL中用悲观锁务必要确定走了索引,而不是全表扫描

乐观锁

乐观锁,也叫乐观并发控制,它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,那么当前正在提交的事务会进行回滚

乐观锁的特点先进行业务操作,不到万不得已不去拿锁。即“乐观”的认为拿锁多半是会成功的,因此在进行完业务操作需要实际更新数据的最后一步再去拿一下锁就好
乐观锁在数据库上的实现完全是逻辑的,不需要数据库提供特殊的支持

一般的做法是在需要锁的数据上增加一个版本号,或者时间戳,在查询时查出来该版本号select id,name,version from table where id = 1 ,进行业务操作后进行修改时,带上版本号修改update table set name='修改后的值',version=version+1 where id = 1 and version=刚查出来的version,这样就可以防止并发更新了且没有加锁

三种算法

Record Lock(单行记录)

Record Lock总是会去锁住索引记录

Gap Lock(间隙锁)

Gap Lock间隙锁,锁定一个范围,不包括锁定记录,间隙锁主要用于范围查询的时候,锁住查询的范围

Next-Key Lock(Record Lock+Gap Lock)

Next-Key Lock,锁定一个范围,包括记录本身,innodb对于行的查询都是采用这种锁定算法,但是在查询的索引中含有唯一索引时innodb会对Next-Key Lock进行优化,降级为Record Lock,仅仅锁住索引本身,而不是范围

Next-Key Lock在REPEATABLE READ下解决了幻读(幻读是指在同一事务下,连续执行两次相同的sql语句可能导致不同的结果,第二次sql语句可能会返回之前不存在的行)

死锁

数据库加锁可能会导致死锁,而解决死锁最简单的方法就是超时innodb_lock_wait_timeout设置超时时间

欢迎关注我的其它发布渠道