一、锁的分类
二、从数据操作的类型划分:读锁、写锁
在并发事务中读-读
并不会引起什么问题。对于写-写
、读-写
、写-读
可能会引起一些问题,需要使用mvcc或加锁解决。由于既要允许读-读
情况不受影响,所以mysql实现了一个由两种类型的锁组成的锁系统来解决。通常被称为共享锁(Share Lock)
和排他锁(Exclusive Lock)
,也叫读锁和写锁。
(1) 对记录加S锁
SELECT ... LOCK IN SHARE MODE;或者SELECT ... FOR SHARE;(8.0)
(2) 对记录加X锁
SELECT ... FOR UPDATE;(8.0)
在mysql5.7及之前版本,如果获取不到锁,会一直等待,直到innodb_lock_wait_timeout
超时。在8.0中,如果后面加nowait、skip locked可以跳过等待,或者只返回没锁定的行。
三、从数据操作的粒度划分:表级锁、页级锁、行锁
锁定的数据范围越小,往往系统需要耗费更高的资源。所以数据库系统需要在高并发响应
和系统性能
两方面进行平衡,这样就产生了锁粒度
的概念。
1. 表锁
该锁会锁定整张表,并不依赖存储引擎。表锁可以避免死锁问题,但是并发率大打折扣。
表锁分为:表级别的s锁、x锁
、意向锁
、自增锁
、元数据锁
1.1 表级别的s锁、x锁
一般情况下,不会使用innodb存储引擎提供的表级s锁和x锁。在一些特殊情况下,比如崩溃恢复过程中用到。在系统遍历autocommit = 0,innodb_table_locks=1
时,手动获取表锁方式未:
LOCK TABLES t READLOCK TABLES t WRITEunlock tables : 解锁当前加锁的表show open tables : 查看表当前是否加锁
1.2 意向锁
意向锁是一种表锁,它的存在是为了协调行锁和表锁关系的,它不与行锁冲突,表明某个事务正在某些行持有了锁。
意向锁分为意向共享锁(IS)和意向排他锁(IX)。
如果我们给某一行数据加上了排他锁,数据库会自动给更大一级的空间加上意向锁。
1.3 自增锁(AUTO-INC锁)
所有插入数据的方式总共分三类,分别是:
- Simple inserts (简单插入),可以预先确定要插入的行数。
- Bulk inserts (批量插入),事先不知道要插入的行数。
- Mixed-mode-inserts (混合模式插入),只指定了部分id的值,还有未知id。
在插入时,mysql采用自增锁的方式来实现。当向使用auto_increment列插入数据时需要获取一种特殊的表级锁,在插入语句时加一个自增锁。然后再语句执行后,再把自增锁释放掉。一个事务再持有锁时,其他事务的插入语句都要被阻塞,所以并发性并不高。所以innodb通过innodb_autoinc_lock_mode
的不同取值来提供不同的锁定机制。
- 0 (传统锁定模式),并发差,就如上面所说的流程。
- 1 (连续锁定模式) ,mysql8.0之前默认的模式。对于插入数量已知情况下,只在分配过程中保持,而不是直到语句完成。
- 2 (交错锁定模式),在这种模式下,所有类insert语句都不会使用表级自增锁。自动递增保证在所有并发执行中是唯一且单调递增的。但是可能存在间隙。
1.4 元数据锁(MDL锁)
当对一个表做增删改查操作的时候,加MDL读锁;当要对表的结构变更时,加MDL写锁。
用来解决DML和DDL操作之间一致性问题,不需要显式使用。
2. 行锁
行锁也成为记录锁,mysql服务器层并没有实现行锁机制,行锁只在存储引擎层实现。
InnoDB与MyISAM的最大不同有两点:一是支持事务,二是采用了行级锁。
行锁分为:记录锁
、间隙锁
、临键锁
和插入意向锁
。
2.1 记录锁
官方的类型名称为:LOCK_REC_NOT_GAP
,用来锁住一条记录的。
记录锁分为S型记录锁
和X型记录锁
。
2.2 间隙锁
MYSQL在RR隔离级别下是可以解决幻读的。解决方案有两种,第一种是MVCC,第二种是加间隙锁。对一条记录加了gap锁,并不会限制其他事务对这条记录加记录锁或者gap锁。
- 索引上的等值查询(唯一索引),给不存在的记录加锁时, 优化为间隙锁 。
- 索引上的等值查询(普通索引),向右遍历时最后一个值不满足查询需求时,next-key lock 退化为间隙锁。
- 索引上的范围查询(唯一索引)–会访问到不满足条件的第一个值为止。
2.3 临键锁
临键锁可以理解为一种特殊的间隙锁,上面说过了通过临建锁可以解决幻读的问题。 每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
2.4 插入意向锁
在插入一条记录时,如果插入位置被别的事务加了gap锁,那么就需要等待。在等待时,innodb规定必须再内存中生成一个锁结构,表明有事务再等待。把这种类型的锁命名为Insert intention locaks
。
插入意向锁是一种特殊的间隙锁。
3. 行锁
页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁多,开销介于表锁和行锁之间,会出现死锁。
每个层级的锁数量是有限制的,应为锁会占用空间,锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁。
四、从对待锁的态度划分:乐观锁、悲观锁
1 悲观锁
悲观锁总是假设最坏的情况,每次去拿数据都认为别人会修改,所以每次在拿数据的时候都会上锁。(每次只有一个线程使用,其他线程阻塞)。比如行锁、表锁都是在操作前就上锁,其他资源都需要阻塞。
java中的synchronized和reentrantlock等独占锁就是悲观锁思想的实现。
注意: select … fro update语句在执行过程中所有扫描的行都会被锁上,因此在mysql中用悲观锁必须确定使用了索引,而不是全表扫描,否则将会把整个表锁住。
2. 乐观锁
乐观锁认为对同一数据的并发操作不会总发生,不用每次都上锁。在更新的时候判断有没有人去更新这个数据即可。也就是不采用数据库自身的锁机制,而是通过程序来实现。在java中juc.atomic包下的原子变量类就是使用了乐观锁的一种实现方式:cas实现的。
- 乐观锁的版本号机制
- 乐观锁的时间戳机制
五、从加锁方式划分:显示锁、隐式锁
1. 显示锁
即一个事务对新插入的记录可以不显示的加锁。但是由于事务id的存在,相当于加了一个隐式锁。别的事务在对这条记录加锁时,会帮当前事务生成一个锁结构,从而减少锁的数量。
六、其他锁
1 全局锁
全局锁就是对整个数据库加锁。当你需要让整个库处于只读状态的时候,可以使用这个命令。典型的使用场景是:做全库逻辑备份。
Flush tables with read lock
2. 死锁
死锁就是两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。处理方式有两种:
- 等待,直到超时(innodb_lock_wait_timeout=50s)
- 使用死锁检测进行死锁处理。
3. 如何避免死锁
- 合理设计索引,使业务sql尽可能通过索引定位更少的行,减少锁竞争。
- 调整业务sql执行顺序,避免长时间持有锁的事务在前面。
- 避免大事务,尽量将大事务拆成多个小事务处理。
七、锁的结构
一个锁的本质就是在内存中创建一个锁结构与之相关,符合下边条件的记录会被放到一个锁结构中。
- 在同一个事务中进行加锁操作
- 被加锁的记录在同一页面中
- 加锁的类型是一样的
- 等待状态是一样的
innodb存储引擎中的锁结构如下:
八、锁监控
show status like 'innodb_row_lock%'
mysql中把事务和锁的信息记录在了information_schema
库中,涉及到的三张表为:innodb_trx、innodb_locks和innodb_lock_waits。