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 updatelock 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时间戳字段

总结:对于以上,可以看得出来乐观锁和悲观锁的区别:

悲观锁实际使用了排他锁来实现。

乐观锁更新有可能会失败,甚至是更新几次都失败,这是有风险的。所以如果写入居多,对吞吐要求不高,可使用悲观锁。

 

原文地址:https://www.cnblogs.com/jiezai/p/15067229.html