此篇文章用来加深自己对事务的理解,同时为了让大家少搜索到一些杂乱无序的文章。如果文中有纰漏不严谨的地方或错误的地方,欢迎各位大佬批评指出。
在讲述之前,我们可以先了解下 什么是事务?
事务一般是指逻辑上的一组操作或者作为单个逻辑单元执行的一系列操作。通俗点讲就是,我们平常的下单(预扣库存的场景)业务,我们在下单的逻辑处理中,肯定会涉及到扣减库存业务方法和创建订单的业务方法。这两个业务方法整合在一起的下单业务就会构成事务。
1.1 事务的特性
原子性 要么全部成功,要么全部失败
我们写下单业务方法实现的结果,不能有扣减库存的业务方法异常,竟然还能创建订单
一致性
我们在写业务方法实现的时候,需要保证扣减库存的数量和订单sku(以库存为计量单位)的数量保持一致
隔离性
隔离性体现在并发场景下,也就是在下单业务中(扣减库存的业务方法有减库存的动作)中的事务没有提交之前,有个查询库存数量业务的方法,看到的还是未扣减的数量,不能读取到我们下单业务的事务还没有提交,已扣减库存后的数量
持久性
落盘操作,所有的业务处理,在数据库层面都是需要持久化的。如果没有这个,就像所有的操作没有数据作为凭证。
1.2 事务类型
扁平事务
我们平常最常见的一种,由begin或者start transaction字段开启,以commit或者end transaction结束。它是以一种整体操作的,要么全部成功,要么全部失败。不能支持部分事务回滚的特征。
带有保存点的扁平事务
savepoint机制,事务内部通过该机制可以实现事务部分回滚。mysql语法采用 设置保存点通过语法 savepoint point_name
, 回滚采用语法rollback to point_name
实现。
这里强调一点,普通的扁平事务也是有一个隐式的保存点,默认就是在事务开始的时候自动设置的
链式事务
链式事务是在带有保存点的扁平事务基础上扩展的,首先它是通过多个事务组成,第一个事务的提交和下一个事务的开始操作是具备原子性的操作,也就是上一个事务的提交对于下一个事务是可见的。多个事务组成起来就像链条一样一直传递下去。
链式事务回滚的时候,只能回滚自己当前所在事务的保存点,不能回滚到前面事务提交的保存点
嵌套事务
和链式事务不同,它的特征就像洋葱一样的,外层事务里面还有内层的子事务。内层子事务提交完成,外层事务并不会提交。只有外层的事务提交完成后,整体的事务才算提交完成。
1.3 并发事务带来的问题
针对大多数互联网领域,mysql数据库是使用最多的关系型数据库之一。既然是并发,那肯定是两个或以上的事务同时发生才产生的,这里我们就拿mysql数据库来介绍并发事务带来的问题
脏写
场景:一行数据记录字段a的值为0,当A事务更新一行数据记录的时候,将字段a值改为1,而B事务同时更新这行数据将字段a改为2,那么B事务就会覆盖A事务的更新操作。
如何解决?
首先,我们分析下这个本质上就是写写操作的冲突,那解决办法就是让每个事务串行执行,按照一定的顺序依次执行写操作
脏读
场景:一行数据记录字段a的值为0,当A事务更新一行数据记录的时候,将字段a值改为1,但是这个时候,事务还没有提交。B事务就读取这行记录的a字段值1
如何解决?
首先,我们分析下这个本质上就是读写操作的冲突,那解决办法就是让A事务写先操作完,B事务再读
不可重复读
场景:一行数据记录字段a的值为0,B事务读取到值为0,在B事务中做其他操作的时候(A事务操作将a值改为了1),其他操作做完这个时候,再次读取该行记录的时候,读取到字段a的值为1。
如何解决?
首先,我们分析下这个本质就是读写操作的冲突,那解决办法就是B事务第二次读的时候,读操作先执行,A事务再写
幻读
场景:A事务现在查询某个时间点产生了多少个订单,B事务这个时候删除一个订单或者添加一个订单,那么A事务在B事务操作前和操作后统计的数量就是不一样的
如何解决?
首先,我们分析下这个本质上就是读写操作的冲突,那解决办法就是A事务先读操作完,B事务在写
1.4 事务隔离级别
同样,这里我们也使用mysql数据库来讲述对应的隔离级别
首先,我们提隔离级别,指的是mysql InnDb存储引擎提供的。
-- 如何查看mysql数据库默认事务隔离级别
-- 全局事务隔离级别
SELECT @@GLOBAL.tx_isolation;
-- 当前数据库连接的事务隔离级别
SELECT @@SESSION.tx_isolation;
读未提交
B事务可以读A事务没有提交到的记录,一听这个,就是上面我们脏读啊。那这种脏读、不可重复读、幻读并发问题都没有解决
读已提交
B事务读取A事务提交到的记录,解决了脏读问题,但是没有解决不可重复读、幻读的问题
可重复读
同一个事务无论何时查询的数据都和第一查询的数据一样,但是没有解决幻读的问题
串行化
每次读取数据时,都是获取表级别的共享锁,读写阻塞。这种简直是灾难性的控制,说白了就是让数据库丢失支持并发特性啊,当然就没有并发带来的事务问题
1.5 mysql事务隔离实践
首先声明点这里以下sql演示的mysql版本是5.7.29 ,其他版本没有试过,现象可能会有不同
-- 初始化数据
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`balance` decimal(10, 2) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
INSERT INTO `account`(`id`, `name`, `balance`) VALUES (1, '张三', 300.00);
INSERT INTO `account`(`id`, `name`, `balance`) VALUES (2, '李四', 350.00);
INSERT INTO `account`(`id`, `name`, `balance`) VALUES (3, '王五', 500.00);
读未提交
A会话 时间点1
set session transaction isolation level read uncommitted;
start transaction ;
select * from account;
B会话 时间点1
set session transaction isolation level read uncommitted;
start transaction ;
update account set balance = balance +100 where id = 1;
A会话 时间点2
可以看到B会话并没有提交 已经看到B提交后的数据 张三的金额变为了400
select * from account;
B 时间点2 会话执行回滚操作
rollback;
A会话 时间点3
可以看到张三的金额 回滚到之前的初始值300
select * from account;
A会话 时间点4
更新记录金额减去100
update account set balance = balance -100 where id = 1;
select * from account;
总结下上面的演示结果
首先是并发情况下,整个操作下来,就是张三金额初始值为300,B事务操作+100,A事务读取到400,结果B事务异常回滚了,数据库记录回到了300。但是A事务操作了-100 ,按照道理张三的金额应为300,但是却是200。因为A事务根本不不知道B事务回滚了呀。这就是脏读
读已提交
A会话 时间点1
set session transaction isolation level read COMMITTED;
start transaction ;
select * from account;
B会话 时间点1
set session transaction isolation level read COMMITTED;
start transaction ;
update account set balance = balance +100 where id = 1;
A会话 时间点2
可以看到a查询出来的张三的金额 依然是300和时间点1是一样的,说明读提交的隔离级别解决了,脏读的问题
select * from account;
B会话时间点2
commit;
A会话 时间点3
可以看到张三的金额变为了400,说明有不可重复读的问题
select * from account;
可重复读
A会话 时间点1
set session transaction isolation level REPEATABLE READ;
start transaction ;
select * from account;
B会话 时间点1
set session transaction isolation level REPEATABLE READ;
start transaction ;
update account set balance = balance +100 where id = 1;
commit;
select * from account;
A会话 时间2
可以看到张三的金额和时间点1 是一样的,说明解决了脏读、可重复读的问题。
select * from account;
A会话时间点3
update account set balance = balance +100 where id = 1;
select * from account;
可以看到张三的金额为500,这个一致性的问题 并没有遭到破坏。可重复读的隔离级别使用了MVCC的机制,使用的是快照读。并不会更新版本号
但是insert、update、delete则会更新版本号,是当前读。
B会话时间点2 新打开事务
可以看到显示出来新插入的数据赵六
start transaction ;
insert into account(name,balance) values ('赵六',100);
commit;
select * from account;
A会话 时间点4
可以看到赵六并没有显示,说明没有幻读的问题。
select * from account;
A会话 时间点5
update account set balance = balance +100 where id = 4;
select * from account;
可以看到A执行完更新操作后,出现了幻读问题。如何解决这个问题呢? 使用可串行化的事务隔离级别或者间隙锁和临键锁
串行化
A会话时间点1
set session transaction isolation level SERIALIZABLE;
start transaction ;
select * from account;
B会话时间点1
可以看到更新操作发生了阻塞,解决了幻读问题
set session transaction isolation level SERIALIZABLE;
start transaction ;
update account set balance = balance +100 where id = 1;