mysql - 第10课 - 深入理解mysql事务隔离级别与锁机制 (mysql各种锁,mysql事务隔离级别实现原理)

官网文档:MySQL :: MySQL 5.7 Reference Manual

导图(用于总结和复习)

目录

一. 事务与事务的四大特性(ACID)

二. 并发事务带来的问题

三. 四种事务隔离级别

四. 数据库的各种锁

锁分类

怎么加锁

锁详解

表锁

行锁

间隙锁(Gap Lock):

临键锁(Next-key Locks)

行锁分析

总结:

五. 死锁

什么是死锁

为什么会形成死锁

mysql如何处理死锁

如何避免死锁


一. 事务与事务的四大特性(ACID)

事务定义:事务是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合。

ACID:

  • 原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
  • 一致性(Consistent) :在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性。
  • 隔离性(Isolation) :数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
  • 持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

二. 并发事务带来的问题

多个事务并发执行的时候会出现以下问题:

1.脏读:事务1读取到事务2未提交的数据,事务2回滚了数据,则事务1读取到了无效数据。

image.png

2.不可重复读: 事务1读取了一条数据,然后事务2修改了这条数据并且提交了,然后事务1再读这条数据,数据前后不一致。跟脏读有点像,但是这里提交了数据

不可重复读重点在于 update 和 delete。

image.png

3.幻读:事务1使用范围查询数据,事务2往表里添加了数据,事务1再次查询,前后查询结果不一致。跟不可重复读有点像,但是这里是插入数据

幻读重点在于 insert。

image.png

“脏读”、“不可重复读”和“幻读”,都是数据库读一致性问题,由数据库提供一定的事务隔离机制来解决。

三. 四种事务隔离级别

不同的隔离级别是在数据可靠性并发性之间的均衡取舍,隔离级别越高,对应的并发性能越差,数据越安全可靠

隔离级别

脏读(Dirty Read)

不可重复读(NonRepeatable Read)

幻读(Phantom Read)

未提交(Read uncommitted)

可能

可能可能

已提交(Read committed)

不可能

可能可能

可重复读(Repeatable read)

不可能不可能可能

串行化(Serializable)

不可能不可能不可能
  1. 读未提交(Read uncommitted):可以读取到其他事务未提交的数据。
    某个数据在被写的时候一定会加上写锁,写锁会阻止其他事务给这个数据加读锁,但是对不加锁的读就会阻止无效了。
    READ UNCOMMITTED 隔离级别下,读不会加任何锁,所以写锁的阻止作用无效,直到事务结束之后释放。
  2. 读已提交(Read committed):可以读取到其他事务已提交的数据。
    1).该隔离级别可以解决读未提交的问题。
    2).在该隔离级别下读取数据时不加任何锁,而是使用MVCC(多版本并发控制机制,下节课介绍)获取当前数据的最新快照。
    3).该隔离级别会有不可重复读和幻读问题。问题在于MVCC版本的生成时机是在每次select时。如果我们在事务A中执行多次select,在每次select之间有其他事务更新并提交了我们读取的数据,那就出现了不可重复度
  3. 可重复读(Repeatable read):
    1).可重复度与读已提交不同的是MVCC版本的生成时机。即:一次事务中只在第一次select时生成版本,后续的查询都是在这个版本上进行,从而实现了可重复读。2).使用间隙锁(Gap Lock)。间隙锁可以解决大部分场景的幻读问题。但是有种幻读场景不能解决:B事务插入一条数据后,A事务可以更新这条数据,然后A事务就可以查看这条数据。
  4. 串行化(Serializable):
    该级别下,会自动将所有普通select转化为读锁(select ... lock in share mode)执行,即针对同一数据的所有读写都变成互斥的了,可靠性大大提高,并发性大大降低。

    注意:在所有隔离级别下的写操作都会加写锁。
设置系统级隔离级别设置会话级隔离级别
set global tx_isolation='read-uncommitted';set tx_isolation='read-uncommitted';
set global tx_isolation='read-committed';set tx_isolation='read-committed';
set global tx_isolation='repeatable-read';set tx_isolation='repeatable-read';
set global tx_isolation='serializable';set tx_isolation='serializable';
查看系统级隔离级别查看会话级隔离级别
select @@global.tx_isolation;select @@tx_isolation;

注意:修改隔离级别后需要重新连接客户端才会生效!

Mysql默认的事务隔离级别是可重复读(oracle是读已提交),用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔离级别,如果Spring设置了就用已经设置的隔离级别 。

上面说到数据库使用mvcc和锁实现隔离性,接下来将详细介绍 。下篇详细介绍MVCC。

四. 数据库的各种锁

mysql锁官网文档:MySQL :: MySQL 5.7 Reference Manual :: 14.7.1 InnoDB Locking

锁分类

  • 从性能上分为乐观锁(用版本对比来实现)和悲观锁
  • 从对数据库操作的类型分,分为读锁写锁(都属于悲观锁) ,也叫共享锁排他锁

读锁(共享锁,S锁(Shared)):针对被锁住的数据,可以被所有事务加读锁,但是不能加写锁,直至读锁被释放。读操作在可重复度级别之前的都不加锁

写锁(排它锁,X锁(eXclusive)):针对被锁住的数据,只可以被当前事务加读锁、写锁,其他事务读锁、写锁都不能加,直至写锁被释放。所有的写操作都会加写锁

  • 从对数据操作的粒度分,分为表锁、页锁(BDB引擎)行锁(InnorDb引擎【按算法包括:记录锁(Record Locks)隙锁(Gap Lock)、临键(Next-Key Locks)

怎么加锁

image.png

  • 读锁(共享锁):select * from t_account where id=1 lock in share mode;
  • 写锁(排他锁):select * from t_account where id=1 for update;
  • 表锁:lock table 表名称 read(write),表名称2 read(write); 删除表锁:unlock tables;
  • 行锁:select * from t_account where id=1 for update; 或者 update t_account set balance=1 where id=1; 注意:id必须是唯一索引列或主键列,否则会变成临键锁。
  • 隙锁(Gap Lock):它会锁住两个索引之间的区域。比如select * from t_account where id>8 and id<18 for update,就会在id为 (3,20] 的索引区间上加Gap Lock。
  • 临键(Next-Key Locks):它是Record Lock + Gap Lock形成的一个闭区间锁。比如select * from t_account where id>=1 and id<=10 for update,就会在id为[1,10]的索引闭区间上加Next-Key Lock。

锁详解

接下来对这些锁进行详细解说和测试。

表锁

每次锁住整张表,粒度大,发生锁冲突概率高,并发低;开销小,加锁快;不会出现死锁。

  • 手动增加表锁sql

lock table 表名称 read(write),表名称2 read(write);

  • 查看表上加过的锁

show open tables;

  • 删除表锁

unlock tables;

例:(注意引擎是InnoDB)

DROP TABLE IF EXISTS `t_mylock`;
CREATE TABLE `t_mylock` (
  `id` int(10) unsigned NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `t_mylock` VALUES (1, 'a');
INSERT INTO `t_mylock` VALUES (2, 'b');
INSERT INTO `t_mylock` VALUES (3, 'c');
INSERT INTO `t_mylock` VALUES (4, 'd');

测试读锁:

image.png

给表加读锁后,当前session和其他session都可以读该表。

当前session插入或者更新锁定的表会报错,其他session插入或更新则会等待。

image.png

解锁后可以插入数据。

测试写锁:

image.png

上图得知:针对被锁住的数据,可以被当前的事务读和写。

图1:

image.png

图2:

image.png

图3:

image.png

图1、2、3得知:其他session 开启的事务读和写都会被阻塞,默认30秒后自动释放。

行锁

注意:仅InnoDB支持事务和行锁;MyIsam不支持事务,只支持表锁。

每次锁住一行数据,粒度小,发生冲突概率低;开销大,加锁慢;会出现死锁。mysiam不支持行锁,innorDB支持行锁。

行锁测试(注意引擎是InnoDB):

image.png

修改 id=1 的一行数据,会自动给这行数据自动加上行锁和写锁。

image.png

这时候用其他的事务修改 id=1 这行数据会被阻塞。但是可以读这条数据,因为InnoDB的读操作使用mvcc(多版本并发控制)机制,不需要加锁

image.png

这时候修改其他行的数据可以成功,说明只给 id =1 的一行数据加上了行锁。

间隙锁(Gap Lock):

测试环境:mysql5.7,InnoDB,默认的隔离级别(RR)

在隔离级别为可重复读(Repeatable read)生效。间隙锁会封锁索引的间隙,禁止往索引区间内插入数据,在某些情况下可以解决幻读问题。

使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。

产生间隙锁的条件(RR事务隔离级别下):

  1. 使用普通索引加锁;
  2. 使用多列唯一索引加索;
  3. 使用主键索引或唯一索引锁定多行记录或锁定不存在的记录。

测试数据:

DROP TABLE IF EXISTS `t_mylock`;
CREATE TABLE `t_mylock` (
  `id` int(10) unsigned NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `t_mylock` (`id`,`name`) VALUES (1,'a');
INSERT INTO `t_mylock` (`id`,`name`) VALUES (5,'e');
INSERT INTO `t_mylock` (`id`,`name`) VALUES (7,'g');
INSERT INTO `t_mylock` (`id`,`name`) VALUES (10,'j');

在session1下执行SELECT * FROM t_mylock WHERE id BETWEEN 5 and 7 FOR UPDATE; 则其他事务无法添加和修改 (1, 7] 之间的数据。

从id索引分析到 t_mylock 表隐藏间隙:

  1. (-∞, 1]
  2. (1, 5]
  3. (5, 7]
  4. (7, 10]
  5. (10, +∞]

1.只使用记录锁不会产生间隙锁。

session1:

begin
SELECT * FROM t_mylock where id=5 for update;
-- commit;

sessoin2:

INSERT INTO `t_mylock` (`id`,`name`) VALUES (4,'a'); -- 成功
INSERT INTO `t_mylock` (`id`,`name`) VALUES (6,'a'); -- 成功

session1的查询只会锁住一行记录锁,不会产生间隙锁。

还原测试数据继续测试。

2.使用 主键索引/唯一键索引 范围查询产生间隙锁

session1:

begin;
SELECT * FROM t_mylock where id between 5 and 7 for update;
-- commit;

session2:

insert into t_mylock(id,name) values (3,'a');  -- 成功
insert into t_mylock(id,name) values (4,'a');  -- 成功
insert into t_mylock(id,name) values (5,'a');  -- 阻塞
insert into t_mylock(id,name) values (8,'a');  -- 阻塞
insert into t_mylock(id,name) values (10,'a'); -- 阻塞
insert into t_mylock(id,name) values (11,'a'); -- 成功

测试结果:(5, 7]和(7, 10]这两个区间被加了区间锁不能插入数据,其他区间都可以。给[5, 7]区间加锁的时候,会锁住(5, 7]和(7, 10]这两个区间

还原测试数据继续测试。

3.测试锁住不存在的数据

session1:

begin;
SELECT * FROM t_mylock where id = 3 for update;
-- commit;

session2:

insert into t_mylock(id,name) values (2,'a');  -- 阻塞
insert into t_mylock(id,name) values (4,'a');  -- 阻塞
insert into t_mylock(id,name) values (6,'a');  -- 成功

测试结果:给一条不存在的数据加锁的时候会产生间隙锁

4.测试普通索引的间隙锁

测试数据脚本:

DROP TABLE IF EXISTS `t_mylock`;
CREATE TABLE `t_mylock` (
  `id` int(10) unsigned NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `t_mylock` (`id`,`name`) VALUES (3,'a');
INSERT INTO `t_mylock` (`id`,`name`) VALUES (5,'e');
INSERT INTO `t_mylock` (`id`,`name`) VALUES (8,'g');

session1:

begin;
select * FROM t_mylock where name='e' for update;
-- commit;

name索引隐藏间隙:

1.(-∞, a]

2.(a, e]

3.(e, g]

4.(g, +∞]

session2:

insert into t_mylock(id,name) values (2, 'a');  -- 成功
insert into t_mylock(id,name) values (4, 'a');  -- 阻塞
insert into t_mylock(id,name) values (5, 'b');  -- 阻塞
insert into t_mylock(id,name) values (5, 'e');  -- 阻塞
insert into t_mylock(id,name) values (6, 'g');  -- 阻塞
insert into t_mylock(id,name) values (7, 'g');  -- 阻塞
insert into t_mylock(id,name) values (9, 'g');  -- 成功
insert into t_mylock(id,name) values (10, 'h');  -- 成功 

​测试结果:区间 (a, e] 、 (e, g] 被锁住,a到g的数据不能插入。但是有个奇怪的现象, (2, 'a') 和 (9, 'g')却执行成功了,这是怎么回事呢。

下面用一张索引的叶子节点图示意:

未命名文件.jpg

InnorDb的行锁是针对索引加的,当name相等的时候,id会按照大小进行排序。锁住e的时候会根据数据形成(a,3)到(g,8)的区间。

结论:

1.在普通索引上只要加锁都会产生间隙锁,而主键索引或唯一索引只有锁住多行才会加索。

2.普通索引上间隙锁,是先根据普通索引排序,然后再根据主键索引排序,锁住原始数据中间的数据。

5.无索引行锁会升级为表锁

DROP TABLE IF EXISTS `t_mylock`;
CREATE TABLE `t_mylock` (
  `id` int(10) unsigned NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `t_mylock` (`id`,`name`) VALUES (3,'a');
INSERT INTO `t_mylock` (`id`,`name`) VALUES (5,'e');
INSERT INTO `t_mylock` (`id`,`name`) VALUES (8,'g');

session1:

begin;
select * FROM t_mylock where name='e' for update;
-- commit;

session2:

insert into t_mylock(id,name) values (1, 'a');    -- 阻塞
insert into t_mylock(id,name) values (100, 'z');  -- 阻塞

结论:给非索引字段加行锁会升级为表锁。

临键锁(Next-key Locks)

临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。

行锁分析

通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况

show status like'innodb_row_lock%';

image.png

对各个状态量的说明如下:
Innodb_row_lock_current_waits: 当前正在等待锁定的数量
Innodb_row_lock_time: 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg: 每次等待所花平均时间
Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间
Innodb_row_lock_waits:系统启动后到现在总共等待的次数

查看被锁住的表的sql:

查询是否锁表
show OPEN TABLES where In_use > 0;
查看所有进程
MySQL:
 
show processlist;
mariabd:
 
show full processlist;
查询到相对应的进程===然后 kill id
 
杀掉指定mysql连接的进程号
kill $pid
查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS; 
查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS; 
查看innodb引擎的运行时信息
show engine innodb status\G;
查看造成死锁的sql语句,分析索引情况,然后优化sql语句;
 
查看服务器状态
show status like '%lock%';
查看超时时间:
show variables like '%timeout%';

总结:

  • MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。
  • InnoDB在执行查询语句SELECT时,因为有mvcc机制不会加锁。但是update、insert、delete操作会加写锁。
  • InnorDb的行锁(包括记录锁、间隙锁和临键锁)是针对索引加的,如果加锁字段没有索引会升级为表锁。
  • 记录锁会锁住一行记录,被锁住后其他事务修改该数据会被阻塞。
  • 间隙锁只在可重复读(Repeatable read)和 串行化(Serializable)隔离级别下产生。
  • 间隙锁会封锁该条记录相邻两个键之间的空白区域,防止其它事务在这个区域内插入、修改、删除数据。
  • 间隙锁会在以下情况下产生:(1).使用普通索引加锁;(2).使用多列唯一索引加索;(3).使用主键索引或唯一索引锁定多行记录或锁定不存在的记录。

五. 死锁

测试数据脚本:

DROP TABLE IF EXISTS `t_mylock`;
CREATE TABLE `t_mylock` (
  `id` int(10) unsigned NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `t_mylock` (`id`,`name`) VALUES (1,'a');
INSERT INTO `t_mylock` (`id`,`name`) VALUES (2,'b');

什么是死锁

死锁定义:两个事务互相持有对方需要的锁,互相等待对方释放,并且都不释放。

例:

session1:

begin;
select * from t_mylock where id=1 for update;
select sleep(10);
select * from t_mylock where id=2 for update;
commit;

session2:

begin;
select * from t_mylock where id=2 for update;
select sleep(10);
select * from t_mylock where id=1 for update;
commit;

结果:

session1:

image.png

session2:

image.png

大多数情况mysql可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况mysql没法自动检测死锁。

查看近期死锁日志信息:show engine innodb status;

为什么会形成死锁

mysql并发控制有两种方式,一个是MVCC(多版本并发控制),用于读的时候。令一个是2PL(两阶段锁协议),用于写的时候。使用2PL加锁的时候就有可能产生死锁。

2pc分两个阶段:1.获取锁。2.释放锁。

2pc不会一次将所有需要使用的数据加锁,并且在加锁阶段没有顺序要求,所以这种并发控制方式会形成死锁。

mysql如何处理死锁

  1. 等待超时(innodb_lock_wait_timeout=50s)。
  2. 发起死锁检测,主动回滚一条事务,让其他事务继续执行(innodb_deadlock_detect=on)。

如何避免死锁

  1. 修改的时候将表顺序保持一致。
  2. 合理使用索引,缩小锁范围,最好使用主键索引或唯一索引检索数据。
  3. 尽可能减少检索范围,避免间隙锁。
  4. 将大事务拆分成小事务。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值