数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则。
MySQL 数据库由于其自身架构的特点,存在多种数据存储引擎,每种存储引擎的锁定机制都是为各自所面对的特定场景而优化设计,所以各存储引擎的锁定机制也有较大区别。
1. 锁的分类
按照锁的粒度:
- 行锁:MySQL 中锁定粒度最小的一种锁,是针对索引字段加的锁,只针对当前操作的行记录进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。
- 表锁:MySQL 中锁定粒度最大的一种锁,是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM 和 InnoDB 引擎都支持表级锁。
- 页锁:页的粒度上进行锁定,锁定资源和开销介于行锁和表锁之间,并发度一般。
总的来说,MySQL 这 3 种锁的特性可大致归纳如下:
- 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;使用表级锁定的主要是
MyISAM
,MEMORY
,CSV
等一些非事务性存储引擎。 - 行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高;使用行级锁定的主要是
InnoDB
存储引擎。 - 页面锁:开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般。使用页级锁定的主要是
BerkeleyDB
存储引擎。
按数据库管理角度:
- 共享锁:也叫读锁或S锁,锁定的资源可以被其他用户读取,但不能修改。
- 排它锁:也叫写锁或X锁,锁定的资源只允许进行锁定的事务操作,其他事务无法进行读取和修改。
- 意向锁:简单说就是给上一级的空间示意里面是否已经上过锁,如需要用到表锁的话,如何判断表中的记录没有行锁呢,使用意向锁可以来快速判断。
- 当有多个事务对同一数据获得读锁的时候,可能出现死锁的情况。
# 共享锁
SELECT ... LOCK IN SHARE MODE;
# 排他锁
SELECT ... FOR UPDATE;
按程序员的角度:
- 乐观锁:认为数据的变动不会太频繁,因此允许多个事务同时对数据进行变动。一般采用版本号机制或CAS算法实现。思想是有线程过来,先放过去修改,如果别的线程没修改过,就能修改成功,否则就失败。 比如数据库提供的类似于
write_condition
机制,Java API 并发工具包下面的原子变量类就是使用了乐观锁的CAS(Compare and Swap,比较并交换)
来实现的。 - 悲观锁:只专注于当前事务,当一个事务拥有悲观锁后,任何其他事务都不能对数据进行修改,只能等待锁被释放。Java API 中的
synchronized
和ReentrantLock
等独占锁都是悲观锁思想的实现。
举一个版本号的例子,
数据表中除了数据还有一个version字段,更新数据时version字段会加一,
假设线程A在读取数据和version(version = 1)的期间,有另一个线程B也读取了version(version = 1),
线程A修改数据,更新version(version = 2),提交更新时,在更新version前读取的version(version = 1)和当前数据表中的version(version = 1)相同,则更新成功
线程B也修改数据,更新version(version = 2)提交更新时,由于读取时version = 1 而当前数据表version = 2 不相等,则更新失败,
根据前面对两种锁的介绍,总结一下两种锁的应用场景:
- 乐观锁,它适用于读多写少的情况,也就是说减少操作冲突,这样可以省去锁竞争的开销,提高系统的吞吐量。
- 而悲观锁呢,它适用于写多读少的情况。因为,如果还使用乐观锁的话,会经常出现操作冲突,这样会导致应用层会不断地 Retry,反而会降低系统性能。
思考一刻:你认为秒杀场景下,并发下单和支付应该使用什么锁?
- 使用乐观锁并发下单
- 使用悲观锁发起支付
2. InnoDB 有哪几类行锁?
MySQL InnoDB 支持三种行锁定方式:
- 记录锁(Record Lock) :也被称为记录锁,属于单个行记录上的锁。
- 间隙锁(Gap Lock) :锁定一个范围,不包括记录本身。
- 临键锁(Next-key Lock) :Record Lock+Gap Lock,锁定一个范围,包含记录本身。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
InnoDB 的默认隔离级别 RR(可重读)是可以解决幻读问题发生的,主要有下面两种情况:
- 快照读(一致性非锁定读) :由 MVCC 机制来保证不出现幻读。就是单纯的 SELECT 语句
- 当前读 (一致性锁定读): 使用 Next-Key Lock 进行加锁来保证不出现幻读。就是给行记录加 X 锁或 S 锁。
这里对 记录锁、间隙锁、临键锁 做一个总结:
- InnoDB 中的 行锁 的实现依赖于 索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
- 记录锁 存在于包括 主键索引 在内的 唯一索引 中,锁定单条索引记录。
- 间隙锁 存在于 非唯一索引 中,锁定开区间范围内的一段间隔,它是基于 临键锁 实现的。
- 临键锁 存在于 非唯一索引 中,该类型的每条记录的索引上都存在这种锁,它是 一种特殊的间隙锁,锁定一段 左开右闭 的索引区间。
3. CAS 是什么?
CAS(compare and swap) 比较并交换,依赖CPU的CAS指令在更新数据前先判断数据是否被其他线程修改过,如果没有修改则更新,整个操作是原子操作。
CAS指令执行时,比较内存值与期望值是否相等,若相等则用目标值替换当前值,(不相等则会循环比较直到相等)整个比较赋值操作是一个原子操作;
过程:
1、取出内存值,比如求a++,首先取a此时的内存值0
2、计算目标值,计算a+1=1,此值是基于a=0得到
3、调用CAS指令
4、取出当前a的内存值,与期望值比较,如果相等,则用目标值替换。
5、如果不一致,则说明其他线程已经更改,转到第一步重新执行。
CAS有三个缺点:
1,CAS自旋操作:当内存地址V与预期值B不相等时会一直循环比较直到相等,
2,只能保证一个共享变量的原子操作,
3,出现ABA问题:如果内存值初次读取的时候为A,在将要赋值的时候再次检查还是A,能说明V没有改变过吗?
有一种可能时当读取内存值V的时候是A,有一个线程将A改为B,后有改为A,CAS会误认为内存值V没有改变,这称为CAS操作的ABA问题;
4. 意向锁有什么作用
如果需要用到表锁的话,如何判断表中的记录没有行锁呢?一行一行遍历肯定是不行,性能太差。我们需要用到意向锁来快速判断是否可以对某个表使用表锁。
意向锁是表级锁,共有两种:
- 意向共享锁(Intention Shared Lock,IS 锁):事务有意向对表中的某些加共享锁(S 锁),加共享锁前必须先取得该表的 IS 锁。
- 意向排他锁(Intention Exclusive Lock,IX 锁):事务有意向对表中的某些记录加排他锁(X 锁),加排他锁之前必须先取得该表的 IX 锁。
意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
5. 死锁是什么,怎么解决?
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致的恶性循环现象。
常用解决方法:
- 如果不同程序并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁的概率。
- 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁发生概率。
- 对于非常容易发生死锁的业务部分,尝试使用升级锁的粒度,通过表级锁来减少死锁发生的概率。