开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情
简介
在进入正题之前我们先简单了解一下什么是数据库事务,数据库事务是指在数据库一系列SQL操作中,这个流程中的任务要么全部完成,要么全部失败,并且数据库事务必须满足ACID特性。
事务的特性
数据库事务存在四种特性,分别为:原子性(Atomic)、一致性(Consistent)、隔离性(Isolation)、持久性(Durable),即ACID特性。
原子性:事务即是一个工作单元,在数据库事务中需要保证当前事务的数据要么全部提交,要么全部回滚。
例如网上购买一个商品,商家收到订单后开始发货,经过一系列的物流运输最终到客户手里,客户可以选择签收或者拒收,拒收货物就会退回到商家,那么这整个过程要么交易完成,要么拒收退回。
一致性:事务必须保证数据从一个状态转变至另一个状态时,开始的数据与最终的数据必须是一致的(这里的一致指的不是数据一样,参考以下例子)
例如有一个彩票站一共有100张刮刮乐,我们去购买10张,那么就会有一笔出售10张的订单,库存就剩下90张刮刮乐,也就是这个购买前后刮刮乐的总量都是100一致不变的。
隔离性:在多个事务并发使用同一个数据库资源时,事务在运行过程中会根据隔离级别体现出不同的影响程度,后面我们会更详细的进行解说。
好比几个货运人员在同时搬一批货,货物搬运的过程中他们之间会不会出现小插曲,最终导致的结果也就无法定论,这就需要依靠我们的隔离级别来决定。
持久性:每一个事务操作执行完成后(提交事务),最终数据的变化结果都将持久化记录到磁盘上,不可再进行回滚操作。
事务隔离级别
数据库事务隔离级别分为四种:读未提交(Read Uncommited)、可重复读(Repeatable Read)、读已提交(Read Commited)、串行化(Serializable)。
MySql数据库默认使用的是可重复读,Oracle默认使用的是读已提交,这四种隔离级别都有什么不同,应该如何使用接下来做进一步分析。
不同隔离级别的影响程度
隔离级别\影响程度 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | ✔ | ✔ | ✔ |
读未提交 | ❌ | ✔ | ✔ |
可重复读 | ❌ | ❌ | ✔ |
串行化 | ❌ | ❌ | ❌ |
脏读:事务A在执行过程中,事务B对同一数据进行更新操作,此时事务B未执行完,事务A即可读取到事务B未提交的数据进行运算,此时事务B在后面流程仍可能对数据进行修改或者回滚,最终将造成事务A读取的数据为脏数据。
不可重复度:多个事务并发处理同一数据的过程中,事务A第一次读取数据data1,事务B同时对数据data1进行更新并提交事务,事务A再次读取data1时与最初读取的数据不一致,也就是并发过程中事务的一致性被破坏。
幻读:与不可重复读类似,一个事务按照相同的条件读取数据时,发现其他事务为满足各自业务诉求插入新数据,这种情况就称为幻读。
例如一个商家系统挂单出售商品库存为100,假设被购买掉10个后库存剩余90,此时客户A再次购买一个商品,而客户B对购买的一个商品不满意进行退款,此时库存既减1又加1,最终结果库存量还是一致的,但是实际情况出现了退货的商品,这种场景就称之为幻读。
事务隔离级别配置
为了方便大家更好的理解,我们先将看看怎么配置事务隔离级别,再根据具体的隔离级别结合数据进行分析。
- 通过配置文件配置
在我们mysql的安装目录都会有一个
my.ini
数据库配置文件,在文件里添加transaction-isolation = READ-COMMITTED
即可配置默认隔离级别为读已提交。具体大家可以根据需要进行更改隔离级别配置:READ-UNCOMMITED、READ-COMMITED、REPEATABLE-READ、SERIALIZABLE
,需注意更改配置文件后数据库必须要重启后才能生效。
- 使用命令进行设置
-- 设置全局事务隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置当前session事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
- 查看当前事务隔离级别
-- 查看全局事务隔离级别
SELECT @@GLOBAL.transaction_isolation;
-- 查看当前session事务隔离级别
SELECT @@SESSION.transaction_isolation;
四种事务隔离级别
这里我们先创建一个学生信息表,并初始化数据以协助分析:
DROP TABLE IF EXISTS t_student;
CREATE TABLE `t_student`(
`id` BIGINT(12) NOT NULL AUTO_INCREMENT,
`name` VARCHAR(30) COMMENT '姓名',
`number` VARCHAR(12) COMMENT '学号',
`height` INT COMMENT '身高cm',
`weight` INT COMMENT '体重kg',
PRIMARY KEY (`id`)
) ENGINE=INNODB CHARSET=utf8 COLLATE=utf8_general_ci;
INSERT INTO t_student(`name`, number, height, weight)
VALUES
('张三', '20230010001', 0, 0),
('李四', '20230010002', 0, 0),
('王五', '20230010003', 0, 0);
读未提交
修改事务隔离级别为读未提交:SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
读未提交也就是说多个事务并发的时候,事务修改数据但未进行提交的操作对其他事务来说是可见的,由此我们来验证一下。
模拟事务并发,我们使用两个数据库连接并开启事务START TRANSACTION
。
在事务A中查看学生信息如下:
在事务B对学生(张三)进行数据更新如下:
此时事务B数据并未进行提交,这时候我们再去事务A查看学生数据:
这时我们就会发现事务A读取到了事务B未提交的数据,这种情况就发生了读未提交。如果此时学生(张三)的数据未被事务B提交,而此时事务A将此数据输出作为学生信息的实际结果,然后事务B再将数据进行回滚,结果就导致事务A的数据出现了脏读。
读已提交
修改事务隔离级别为读已提交:SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED
同上初始化后的学生数据,开启两个事务进行分析:
同样事务B对学生(李四)进行数据更新:
在事务B未进行数据提交时,事务A读取的数据仍为原来更新前的数据:
当事务B进行commit
提交数据后,事务A再次刷新查看数据:
可重复读
修改事务隔离级别为读已提交:SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ
同时开启两个事务,初始数据情况如下:
使用事务A对学生(王五)进行数据修改并进行数据提交如下:
此时在事务B中再次刷新查看学生数据:
这时候我们发现事务B查询到的数据跟原来事务开始时的一样,这就说明了在一个事务中数据是可重复读,其他事务数据提交与否并不会重新读取最新数据。
串行化
修改事务隔离级别为读已提交:SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE
可串行化指的是数据库每个事务独立执行,每个SQL的执行都会枷锁,让所有的数据操作都是串行的。
总结
不同数据库事务隔离级别产生的问题,都是由于事务并发操作同一数据出现的问题,所以在单个事务运行时或者隔离级别为串行化时就不会出现此问题,但是限制系统只允许单事务运行时将会大大降低系统并发能力,一般系统都会采用数据库连接池,所以还是得根据实际业务场景充分考虑后进行设计。