mysql锁(附案例讲解)

mysql 锁概述

  • 表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。
  • 行级锁:开销大,加锁慢;会出现死锁,锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 页面锁:介于表级锁和行级锁之间(一般开发中不用)

MyISAM 表锁

只支持表锁

查询表级锁争用情况

show status like 'table%'

在这里插入图片描述
如果Table_locks_waited的值大于0,表示当前datebase 存在表锁的情况,线上值越大,需要注意了。

如何加锁表

在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE,DELETE,INSERT)前,会自动添加写锁

但是方便为了本地的测试,可以使用一下sql 模拟

-- 给一张或多张表表添加读或者写锁
lock table 表名称 readwrite),表明2...
-- 去除锁
unlock tables

读锁:

session1 会话

-- 给表添加读锁
lock table t_emp read;
-- 查询 不阻塞
select * from t_emp;
-- 查询 报错
select * from t_dept;
-- 更新 报错
update t_emp set name = 'qweqwe' where id = 1;

-- 如果同时在 锁t_emp 时,又执行 lock table 语句,那么上面的表锁即会失效
-- 释放锁
unlock tables;

session2 会话

-- 查询 不阻塞
select * from t_emp;
-- 查询 不阻塞
select * from t_dept;
-- 更新 阻塞 ,只有当表释放的时候,才会释放更新结果
update t_emp set name = 'qweqwe' where id = 1;

写锁:

session1 会话

-- 添加写锁
lock table t_dept write;
-- 查询 报错
select * from t_emp where id = 1;
-- 查询 成功
select * from t_dept where id = 1;
-- 更新成功
update table t_dept set name = '123' where id = 1;

unlock tables;

session2 会话

-- 写 和 读 t_dept 都会阻塞
-- 其他表 ,查询成功

注意:同一个表在sql 语句中出现多次,就要通过sql语句中的别名锁锁定多少次,否则也会出错

LOCK table t_emp read;
-- 查询报错
select t.* from t_emp t; 

LOCK table t_emp read , t_emp t read;
-- 查询正确
select t.* from t_emp t; 

并发插入

MyISAM 储存引擎有一个系统变量 concurrent_insert , 专门用以控制并发插入的行为,其值分别可以为0、1和2.

  • 设置为0时,不允许并发插入
  • 设置为1,当MyISAM 表中没有空洞(即表中间没有删除的行),MyISAM 允许在一个进程读表的同时,另一个进程表尾插入记录。这也是mysql的默认设置。
  • 设置为2,无论有没有空洞,都允许在表尾并发插入。
-- 表中间没有删除的行
-- session 1 会话
-- local 加在 read 后面才可以执行并发操作
LOCK table t_order read local;

-- session 2 会话
-- insert 语句插入成功
INSERT INTO `test`.`t_order`(`id`, `name`, `age`) VALUES (null, '2', 3); 

如果表中间之前删除过,那么需要执行以下语句

OPTIMIZE TABLE t_order;

执行后,再按上述操作,就可以在session2 会话中执行insert语句。

MyISAM 的锁调度

MyISAM 的读写锁是互斥的,读写操作是串行的。
同时请求读锁和写锁的话,写进程会先获得锁;不仅如此,即使读请求先到锁等待队列,写锁也会插到读锁前面,因为mysql会认为写比读重要。因此MyISAM 不太适合做大量更新插入。
如果存在大量的写操作,而导致读阻塞,,可以调节MyISAM 的调度

-- 给予读请求以优先的权利
set LOW_PRIORITY_UPDATES = 1

或者使用折中的方法:

-- 值设为1的话,读锁就优先了,但是开发环境中不建议这么干,这里只做了解
set GLOBAL max_write_lock_count = 1;

还需要注意的是,一些开发中不建议写特别复杂耗时的长sql,看似很酷,其实很有可能饿死写进程

InnoDB 表锁

  • 支持事务锁
  • 采用行级锁

1,事务(Transaction)及其ACID属性
原子性: 事务是一个原子操作单元,对其数据的修改,要么全都执行,要么全都不执行
一致性:事务开始和完成时,数据都保持一致的状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性;事务结束时,所有的内部数据结构也都必须正确。
隔离性:数据库提供一定的隔离机制,保证事务在不受外部并发的操作影响的独立环境执行。这意味着事务处理过程中的中间状态是对外部不可见的。
持久性:事务完成之后,它对数据的修改是永久性。

2,并发事务处理带来的问题

更新丢失:两个或多个事务,对同一行的数据进行修改,由于每个事务都不知道其他事务的存在,后提交的把之前提交的数据给覆盖掉了。如果在事务提交前不允许其他人访问同一行,则会避免此问题。
脏读:一个事务对一条数据修改,在事务提交之前,另外一个事务读取同一条记录,如果不加以控制,第二个事务就读到脏数据了。
不可重复度:一个事务在读取某些数据后的某个时间,再去读取以前读取过的数据,发现这些数据已经被修改过了

幻读:一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象叫做幻读

3, 事务隔离级别
可重复读是InnoDB 默认事务隔离级别
在这里插入图片描述

-- 为了防止重复读的情况,将事务的隔离级别设置为REPEATABLE READ 可重复读
-- 设置事务的隔离级别为REPEATABLE READ时,可以避免重复读的情况
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

查看当前的mysql 的事务隔离级别

-- mysql 8.0+
select @@transaction_isolation
-- mysql 5.X
select @@tx_isolation

获取InnoDB行锁争用情况

InnoDB锁超时时间由变量innodb_lock_wait_timeout控制,默认是50s

show status like 'innodb_row_lock%'

在这里插入图片描述
如果Innodb_row_lock_waits 和 Innodb_row_lock_time_avg 比较高的话
(1),可以同过查询information_schema 数据库中的表查询了解锁等待情况

select * from innodb_locks 

(2),通过设置InnoDB Monitors 观察锁的冲突情况

show engine innodb status;

InnoDB 的行锁模式及加锁方法

共享锁(S):允许衣蛾事务去读一行,阻止其他事务获得相同数据集的排他锁
排它锁(X):允许获得排它锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排它锁
为了允许行锁和表锁共存,实现多颗粒度机制,InnoDB 还有两种内部使用的意向锁,这两种锁都是表锁。
意向共享锁(IS):事务打算给数据行加行共享锁,事务在给另一个数据行加共享锁前必须取得该表的IS锁。
意向排它锁(IX):事务打算给数据行加排它锁,事务在给一个数据行家加排它锁前必须取得该表的IX锁

在这里插入图片描述
如果一个事务请求的锁模式与当前锁兼容,InnoDB就将请求的锁授予该事务,反之就要等待锁释放。
意向锁是InnoDB自动加的,对于 UPDATE、DELETE 和 INSERT 会自动的给涉及数据集加排它锁。对于普通的SELECT ,InnoDB不会加任何锁。
事务可以通过以下语句显示给记录集添加共享锁或排它锁

-- 共享锁
select * from ... lock in share mode;
-- 排它锁
select * from ... for update;

使用共享锁时,确保在同一事务中没有update,delete ,否则很有可能造成死锁。
在这里插入图片描述
如果上述使用排它锁的话,就不会出现这个问题了。
在这里插入图片描述

InnoDB 对行锁实现方式

InnoDB 行锁是通过索引上的索引项加锁来实现的,如果没有索引,InnoDB 将通过隐藏的聚簇索引来对记录加锁,InnoDB的行锁分为三种情形:
Record lock: 对索引项加锁。
Gap lock:对索引项之间的‘间隙’,第一条记录前的间隙,或最后一条记录后的间隙 加锁
Next-Key:前两种的组合,对记录及前面的间隙加锁

InnoDB 如果不通过索引条件检索数据,那么将对所有的记录加锁,实际效果和锁表一致了。
(1),不通过索引条件查询时,InnoDB会锁定表中所有记录

create table apple (
`key` varchar(20) not null,
`value` VARCHAR(20) not null
) ENGINE = INNODB

insert apple(`key`,`value`) value ("1","1")
insert apple(`key`,`value`) value ("2","2")
insert apple(`key`,`value`) value ("3","3")
insert apple(`key`,`value`) value ("1","4")

在这里插入图片描述

(2),由于mysql的的行锁是针对针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
在这里插入图片描述
虽然查询了不同的数据行,但是因为使用了相同的索引,索引也会阻塞。

(3),当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,不论是使用主键索引,唯一索引还是普通索引,InnoDB都会使用行锁来对数据加锁。
在这里插入图片描述

(4),即便在条件中使用了索引字段,但是是否使用索引来检索数据是由mysql通过判断不同执行计划的代价来决定的。可以通过explain 来分析。

Next-key 锁

在我们使用范围条件检索数据,并请求共享锁或排它锁,InnoDB会给符合条件的已有数据记录的索引项加锁,对于键值在条件范围内但不存在的记录,叫做 ‘间隙(GAP)’ ,InnoDB也会对这个间隙加锁,这种锁机制就是所谓的Next-key锁。

使用范围检索,并锁定记录时,InnoDB 这种加锁机制会阻塞符合条件范围内键值的并发插入,这往往会造成严重的锁等待。
除此之外,InnoDB使用相等的条件,请求给一个不存在的记录加锁,InnoDB也会使用Next-Key锁
在这里插入图片描述

InnoDB 在不同隔离级别下的一致性读及锁的差异

在这里插入图片描述

从上图中看出来,使用范围锁的时候,产生锁冲突的可能性也就越高。
实际上,通过优化事务逻辑,大部分应用使用 Read Commit 隔离级别就足够了

什么时候使用表锁

1,事务需要更新大部分或全部数据,表有比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突。这种情况下可以考虑使用锁表来提高该事务的执行速度
2,事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁,减少数据库因事务回滚带来的开销。

关于死锁

MyISAM 表锁是deadlock free 的,这是因为MyISAM 总是一次获得所需的全部锁,要么全部满足,要么等待,因为不会出现死锁。但在InnoDB中,除了单个sql组成的事务外,锁是逐步获得的,这就决定了InnoDB中发生死锁的可能。
发生死锁后,InnoDB一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务,但在涉及外部锁即涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁,需要设置innodb_lock_wait_timeout 来解决。

(1),由于两个 Session 访问两张表的顺序不同,发生死锁的机会就非常高。但如果以相同的顺序来访问,死锁就可以避免。
(2),在程序以批量方式处理数据的时候,如果事先对数据排序,保证每个每个线程按固定的顺序来处理记录,也可以大大降低出现死锁的可能。
(3),在事务中,如果要更新记录,应该直接申请足够级别的锁,即排它锁,因为当用户申请排它锁时,掐事务可能又已经获得相同记录的共享锁,从而造成锁冲突。
(4),在REPEATABLE READ 隔离级别下,如果两个线程同时对相同的条件下记录使用 … for update 加排它锁。两个线程都会加锁成功,如果没有符合该条件的记录,两个线程都会这么做,就会出现死锁。这种情况将隔离级别改成READ COMMITTED

如果出现死锁,可以使用

-- 查看最后一个死锁的原因
show engine innodb status

返回结果中包括死锁相关事务的详细信息,如引引发死锁的sql语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等

发布了46 篇原创文章 · 获赞 6 · 访问量 2659

猜你喜欢

转载自blog.csdn.net/renguiriyue/article/details/103338569