概念
锁是为了保证数据库中的数据一致性,使各种【共享资源】在被访问时变得【有序】而设计的一种规则。
MySQL中不同的存储引擎支持不同的锁机制。
Innodb支持【行锁】,有时也会升级为表锁,MyIsam只支持表锁。
- 表锁:开销小、加锁快,不会出现死锁。锁的粒度大,发生锁冲突的概率小,并发度相对较低
- 行锁:开销大、加锁慢,会出现死锁。锁粒度小,发生锁冲突的概率高,并发度高。
Innodb的锁类型
1、共享锁
共享锁,也称为读锁。一个事务获取了一行数据的读锁,其他事务也能获取改行对应的读锁(共享),但不能获得写锁。即,一个事务在读取一行数据时,其他事务也可以读,但不能对该行数据进行修改。
加锁语句:
[select 查询语句] lock in share mode;
模拟:
# 事务A
start transaction ; -- 1
select * from tb_student where id = 1 lock in share mode ; -- 3
commit ; -- 7
# 事务B
start transaction ; -- 2
select * from tb_student where id = 1 lock in share mode ; -- 4
commit ; -- 8
# 事务C
start transaction ; -- 5
update tb_student set age = 98 where id = 1; -- 6
commit ; -- 9
可以开三个客户端去模拟一下。结果可以发现,多个事务可以并发的获取同一把读锁。一旦某一行数据被上了写锁,那么其他的事务只能读取该行数据,但是不能对该行数据进行写操作。
2、排它锁
排它锁,也称作写锁,或者独占锁。一个事务获取了一行数据的写锁,既可以读取该行数据(不然怎么写呢),也可以修改该行数据,其他事务无法获取该数据行的其他任何锁,直到事务提交或者回滚。
(保证了当前事务对数据修改的原子性)
加锁语句:
【update、insert、update】语句,默认都会自带一把写锁的。
[select 查询语句] for update;
其实我们发现,尽管加了写锁,其他事务依然能够读取到对应行的数据的,这是应为 MVCC 模型,后面会说。
3、记录锁
记录锁就是我们常说的行锁,只有Innodb才支持。但是其中也有一些细节需要注意:
(1)两个事务并发的修改【同一行】数据,where 检索列【不加索引】
# 事务A
begin ; -- 1
update tb_student set name = 'kaiven' where age = 20; -- 3
commit ; -- 5
# 事务B
begin ; -- 2
update tb_student set name = 'lucy' where age = 20; -- 4
commit ; -- 6
我们会发现,事务A先执行了update语句,拿到了这一行的【写锁】,事务B只能等待锁的释放。
(2)两个事务并发的修改【同表不同行】的数据,where 检索列【不加索引】
# 事务A
begin ; -- 1
update tb_student set name = 'kaiven' where age = 20; -- 3
commit ; -- 5
# 事务B
begin ; -- 2
update tb_student set name = 'lucy' where age = 18; -- 4
commit ; -- 6
很明显,整张表都被锁住了。
(3)两个事务并发的修改【同表不同行】的数据,where 检索列【有索引】
create index idx_age on tb_student(age);
# 事务A
begin ; -- 1
update tb_student set name = 'kaiven' where age = 20; -- 3
commit ; -- 5
# 事务B
begin ; -- 2
update tb_student set name = 'lucy' where age = 18; -- 4
commit ; -- 6
现在我们发现,做到了真正意义上的【行级锁】。
所以,行锁是加在索引上的。
4、间隙锁
间隙锁帮我们解决了MySQL在RR隔离级别下的一部分幻读问题。间隙锁锁定的是记录范围,不包含记录本身,也就是不允许在某个范围内插入数据。
间隙锁生成的条件:
- A事务使用where进行范围检索时未提交事务,B事务向A事务检索条件的范围内插入数据
- where条件必须有索引
模拟:
# 事务A
begin ; -- 1
select * from tb_student where id between 1 and 5 lock in share mode ; -- 3
commit ; -- 5
# 事务B
begin ; -- 2
insert into tb_student values (2,'kaiven',20); -- 4
commit ; -- 6
我们会发现,事务B在进行插入操作时,会阻塞直至事务A提交。
5、记录锁和间隙锁的组合(next-key lock)
临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含【索引记录】,又包含【索引区间】。
注:临键锁的主要目的,也是为了避免幻读。如果把事务的隔离级别降为RC,临键锁则也会失效。
6、MDL锁
MySQL5.5之后引入了【meta data lock】,简称MDL锁,用于保证表中元数据
的信息。在一个事务进行时,另一个事务不能执行任何DDL语句来修改表结构,这也是一种保证数据一致性的手段。
7、死锁问题
什么样的场景会导致死锁的发生?
对应MySQL来说,所谓的死锁就是两个事务A、B,操作相同的共享资源C、D,事务A、B操作资源C、D的时候都要分别为其加锁,然后操作资源的顺序不同,也就是加锁的顺序不同,就会导致死锁。
比如说,事务A操作顺序是C、D,先给C上锁,再给D上锁;而事务B操作顺序是D、C,先给D上锁,在给C上锁。如果事务A、B并发执行的情况下,很有可能造成两个事务互相等待对方释放锁,再进行下一步操作。
Innodb使用行级锁,在某些情况下会产生死锁的问题,索引Innodb存储引擎采用了一种叫做等待图的方法来自动检测死锁,如果发现死锁,就会自动回滚一个事务。
如果避免死锁呢?
这个问题其实没有一个明确的答案,要根据实际的业务场景去做测试:
- 查询尽量走索引,避免无效的查询导致行锁升级为表锁
- 合理设计索引,尽量缩小锁的范围
- 缩小查询范围,尽量避免间隙锁或者缩小间隙锁的范围
- 尽量避免长事务,减少同一个事务中资源锁定的数量,缩短资源锁定的时间
表锁
对于Innodb表,绝大部分情况下都应该使用【行级锁】,因为事务和行锁往往是我们选在Innodb存储引擎的理由。但是一些特殊的场景下,也可以考虑使用表级锁:
- 第一种情况是:事务需要更新【大部分或全部数据】,表又比较大,如果使用默认的行级锁,不仅这个事务执行的效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。
- 第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。
由于【表锁】平时很少应用,要么就是大半夜去对于表做大改动,否则不会轻易加表锁的。
注意:
必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层MySQL Server负责的,autocommit=0、innodb_table_lock=1(默认设置)时,InnoDB层才能感知MySQL加的表锁,MySQL Server才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁;否则,InnoDB将无法自动检测并处理这种死锁。
(表锁的粒度很大,请谨慎使用)
乐观锁和悲观锁
这里贴一篇文章:https://blog.csdn.net/jam_yin/article/details/143086530
(本质上就是对于数据安全性的看法不同,乐观的看法认为,高并发修改数据的概率并不是很高;悲观的看法认为,高并发修改数据大概率会发生)
2024.11.20
writeBy kaiven