MySQL 为了解决事务并发、数据安全
的问题,提供并使用了锁机制
,下面先看一张图,了解一下 MySQL 有哪些锁:
接下来,一起深入学习 MySQL 的锁机制。
1. 锁的分类
1.1 行级锁
行级锁
应用在 InnoDB
存储引擎中,每次锁住一行数据。
- 优点:锁定粒度小,发生锁冲突的概率最低,并发度最高。
- 缺点:开销大,加锁慢;会出现死锁。
1.2 页级锁
页级锁
应用在 DBD
存储引擎中,每次锁住一页数据 - 16KB左右。
- 特点:开销和加锁时间介于表级和行级之间;会出现死锁,锁定力度介于表锁和行锁之间,并发度一般。
1.3 表级锁
表级锁
应用在 MyISAM、InnoDB、BDB
等存储引擎中,每次操作锁住整张表。
- 优点:开销小,加锁快;不会出现死锁。
- 缺点:锁定粒度大,发生锁冲突的概率最高,并发度最低。
2. 行级锁的分类
接下来进行行级锁
的详解,行级锁主要分为以下7
类:共享/排他锁、意向锁、记录锁、间隙锁、临建锁、插入意向锁、自增锁。
2.1 共享锁、排他锁
共享锁(S锁)
:共享锁(Share Locks,简记为S锁)又被称为读锁
,其他事务可以并发读取数据,但任何事务都不能获取数据上的排他锁(只能加共享锁,不能加排他锁),直到已释放所有共享锁。
若事务 T 对数据对象 A 加上 S锁,则事务 T 只能读 A;其他事务只能再对 A 加 S锁,而不能加 X锁,直到T释放A上的S锁。这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
select ... lock in share mode;
排他锁(X锁)
:排它锁((Exclusive lock,简记为X锁))又称为写锁
,不能允许读,也不能允许写,排他锁不能与其他锁一起使用。
若事务T对数据对象 A 加上 X锁,则只允许 T 读取和修改 A,其它任何事务都不能再对 A 加任何类型的锁,直到 T 释放 A 上的锁。它防止任何其它事务获取资源上的锁,直到在事务的末尾将资源上的原始锁释放为止。在更新操作(INSERT、UPDATE 或 DELETE)过程中始终应用排它锁。
select ... for update;
在 MySQL
中,update
,delete
,insert
,alter
这些写的操作默认都会加上排他锁。select
默认不会加任何锁类型。一旦写数据的任务没有完成,数据是不能被其他任务读取的,这对并发操作有较大的影响。
2.2 意向锁
InnoDB
为了支持多粒度的锁,即允许行级锁和表级锁共存,而引入意向锁。意向锁是指未来的某个时刻,事务可能要加共享/排他锁,先提前声明一个意向。这样如果有人尝试对全表进行修改,就不需要判断表中的数据是否被加锁了,只需要通过等待意向互斥锁被释放就行了。
意向共享锁(IS
):事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。
意向互斥锁(IX
):事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁。
意向锁其实不会阻塞全表扫描之外的任何请求,它们的主要目的是为了表示是否有人请求锁定表中的某一行数据。
2.3 记录锁(RS)
单个行记录上的锁。记录锁总是会锁住索引记录,如果 InnoDB
存储引擎表
在建立的时候没有设置任何一个索引,那么InnoDB
存储引擎会使用隐式的主键来进行锁定。
2.4 间隙锁(GR)
间隙锁锁住记录中的间隔,即范围查询的记录。
select * From user where id between 1 and 10 for update;
这个脚本会锁住 1 到 10 的数据,以防止其他事务修改该区间的记录;
间隙锁的主要目的,就是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。如果把事务的隔离级别降级为读提交(Read Committed
, RC
),间隙锁则会自动失效。
2.5 临建锁(next-key Locks)
临建锁是记录锁和间隙锁的组合,锁的范围既包含记录又包含索引区间。默认情况下,InnoDB
使用临建锁来锁定记录。但当查询的索引含有唯一属性的时候,临建锁会进行优化,将其降级为记录锁,即仅锁住索引本身,不是范围。
临键锁的主要目的,也是为了避免幻读(Phantom Read
)。如果把事务的隔离级别降级为 RC
,临键锁则也会失效。
2.6 插入意向锁(insert intention locks)
对已有数据行的修改和删除,必须加互斥锁,对于数据的插入,加插入意向锁。是专门针对于 insert
操作的。
2.7 自增锁(auto-inc locks)
是一种特殊的表级别的锁,专门针对事务插入 auto-increment
类型的列。最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
3. 悲观锁和乐观锁
3.1 悲观锁
悲观的假定
大概率会发生并发更新冲突,访问、处理数据前就加排他锁
,在整个数据处理过程中锁定数据,事务提交或回滚后才释放锁。
- 优点:
悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。
- 缺点:
(1)在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会;
(2)在只读型事务处理中由于不会产生冲突,也没必要使用锁,这样做只能增加系统负载;还有会降低了并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数
案例:
- 假定 MySQL 有客户端A 与客户端B 同时开启事务
- 在 A 里面进行查询数据:select * from tab where id = 1 for update;
- 由于 B端 还没有提交事务,A端 则会在执行这条 SQL 语句时锁住(卡住不动),B端 结束了事务,A端 才会继续执行 SQL。
3.2 乐观锁
乐观的假定
大概率不会发生并发更新冲突,访问、处理数据的过程中不加锁,只在更新数据时根据版本号
或时间戳
判断是否有冲突,有则处理,无责提交事务。
如果系统并发量非常大,悲观锁会带来非常大的性能问题,选择使用乐观锁,现在大部分应用属于乐观锁
下单案例:
- 查询商品信息
select (quantity,version) from products where id=1;
- 根据商品信息生成订单
insert into orders ... insert into items ...
- 修改商品库存
update products set quantity = quantity - 1,version = version + 1 where id = 1 and version = #{version};
除了自己手动实现乐观锁之外,许多数据库访问框架也封装了乐观锁的实现,比如 hibernate 框架。MyBatis 框架大家可以使用 OptimisticLocker 插件来扩展。