事务并发访问同一数据资源的情况主要就分为读-读
、写-写
和读-写
三种。
读-读
即并发事务同时访问同一行数据记录。由于两个事务都进行只读操作,不会对记录造成任何影响,因此并发读完全允许。写-写
即并发事务同时修改同一行数据记录。这种情况下可能导致脏写
问题,这是任何情况下都不允许发生的,因此只能通过加锁
实现,也就是当一个事务需要对某行记录进行修改时,首先会先给这条记录加锁,如果加锁成功则继续执行,否则就排队等待,事务执行完成或回滚会自动释放锁。读-写
即一个事务进行读取操作,另一个进行写入操作。这种情况下可能会产生脏读
、不可重复读
、幻读
。最好的方案是读操作利用多版本并发控制(MVCC
),写操作进行加锁。
锁的粒度
按锁作用的数据范围进行分类的话,锁可以分为行级锁
和表级锁
。
行级锁
:作用在数据行上,锁的粒度比较小。表级锁
:作用在整张数据表上,锁的粒度比较大。
锁的分类
为了实现读-读
之间不受影响,并且写-写
、读-写
之间能够相互阻塞,Mysql
使用了读写锁
的思路进行实现,具体来说就是分为了共享锁
和排它锁
:
共享锁(Shared Locks)
:简称S锁
,在事务要读取一条记录时,需要先获取该记录的S锁
。S锁
可以在同一时刻被多个事务同时持有。我们可以用select ...... lock in share mode;
的方式手工加上一把S锁
。排他锁(Exclusive Locks)
:简称X锁
,在事务要改动一条记录时,需要先获取该记录的X锁
。X锁
在同一时刻最多只能被一个事务持有。X锁
的加锁方式有两种,第一种是自动加锁,在对数据进行增删改的时候,都会默认加上一个X锁
。还有一种是手工加锁,我们用一个FOR UPDATE
给一行数据加上一个X锁
。
还需要注意的一点是,如果一个事务已经持有了某行记录的S锁
,另一个事务是无法为这行记录加上X锁
的,反之亦然。
除了共享锁(Shared Locks)
和排他锁(Exclusive Locks)
,Mysql
还有意向锁(Intention Locks)
。
意向锁是由数据库自己维护的,一般来说,
- 当我们给一行数据加上共享锁之前,数据库会自动在这张表上面加一个
意向共享锁(IS锁)
; - 当我们给一行数据加上排他锁之前,数据库会自动在这张表上面加一个
意向排他锁(IX锁)
。
意向锁
可以认为是S锁
和X锁
在数据表上的标识,通过意向锁可以快速判断表中是否有记录被上锁,从而避免通过遍历的方式来查看表中有没有记录被上锁,提升加锁效率。例如,我们要加表级别的X锁
,这时候数据表里面如果存在行级别的X锁
或者S锁
的,加锁就会失败,此时直接根据意向锁
就能知道这张表是否有行级别的X锁
或者S锁
。
InnoDB中的行级锁
我们知道,通过MVCC
可以解决脏读
、不可重复读
、幻读
这些读一致性问题,但实际上这只是解决了普通select
语句的数据读取问题。
事务利用MVCC
进行的读取操作称之为快照读
,所有普通的SELECT
语句在READ COMMITTED
、REPEATABLE READ
隔离级别下都算是快照读
。
除了快照读
之外,还有一种是锁定读
,即在读取的时候给记录加锁,在锁定读
的情况下依然要解决脏读
、不可重复读
、幻读
的问题。由于都是在记录上加锁,这些锁都属于行级锁
。
InnoDB
的行锁,是通过锁住索引来实现的,如果加锁查询的时候没有使用过索引,会将整个聚簇索引都锁住,相当于锁表了。
根据锁定范围的不同,行锁可以使用记录锁(Record Locks)
、间隙锁(Gap Locks)
和临键锁(Next-Key Locks)
的方式实现。
-
记录锁(Record Locks) :所谓记录,就是指聚簇索引中真实存放的数据。记录锁就是直接锁定某行记录。当使用唯一性的索引(包括唯一索引和聚簇索引)进行等值查询且精准匹配到一条记录时,此时就会直接将这条记录锁定。
-
间隙锁(Gap Locks) 间隙指的是两个记录之间逻辑上尚未填入数据的部分。同理,间隙锁就是锁定某些间隙区间的。当我们使用用等值查询或者范围查询,并且没有命中任何一个
record
,此时就会将对应的间隙区间锁定。 -
临键锁(Next-Key Locks) 临键指的是间隙加上它右边的记录组成的左开右闭区间。是记录锁(Record Locks)和间隙锁(Gap Locks)的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。当我们使用范围查询,并且命中了部分
record
记录,此时锁住的就是临键区间。注意,临键锁锁住的区间会包含最后一个record的右边的临键区间。当使用唯一性索引,等值查询匹配到一条记录的时候,临键锁(Next-Key Locks)会退化成记录锁;没有匹配到任何记录的时候,退化成间隙锁。
间隙锁(Gap Locks)
和临键锁(Next-Key Locks)
都是用来解决幻读问题的
常见面试点:
范围
- 表级锁:开销小、加锁快,锁粒度小发生锁冲突的概率大、并发度低,不会出现死锁;
- 行级锁:开销大,加锁慢,锁粒度大发生锁冲突的概率小,并发度高,会出现死锁;
类型
- 共享锁(S):行锁,读取一行
- 排它锁(X):行锁,更新一行
- 意向共享锁(IS):表级,准备加共享锁
- 意向拍他锁(IX):表级,准备加排他锁
- 间隙锁(NX):行级,使用范围条件时
注意:对范围内不存在的记录加锁,一是为了防止幻读,二是为了满足恢复和复制的需要;
加锁
- 增加行级锁之前,InnoDB会自动给表加意向锁
- 执行DML(数据操作语句)语句时,InnoDB会自动给数据加排他锁
- 执行DQL(数据查询语句)语句时:
- 共享锁:SEIECT。。。FROM。。。WHERE。。。LOCK IN SHARE MODE;
- 排他锁:SEIECT。。。FROM。。。WHERE。。。FOR UPDATE;
- 间隙锁:上述SQL采用范围条件时,InnoDB对不存在的记录自动增加间隙锁;
死锁
- 解决方案(已经产生了死锁)
- 一般InnoDB会自动检测到死锁,然后使一个事务回滚释放占有的资源,另一个事务继续执行;
- 设置超时等待参数:innodb_lock_wait_timeout;
- 避免死锁
- 不同的业务并发访问多个表时,应该约定以相同的顺序来访问这些表;
- 以批量的方式处理数据时,应事先对数据排序,保证线程按固定的顺序来处理数据;
- 在事务中,如果要更新记录,应直接申请足够级别的锁,即排他锁;
MySQL乐观锁(CAS算法/自旋锁)
-
CAS算法(Compare and swap):是一种无锁的算法,该算法设计三个操作数:内存值V,旧值A,新值B
-
当V等于A时,采用原子方式用B得值更新V的值;
-
否则,
该算法通常采用自旋操作,也就叫做了自旋锁;它的缺点是:
- ABA问题:某线程将A改为B,再改回A,则CAS会误认为A没有修改过;
- 自旋操作采用循环的方式实现,若加锁时间太长,则会给CPU带来巨大的开销;
- CAS只能保证一个共享变量的院子操作;
-