事务隔离级别及相关锁实践

343 阅读5分钟

这是我参与新手入门的第1篇文章

事务隔离性

事务的四个特性:原子性,一致性,隔离性,持久性。 其中原子性、一致性、持久性容易理解。今天重点来聊聊隔离性。

隔离性简单来说,就是每个事务是相互隔离的,你做你的事,他做他的事,大家相互隔离开来。 但是,大家都是在同一个数据库下操作,那这个「隔离」到底隔离到什么程度呢?是“老死不相往来”,还是”临时隔离“,又或者是"你中有我,我中有你"?这就是涉及到了新的概念:隔离级别

事务隔离级别

事务隔离级别有:

  • 读未提交(READ-UNCOMMITTED):最低的隔离级别,允许读取尚未提交的数据变更
  • 读已提交(READ-COMMITTED):允许读取并发事务已经提交的数据
  • 可重复读(REPEATABLE-READ):对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改
  • 串行化(SERIALIZABLE):最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰

MySQL默认隔离级别为:可重复读

通过实际演示,我们可以很好理解各隔离级别。 相关准备:

MySQL 版本:5.7 (可通过 select version() 命令查询)

表:

CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`number` varchar(20) NOT NULL COMMENT '学号',
`name` varchar(20) DEFAULT NULL COMMENT '姓名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表';

初始数据:

id	number	name
1	2021070501	张三

演示工具:Navicat (通过开启多个查询窗口,实现开启多个事务)

演示一:

步骤查询窗口1(事务1)查询窗口2(事务2)
step1执行SQL:
-- 开启事务
BEGIN;
> OK
> 时间: 0s
UPDATE student SET name = '李四' WHERE id = 1;
> Affected rows: 1
> 时间: 0.002s
step2BEGIN;
SELECT * FROM student WHERE id = 1;
=======结果=======
id number name
1 2021070501 张三
step3COMMIT;
> OK
> 时间: 0.004s;
step4SELECT * FROM student WHERE id = 1;
=======结果=======
id number name
1 2021070501 张三
step5COMMIT;
> OK
> 时间: 0s
step6BEGIN;
SELECT * FROM student WHERE id = 1;
=======结果=======
id number name
1 2021070501 李四

从上面演示可以分析出:

  1. step2中的结果可以看到name还是「张三」,说明事务2并没有读取事务1未提交的数据,所以可以排除「读未提交」隔离级别
  2. 如果MySQL事务的默认隔离级别为「读已提交」,则step3已提交事务1,step4中的结果应为「李四」,但实际结果还是「张三」,这从侧面印证了MySQL事务的隔离级别为「可重复读」(两次读取的结果都为张三)

事务相关的锁

事务之间虽然有隔离性,但如果两个事务操作同一数据,那结果会怎么样呢,最终以哪个事务执行结果为准?没有事务的情况下,我们很容易理解,哪条SQL语句最后执行,就以哪个数据为准。但加了事务,情况就变得复杂起来了,SQL执行顺序与事务提交顺序可能不一致。比如:

事务1先执行

UPDATE student SET name = '张三1' WHERE id = 1;

事务2再执行

UPDATE student SET name = '张三2' WHERE id = 1;

然后先提交事务2,再提交事务1。那最终的结果是「张三1」还是「张三2」呢?站在SQL执行顺序角度来说,结果应该为「张三2」;站在事务提交顺序来说,结果应为「张三1」。

那最终实际结果呢?答案是:这种情况永远不会发生!

我们可以实际操作试试: 演示二:

步骤查询窗口1(事务1)查询窗口2(事务2)
step1BEGIN
> OK
> 时间: 0s


UPDATE student SET name = '张三1' WHERE id = 1
> Affected rows: 1
> 时间: 0.001s
step2BEGIN
> OK
> 时间: 0s


UPDATE student SET name = '张三2' WHERE id = 1
======观察结果=======
没反应,查询时间一直走
step3COMMIT;
> OK
> 时间: 0.004s;
step4再次打开窗口,会发现有以下结果
> Affected rows: 1
> 时间: 4.843s

从演示二我们可以看出,step2并不能正常执行,需要等待step3提交。这就是MYSQL的机制。

当事务1先执行UPDATE student SET name = '张三1' WHERE id = 1时便获得数据行(id=1)的锁。事务2想执行UPDATE student SET name = '张三2' WHERE id = 1时,因为该数据行是被锁住的,所以不能执行,需要等待事务1释放锁。事务1提交事务便会把锁释放掉。所以,不同事务之间修改同一数据不会出现修改顺序和提交顺序不一致的情况。

按照锁粒度,可以把MYSQL锁简单归为:行锁表锁。行锁就是锁定行数据,表锁是锁定整张表数据。上面所说示例,就是行锁。一般来说,匹配条件是主键或索引列时,数据的update和delete会加行锁。如果增删改查时匹配的条件字段不带有索引,innodb使用的将是表级锁。由此可见,我们数据库表设计中合理的索引是非常关键的。

有锁的地方,必然就会出现死锁的问题。以后有机会再聊更多的锁及死锁问题