MySQL中的锁总体可以分为悲观锁和乐观锁。
悲观锁MySQL中有自带的锁。乐观锁需要自己写程序控制来实现乐观锁的功能。
悲观锁
表级锁
表锁
表锁属于mysql服务层,手动添加。
- 读锁:
lock table mylock read;
加读锁后,可以继续加读锁,但不能加写锁。
当一个线程拥有了读锁之后,那么当前的线程只能访问加锁了的表。
其他线程既可以访问加锁的表,也可以访问其他的表。
但是,其他线程如果想要修改加了读锁的表中的内容,必须要等当前线程释放表锁。
UNLOCK tables;
- 写锁:
lock table mylock write;
加了写锁之后,其他进程既不能修改也不能查询加锁的表。
元数据锁
元数据锁属于MySQL服务层,自动添加。
元数据锁是自动添加,顾名思义,是用来保护表的元数据的。
在进行增删改查的操作时,加元数据读锁。在进行DDL操作时,即对表的结构进行修改的时候加元数据写锁。
读锁是为了,当一个线程正在对表进行增删改查,不允许其他进程修改表的结构。
写锁则是为了当一个线程正在修改表的结构时,其他线程既不能访问表的数据也不能修改表的结构。
元数据锁在事务提交之后才会释放。
begin; //Navicat中开启事务
select * from mylock;//Navicat中查询表中数据
mysql> alter table mylock add f int;//命令行中修改表结构,发生阻塞。
commit;//Navicat中提交事务
Query OK, 0 rows affected (2 min 37.60 sec)//命令行中提示修改完成
Records: 0 Duplicates: 0 Warnings: 0
意向锁
属于Innodb存储引擎层。内部使用。
意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。
意向锁的主要作用是为了【全表更新数据】时的性能提升。否则在全表更新数据时,需要先检索该表是否某些记录上面有行锁。
行级锁(InnoDB实现)
行级锁是由Innodb存储引擎实现的。
行级锁按锁定范围来说分为三种:
记录锁
锁定索引中的一条记录。
间隙锁
锁定记录前,记录中,记录后的行。
RR隔离级(可重复读)--MySQL默认隔离级别
Next-Key锁
记录锁+间隙锁
按功能来说分为两种:
共享读锁
允许一个事务去读一行,阻止其他事务活动相同数据集的排他锁。
也就是说允许当前事务或其他事务读,不允许其他事务写。
SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE -- 共享读锁 手动添加
select * from table -- 无锁
排他写锁
允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。
也就是允许当前事务写,不允许其他事务读或者写。
排他写锁对于UPDATE DELETE INSERT 语句,InnoDB会自动给涉及数据集加排他锁。
手动加:
SELECT * FROM table_name WHERE ... FOR UPDATE
两阶段锁
锁操作分为两个阶段:加锁阶段与解锁阶段
加锁阶段与解锁阶段不相交。加锁阶段只加锁不放锁。放锁阶段只放锁不加锁。
加锁和解锁不能在一个sql语句中实现。
行锁演示
InnoDB行锁是通过给索引上的索引项加锁来实现的,因此只有通过索引条件检索的数据,InnoDB才会使用行级锁,否则InnoDB将使用表锁。
show status like 'innodb_row_lock%';//查看行锁状态
- Innodb_row_lock_current_waits:当前正在等待锁定的数量;
- Innodb_row_lock_time:从系统启动到现在锁定总时间长度;
- Innodb_row_lock_time_avg:每次等待所花平均时间;
- Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
- Innodb_row_lock_waits:系统启动后到现在总共等待的次数;
begin;
select * from mylock where id=1 lock in SHARE MODE;//在Navicat中开启事务,但未提交
//此时在id=1的行已经被加了行共享读锁。
命令行中可以访问id为1的行
命令行中可以修改其他行的数据
命令行中修改id=1的行等待锁释放时间超时
如果使用了没有索引的列,行锁会升级为表锁
上面是读锁,下面分析写锁。
BEGIN;
SELECT * from mylock where id=1 for update;//同样在Navicat中加上写锁。
//对于 UPDATE DELETE INSERT 语句来说,InnoDB会自动加行写锁。
//而SELECT语句则需要手动加
直接访问是可以访问的,但是加读锁就会堵塞
select 没有锁的时候即使要查询的行已经有写锁了还是能访问。但是加上了行读锁就发生阻塞了。
间隙锁
间隙锁的产生:
主键索引的范围 ,辅助索引的等值和范围。
表结构
begin;
update news set number=3 where number=4;//Navicat中操作
此时的id间隙范围为[1,6),number范围是[2,5),类型是辅助索引的等值。
start transaction ;//命令行中操作
insert into news value(2,3);#(均在间隙内,阻塞)
insert into news value(7,8);#(均在间隙外,成功)
insert into news value(2,8);#(id在间隙内,number在间隙外,成功)
insert into news value(4,8);#(id在间隙内,number在间隙外,成功)
insert into news value(7,3);#(id在间隙外,number在间隙内,阻塞)
insert into news value(7,2);# (id在间隙外,number为上边缘数据,阻塞)
insert into news value(2,2);#(id在间隙内,number为上边缘数据,阻塞)
insert into news value(7,5);#(id在间隙外,number为下边缘数据,成功)
insert into news value(4,5);#(id在间隙内,number为下边缘数据,阻塞)
只要number在间隙内,就一定会阻塞。如果number在下边缘数据并且id在间隙内的话也会阻塞。
start transaction ;
update news set number=3 where number=13 ;
此时number间隙锁范围(11,无穷)。
此时只有当id也满足间隙锁范围,number=11时才会发生阻塞。
两阶段锁
两阶段锁☞锁的产生和释放是两个阶段,锁在语句执行完毕之后不会立即释放,而是在事务提交之后才释放。
死锁
由于两阶段锁的特性,会产生死锁的问题。
由于线程1先获得了行id=1的锁,线程2获得了 id=2的锁,并且都没有提交,此时锁都没有释放。
当线程1再去修改行id=2的信息时,就会发生阻塞,同时线程2想修改行id=1的信息,同样被阻塞,两个线程都在等对方释放锁,形成了死锁。
死锁检测
发生死锁后,有两种解决策略。
一种是直接进入等待直至超时。超时的事务回滚。超时的时间限制可以通过innodb_lock_wait_timeout设置。
另一种就是死锁检测。也就是主动检测是否时锁,一旦发现死锁就回滚导致死锁的某一条事务。
死锁的检测。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。
采用第一种策略没有额外的性能消耗,但是等待的时间过长影响使用体验,太短又容易误杀其他没有死锁只是在等待锁释放的事务。
第二种策略可以根据是否死锁回滚事务,发现死锁就回滚,但是寻找死锁的过程需要额外的性能消耗,每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。
例如:新来的线程F,被锁了后就要检查锁住F的线程(假设为D)是否被锁,如果没有被锁,则没有死锁,如果被锁了,还要查看锁住线程D的是谁,如果是F,那么肯定死锁了,如果不是F(假设为B),那么就要继续判断锁住线程B的是谁,一直走知道发现线程没有被锁(无死锁)或者被F锁住(死锁)才会终止。
网友评论