文章目录
- 什么是数据库事务?
- 事务的四大特性:ACID
- 事务的开启与结束
- 案例表结构与数据
- 案例
- 事务并发的三大问题:
- 脏读(一个事务读取到了其他事务未提交的数据)
- 不可重复读(一个事务读取到其他事务已提交的数据造成读不一致)
- 幻读(一个事务读取到了其他事务新增的数据造成读不一致)
- 事务隔离级别 —— SQL92标准
- 事务隔离级别的修改
- 事务隔离级别的解决方案
- 多版本并发控制(MVCC)核心思想
- 案例
- MVCC 实现原理:Read View(一致性视图)存储内容
- ReadView判断规则
- RC(已提交读)与RR(可重复读)中ReadView的区别
- 锁🔒
- MyIsam支持表锁,InnoDB支持行锁
- 表锁与行锁的区别
- 表锁的锁定与解锁
- InnoDB锁类型(Lock Mode)
- 共享锁:
- 排他锁:
- 意向锁:意向共享锁(IS) / 意向排他锁(IX)
- 锁的作用:
- InnoDB的行锁到底锁的是什么?锁住的是索引。
- 案例
- 行锁的算法:锁住什么范围?
- 区间划分
- 记录锁(Record Lock):锁定记录
- 间隙锁(Gap Lock):锁定范围
- 临键锁(Next-key Lock):锁定范围加记录
- 事务隔离级别的实现:
- 事务隔离级别的选择:RC(已提交读)和RR(可重复读)的主要区别
- 死锁的条件
- 开启标准监控和锁监控
- 死锁的避免
- 事务的特性与事务并发造成的问题
- 事务读一致性问题的解决方案
- MVCC的原理
- 锁的分类,行锁的原理、行锁的算法
- 死锁的原因,如何避免
本文数据库版本:
select VERSION();
-- 5.7.41
show variables like '%engine%';
-- InnoDB
show global variables like '%tx_isolation%';
-- tx_isolation REPEATABLE-READ 可重复读
什么是数据库事务?
事务是数据库管理系统执行过程的一个逻辑单元,由一个有限的数据库操作序列构成。并不是所有的存储引擎都支持事务。在MySQL中支持事务的存储引擎包括InnoDB和NDB。
事务的四大特性:ACID
- Atomic:原子性(逻辑单位不可再分。要么全部成功,要么全部失败。InnoDB是通过undolog来实现的。)
- Consistency:一致性(数据库完整性的约束没有破坏)
- Isolation:隔离性(事务与其他事务之间相互隔离)
- Durability:持久性(事务提交的结果是持久的。崩溃恢复crash-safe。redolog+双写缓冲。)
事务的开启与结束
案例表结构与数据
CREATE TABLE `student` (
`id` int(16) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`sno` varchar(16) DEFAULT NULL COMMENT '学号',
`sname` varchar(64) DEFAULT NULL COMMENT '姓名',
`company` varchar(128) DEFAULT NULL COMMENT '公司',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `student` (`id`, `sno`, `sname`, `company`) VALUES (1, '1001', 'Huathy', 'Huathy');
INSERT INTO `student` (`id`, `sno`, `sname`, `company`) VALUES (2, '1002', 'hx', 'Huathy');
INSERT INTO `student` (`id`, `sno`, `sname`, `company`) VALUES (3, '1003', 'Dy', 'Huathy');
案例
UPDATE student SET sname = 'WHX' where id = 2;
-- 增删改语句默认开启事务 autocommit ON
show variables like '%autocommit%'
-- 开启事务;
begin;
update student set sname = 'whx2' where id = 2;
-- 这里还没有提交,新开一个窗口则会查询出不同的结果。
select * from student where id = 2;
rollback; -- 回滚事务,连接断开也会自动回滚
commit; -- 提交事务
-- 一个事务持有的锁会在事务结束的时候释放
事务并发的三大问题:
事务并发的三大问题,其实都是数据库读一致性的问题。必须由数据库提供一定的事务隔离机制来解决。
脏读(一个事务读取到了其他事务未提交的数据)
不可重复读(一个事务读取到其他事务已提交的数据造成读不一致)
幻读(一个事务读取到了其他事务新增的数据造成读不一致)
区别:只有插入数据导致的前后两次读不一致,才称为幻读。
事务隔离级别 —— SQL92标准
隔离级别 | 解决问题 | 存在问题 |
---|---|---|
Read UnCommitted(RU 未提交读) | 事务未提交的数据对其他事务也是可见的,会出现幻读。 | |
Read Committed(RC 已提交读) | 解决脏读问题 | 一个事务开启,只能看到已提交的事务所作的修改。 会出现不可重复读。 |
Repeatable Read(RR 可重复读) | 解决不可重复读问题 | 在同一个事务中多次读取同样的数据结果是一样的。 这种未定义解决幻读问题。 |
Serializable(可序列化) | 解决所有问题 | 最高的事务隔离级别,强行事务串行执行 |
事务隔离级别的修改
-- 设置事务隔离级别为 未提交读
set global transaction isolation level read uncommitted;
-- 设置事务隔离级别为 已提交读
set global transaction isolation level read committed;
-- 设置事务隔离级别为 可重复读
set global transaction isolation level repeatable read;
-- 设置事务隔离级别为 可序列化
set global transaction isolation level serializable;
事务隔离级别的解决方案
- 在数据读取前,对其加锁,阻止其他事务对数据进行修改(LBCC)。Lock Based Concurrency Control。
- 生成一个数据请求时间点的一致性数据快照(Snapshot),并利用这个快照来提供一定级别(语句级或事务级)的一致性读取(MVCC)。Multi Version Concurrency Control。
多版本并发控制(MVCC)核心思想
效果:建立一个快照,同一个事务,无论多少次查询的数据都是相同的。
规则:
- 一个事务能看到的数据版本:
- 第一次查询之前已经提交的事务修改
- 本事务的修改
- 一个事务不能看到的数据版本:
- 在本事务第一次查询之后创建的事务(事务ID比当前事务ID大)
- 活跃的(未提交的)事务的修改
案例
-- 创建表
DROP TABLE IF EXISTS `mvcctest`;
CREATE TABLE `mvcctest` (
`id` int(11) NULL DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- 插入数据
truncate table mvcctest;
begin;
INSERT INTO `demo`.`mvcctest`(`id`, `name`) VALUES (1, 'whx');
INSERT INTO `demo`.`mvcctest`(`id`, `name`) VALUES (2, 'Huathy');
commit;
MVCC 实现原理:Read View(一致性视图)存储内容
ReadView判断规则
- 从数据的最早版本开始判断(undo log)
- 数据版本的trx_id = creator_trx_id,本事务修改,可以访问。
- 数据版本的trx_id < min_trx_id(未提交事务的最小ID),说明该版本在生成Read View前已经提交,可以访问。
- 数据版本的trx_id > max_trx_id(下一个事务ID),该版本是生成Read View后才开启的事务,不能访问。
- 数据版本的trx_id在min_trx_id和max_trx_id之间,看看是否在m_ids(活跃事务ID)中,如果在则不可以。如果不在则可以。
- 如果当前版本不可见,就找undo log链中的下一个版本。
RC(已提交读)与RR(可重复读)中ReadView的区别
RR的ReadView是事务第一次查询的时候就创建的。
RC的ReadView是事务每次查询的时候都会重新创建。
锁🔒
MyIsam支持表锁,InnoDB支持行锁
表锁与行锁的区别
锁粒度:表锁 > 行锁
加锁效率:表锁 > 行锁
冲突概率:表锁 > 行锁
并发性能:表锁 < 行锁
表锁的锁定与解锁
lock tables xxx read;
lock tables xxx write;;
unlock tables;
InnoDB锁类型(Lock Mode)
- 共享锁(行锁):Shared Lock
- 排他锁(行锁):Exclusive Lock
- 意向共享锁(表锁):Intention Shared Lock
- 意向排他锁(表锁):Intention Exclusive Lock
共享锁:
也成为读锁,S锁。共享锁就是多个事务对于同一个数据可以共享同一把锁。都能访问到数据,但是只读不能改。
加锁释锁方式:
select * from student where id = 1 lock in shared mode;
commit / rollback;
排他锁:
也成为写锁,X锁。排他锁不能与其他锁并存,如果一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的锁(共享锁、排他锁)。只有该获取了排他锁的事务是可以对数据行进行读写的。
加锁释锁方式:
-- 自动方式:
-- delete / update / insert 默认加上X锁
-- 手动方式:
select * from student where id=1 for update;
commit / rollback;
意向锁:意向共享锁(IS) / 意向排他锁(IX)
意向锁是由数据库引擎自己维护的,用户无法手动操作意向锁。
- 意向共享锁(Intention Shred Lock):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁之前必须取得该表的IS锁。
- 意向排他锁(Intention Exclusive Lock):表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。
问题:为什么需要(表级别的)意向锁?类似于一个锁标志。
一个事务能够成功给一张表加上表锁的前提是没有任何一个事务已经锁定了这个表的任意一行数据。
锁的作用:
为了解决并发资源竞争的问题。
InnoDB的行锁到底锁的是什么?锁住的是索引。
案例
- t1表:不使用索引,锁表
DROP TABLE IF EXISTS `t1`;
CREATE TABLE `t1` (
`id` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `t1` (`id`, `name`) VALUES (1, '1');
INSERT INTO `t1` (`id`, `name`) VALUES (2, '2');
INSERT INTO `t1` (`id`, `name`) VALUES (3, '3');
INSERT INTO `t1` (`id`, `name`) VALUES (4, '4');
-- Session 1
begin;
SELECT * FROM t1 WHERE id =1 FOR UPDATE;
-- Session 2
select * from t1 where id=3 for update;
INSERT INTO `t1` (`id`, `name`) VALUES (5, '5');
行锁锁住的是索引。但是t1表中没有建立主键索引,InnoDB使用了隐藏的_rowId。而我们这里的select语句并没有使用索引来访问,导致全表扫描。所以锁住了所有行。也就是锁表了。这也是我们加锁必须带上精确条件的原因。
- 使用主键索引t2
DROP TABLE IF EXISTS `t2`;
CREATE TABLE `t2` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `t2` (`id`, `name`) VALUES (1, '1');
INSERT INTO `t2` (`id`, `name`) VALUES (4, '4');
INSERT INTO `t2` (`id`, `name`) VALUES (7, '7');
INSERT INTO `t2` (`id`, `name`) VALUES (10, '10');
-- Session 1
begin;
SELECT * FROM t2 WHERE id =1 FOR UPDATE;
-- Session 2
select * from t2 where id=1 for update;
select * from t2 where id=4 for update;
ROLLBACK;
- 使用唯一索引t3
DROP TABLE IF EXISTS `t3`;
CREATE TABLE `t3` (
`id` int(11) ,
`name` varchar(255) ,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `t3` (`id`, `name`) VALUES (1, '1');
INSERT INTO `t3` (`id`, `name`) VALUES (4, '4');
INSERT INTO `t3` (`id`, `name`) VALUES (7, '7');
INSERT INTO `t3` (`id`, `name`) VALUES (10, '10');
-- Session 1
begin;
select * from t3 where name= '4' for update;
-- Session 2
select * from t3 where name = '4' for update;
select * from t3 where id = 4 for update;
select * from t3 where id = 1 for update;
行锁的算法:锁住什么范围?
区间划分
记录锁(Record Lock):锁定记录
间隙锁(Gap Lock):锁定范围
GapLock只在RR(可重复读)中存在。用来阻塞插入。解决幻读问题。
-- Session 1
begin;
select * from t2 where name= '4' for update;
-- Session 2
INSERT INTO t2 VALUES (5, '5');
INSERT INTO t2 VALUES (6, '6');
临键锁(Next-key Lock):锁定范围加记录
Next-key Lock = GapLock + RecordLock
事务隔离级别的实现:
事务隔离级别的选择:RC(已提交读)和RR(可重复读)的主要区别
- RR的间隙锁会导致锁定范围的扩大
- 条件列未使用到索引,RR锁表,RC锁行
- RC的"半一致性"(semi-consistent)读可以增加update操作的并发性。
死锁的条件
互斥、不可剥夺、形成等待环路
开启标准监控和锁监控
# 开启标准监控和锁监控
set global innodb_status_output=on;
set global innodb_status_output_locks=on;
# 查看这条语句的加锁情况
show engine innodb status;
死锁的避免
- 顺序访问
- 数据排序
- 申请足够级别的锁
- 避免没有where条件(不命中索引)的操作
- 大事务分解成小事务
- 使用等值查询而不是范围查询