mysql中的锁
1.全局锁
对整个数据库实例进行加锁
全局读锁: Flush tables with read lock
加锁之后,其他线程的增删改、ddl等语句将被阻塞
使用场景:全局逻辑备份
2.表级锁
表锁
lock tables … read/write
在某个线程A中执行 lock tables t1 read, t2 write
; 这个语句,则其他线程写t1、读写t2的语句都会被阻塞。同时,线程A在执行 unlock tables
之前,也只能执行读t1、读写t2的操作。
元数据锁 MDL(metadata lock)
不需要显式使用,当对一个表做增删改查时,加MDL读锁,当要做表结构变更时,加MDL写锁。
-
读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
-
读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。
3.行锁
由引擎层各个引擎自己实现,本文以InnoDB为例。
在 Mysql 中,行级锁并不是直接锁记录,而是锁索引。索引分为主键索引和非主键索引两种,如果一条sql 语句操作了主键索引,Mysql 就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL会先锁定该非主键索引,再锁定相关的主键索引。
InnoDB 行锁是通过给索引项加锁实现的,如果没有索引,InnoDB 会通过隐藏的聚簇索引来对记录加锁。也就是说:如果不通过索引条件检索数据,那么InnoDB将对表中所有数据加锁,实际效果跟表锁一样。因为没有了索引,找到某一条记录就得扫描全表,要扫描全表,就得锁定表。
两阶段锁协议
在InnoDB事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。
如果事务中需要锁多个行,就需要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放,以此减少锁等待,提升并发度。
死锁和死锁检测
死锁:多个线程在互相等待对方的资源释放,进入死锁状态,有两种策略:
-
一种策略是,直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
-
另一种策略是,发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为on,表示开启这个逻辑。(常用)
共享锁和排他锁
共享锁又称为读锁,简称S锁,对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源(也可以再继续加共享锁,即 共享锁可多个共存),但无法修改。要想修改就必须等所有共享锁都释放完之后。
select * from table lock in share mode
排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select ...for update语句
select * from table for update
例:
表结构和数据:
create table t_user ( user_name varchar(10) not null comment '名称', user_no varchar(10) not null comment '用户编号' primary key, address varchar(100) not null comment '住址', password varchar(10) not null comment '密码', age int not null comment '年龄', update_time timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '更新时间', create_time datetime default CURRENT_TIMESTAMP null comment '创建时间' ) comment '用户表' charset=utf8mb4; create index idx_age on t_user (age); INSERT INTO t_user (user_name, user_no, address, password, age, update_time, create_time) VALUES ('小白', '1', '陕西', '123', 20, '2021-07-15 17:17:26', '2021-07-14 18:09:16'); INSERT INTO t_user (user_name, user_no, address, password, age, update_time, create_time) VALUES ('小青', '2', '陕西', '123', 18, '2021-07-15 17:17:26', '2021-07-14 18:09:43'); INSERT INTO t_user (user_name, user_no, address, password, age, update_time, create_time) VALUES ('小蓝', '3', '陕西', '123', 15, '2021-07-15 17:17:26', '2021-07-14 18:09:43'); INSERT INTO t_user (user_name, user_no, address, password, age, update_time, create_time) VALUES ('小黄', '4', '陕西', '123', 20, '2021-07-15 17:17:26', '2021-07-14 18:09:43');
例:主键索引加锁机制:
T1:
start transaction ; select * from t_user where user_no = '1' for update;
T2:
select * from t_user where user_no = '1' lock in share mode ;
结果:T2被阻塞,T1 commit之后,T2才会执行。因为user_no为唯一索引,T1可以确定需要加锁的主键索引,因此只会锁 user_no = '1'
的一行数据,此时T2要对这行数据加共享锁,获取锁失败,直到T1释放这行数据的排他锁才可以获取并执行。
例:不走索引加锁机制:
T1:
start transaction ; select * from t_user where user_name = '小白' for update ;
T2:
start transaction ; select * from t_user where user_name = '小蓝' lock in share mode ;
结果:T2被阻塞,T1 commit之后,T2才会执行。因为user_name没有索引,InnoDB只能对表中的所有数据加锁,实际相当于表锁,此时T2要对任何一条数据加共享锁,都会获取失败,直到T1释放锁才会成功并执行。
注:
“加锁的查询操作”:加过排他锁的数据行在其他事务中是不能修改的,也不能通过for update或lock in share mode的加锁方式查询,但可以直接通过select ...from...查询数据,因为普通查询没有任何锁机制。
间隙锁
-
在RR隔离级别下才会有
-
检索条件必须有索引(没有索引的话,mysql会全表扫描,那样会锁定整张表所有的记录,包括不存在的记录,此时其他事务不能修改不能删除不能添加)
T1:
start transaction ; select * from t_user where age = 18 for update ;
T2:
start transaction ; update t_user set age = 18 where age = 20;
结果:T2被阻塞,T1 commit之后,T2才会执行。T1加间隙锁范围为age [15,18] 和 [18,20),T2要修改的是age = 20,修改后的age为18,在T1的加锁范围内,因此需要等待T1释放锁。如果不存在age>18的行,锁的范围为age [15,无限大)
4.悲观锁和乐观锁(抽象出来的锁,不真实存在)
悲观锁:通常使用排他锁来实现
乐观锁: 一般是在该商品表添加version版本字段或者timestamp时间戳字段
总结:对于以上,可以看得出来乐观锁和悲观锁的区别:
悲观锁实际使用了排他锁来实现。