什么是事务
一次业务交互涉及多行记录的时候(不管是否扩表),需要保证要么都成功,要么都失败。典型的就是转账场景:先查询余额A账户有100,A少100,B多100,如果在查询余额之后,另一个线程在执行从A给C转账,也是查询到A有100,这样继续操作A的账户很可能是负的。
示例准备工作
为了更好演示下面的各种情况, 需要先创建相应的表。
-- 建表语句
CREATE TABLE `t_transaction` (
`id` INT NOT NULL AUTO_INCREMENT,
`account` INT NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT;
-- 插入数据
INSERT INTO t_transaction (id,account) VALUES(1,1000);
INSERT INTO t_transaction (id,account) VALUES(2,1000);
下面场景需要模拟2个事务,因此需要开2个会话。
事务隔离级别
一般我们说事务要满足ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性),这里核心讨论的是隔离性。
首先效率和安全总是矛盾的,所以隔离级别越高说明牺牲的性能越大。在数据库上并发执行事务的时候(效率),就可能出现:脏读、幻读、不可重复读
读未提交(脏读):
对应的隔离级别是读未提交,一个事务还没提交时,他做的变更就被别的事务看到了。
举个例子:
A 的工资是5k,财务在发工资的时候开启一个事务1,给A账户增加了工资5k(此时事务未提交)。
此时A去查看工资,能看到刚发的工资,很开心。
结果发工资的事务1 在执行其他记录的时候出现异常,导致事务回滚,A这个时候再去看工资又是没发的情况,这个时候就要骂人了。
示例:
会话1 模拟用户查看的情况
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; # 需要设置隔离级别为读未提交
START TRANSACTION;
SELECT * FROM t_transaction;
会话2 模拟财务发工资
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE t_transaction SET account = account+5000 WHERE id = 1;
注意这个时候还没提交事务。
会话1 查一下记录,发现用户1 的工资已经到账了。
SELECT * FROM t_transaction;
会话2 进行事务回滚
rollback;
此时会话1 再查询的时候会发现到手的工资没了。
从上面可以看出来这个问题还是挺大,读到别人还没提交的数据(脏数据,所以叫脏读),会带来很大的风险。
读已提交(不可重复读&幻读)
对应的隔离级别是读已提交。这2个非常像,而且在mysql 下他们的隔离级别是一致,所以放一起讲。
不可重复读:
事务1 在整个事务中存在多次读取同一数据的情况,第一次查询的时候值是1000。
事务2 在这个过程中修改了这条数据并提交了事务,将值改为2000。
事务1 再一次查询的时候发现值变成了2000 。
在一个事务内2次读到的数据是不一样的,因此叫不可重复读。
幻读:
事务1 需要操作所有工资是1000 的记录,此时查询数据库有100 条记录。
事务2 此时要操作其中一个员工离职,那个员工的工资也是1000 。
事务1 再次读取工资是1000的记录,发现只有99条记录。
在一个事务内2次读到的数据记录数是不一样的,就跟产生幻觉一样。
小结:
从上面的描述可以看到共同点都是同一个事务的2次查询产生的结果不一样,区别是一个记录数是一样的但是内容不一样,另一个则是记录数不一样。
示例:
事务1,第一次查询
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
select * from t_transaction ;
事务2,操作数据,这个时候还没提交。
# 不可重复读
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
update t_transaction set account=account+5000 where id=1;
SELECT * FROM t_transaction;
此时事务1 查询的时候看到的是还是1000。
将事务2的事务提交,再在事务1中查询的时候,发现数据变了。
因此读已提交能解决脏读的问题,但是不能解决不可重复读和幻读的问题。为了解决这2个问题我们需要更严格的事务隔离级别。
可重复读
这是MySQL 的默认隔离级别(repeatable read)。
示例:
事务1 开启查询
set session transaction isolation level repeatable read;
start transaction;
select * from t_transaction
注意这个时候没有commit
会话2 进行插入操作
# 事务1加上共享锁之后,事务2update 操作会被卡住
START TRANSACTION;
update t_transaction set account=account+200 where id=1;
commit;
# 可以查询是否生效
select * from t_transaction
会话1 这个时候继续查询,会发现记录并没有变更。也就是避免了不可重复读的情况。
串行化
隔离级别是Serializable
事务1 进行查询操作,这个时候事务没有commit
set session transaction isolation level serializable;
start transaction;
select * from t_transaction;
事务2 进行update 操作
START TRANSACTION;
update t_transaction set account=account+200 where id=1;
这个时候会发现事务阻塞了
时间太长会超时。
如果在事务2 阻塞的时候,事务1 提交了,那么事务2也能顺利的提交。
不仅仅是update 数据,insert 此时也是不会成功的,说明锁住的是整张表。因此如果是serializable 级别,读写将是一把锁,虽然保证了很强的一致性,但是对性能是巨大的损害,基本不太可能在实际使用。
最后
概念有点多,还是挺容易搞混的,特别是幻读和不可重复读。梳理一下好很多,当然时间久了也可能会忘记。后面会进一步分析事务的实现原理MVCC希望能进一步加深理解,敬请期待。