一、什么是事务?
事务 就是一组原子性的SQL操作,也就是说,事务内的SQL语句,要么全部执行成功,要么全部执行失败。
银行转账是解释事务的一个经典例子,假设一个🧑用户A(id=123)需要给👩用户B(id=456)转账1000
元,假设目前用户A、用户B余额都是2000
元(账户表:account),那么需要至少三个步骤:
- 检查用户A的账户余额是否高于
1000
元; - 从用户A的账户余额中扣减去
1000
元; - 给用户B的账户月增加
1000
元。
上述的三个操作必须实是在一个同一个事务中完成,任何一个步骤失败,则必须回滚所有的步骤,可以使用START TRANSACTION
语句开启一个事务,然后要么使用COMMIT
提交事务将修改的数据持久保留,要么使用ROLLBACK
撤销所有的修改,事务的SQL语义样本如下:
1 START TRANSACTION;
2 SELECT balance FROM account WHERE customer_id = '123';
3 UPDATE account SET balance = balance - 1000.00 WHERE customer_id = '123';
4 UPDATE account SET balance = balance + 1000.00 WHERE customer_id = '456';
5 COMMIT;
🤔Tip:试想一下,如果执行到第四条语句的时候服务器崩了,会发生什么?🧑用户A明明转账了
1000
元给👩用户B,但是用户B没有收到,最后结果是用户A无端端没了1000
块钱,如果用户A是你,我估计你要疯了~
很明显这就是一个事务问题,事务必须严格的遵守ACID
特性,ACID
分别表示:原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(duration),一个运行良好的事务处理系统,必须具备这边标准特性。
原子性(atomicity)
一个事务必须被视为一个不可分割的最小工作单元,整个事务的所有SQL操作要么全部提交成功,要么全部失败回滚,这就是事务的原子性。
一致性(consistency)
数据一定满足是由一个一致性状态转变到另外一个一致性状态的,也就是在前面的例子,一致性确保了,即使在执行第3、4条语句之间时系统崩溃,用户A的账户余额也不会损失1000元,因为事务最终并没有提交,所以事务中所做的修改不会保存到数据库中。
隔离性(isolation)
通常来说,一个事务所做的修改在最终提交之前,对其他事务是不可见的,也就是在前面的例子中,当执行完第3条语句时,第四条语句还每开始,此时有另一个用户C给A转500元,此时C看到的A的月并没有被减去1000,所以事务的隔离性通常来说是不可见的。
持久性(duration)
一旦事务提交,则其做的修改就会被永久保存到数据库,即便此时系统崩溃,修改的数据也不会丢失。
二、事务的隔离级别
事务的隔离级别主要分为四种:
- READ UNCOMMITTED(未提交读)
- READ COMMITTED(提交读)
- REPEATABLE READ(可重复读)
- SERIALIZABLE(串行化)
READ UNCOMMITTED(未提交读)
如果数据库是READ UNCOMMITTED
级别,事务中的修改,即使没有提交,对其他事务也是可见的,事务可以读取未提交数据,这也被称为脏读(Dirty Read)
,这个级别会导致很多问题,所以一般都不会用到该级别。
READ COMMITTED(提交读)
大多数数据库系统都默认隔离级别都是READ COMMITTIED
(MySQL不是),该级别表示一个事务开始前,只能看见已提交事务的所有修改,换句话说,一个事务开始直到提交之前,所做的任何修改对于其他事务来说都是不可见的,这个级别有时候也叫做不可重复读级别,因为两次执行同样的查询,可能得到不一样的结果。
REPEATABLE READ(可重复读)
REPEATABLE READ
解决了脏读的问题,该级别保证了在同一个事务中多次多次读取同样的记录结果是一致的。但是可重复读级别还是无法解决幻读
问题,所谓的幻读,指的是当某一个时间查询某个范围数据的时候,另一个事务有在该范围插入了新的记录,当之前的事务再次读取该范围的记录时,会产生 幻行(Phantom Row)
,也就是为什么第一次读是10行记录,再读一次变成11行记录了,InnoDB
通过MVCC(Multiversion Concurrency Control)机制
来解决了幻行的问题。
SERIALIZABLE(串行化)
SERIALIZABLE
串行化是最高隔离级别,它是通过强制事务串行执行,也就是新开启的事务需要等上一个事务提交之后才能开始执行,避免了前面所说的幻读的问题。简单来说,SERIALIZABLE
会在读取每一行数据的数据都加锁,所以会导致大量的超时和锁竞争的问题,考虑到性能问题,一般都不会用到该隔离级别。
开启/关闭自动提交(AUTOCOMMIT)
可以通过命令的形式设置AUTOCOMMIT变量的启用或者禁用自动提交模式,1或者ON表示启用,0或者OFF表示禁用,比如命令:SET AUTOCOMMIT = 0。
//关闭自动提交
SET AUTOCOMMIT = 0;
MySQL
可以通过执行SET TRANSACTION ISOLATION LEVEL
命令来实现事务隔离级别的设置,可以在配置文件中设置整个数据库的隔离级别,也可以只改变当前回话的隔离级别:
mysql > SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
MySQL对SQL显示加锁
- SELECT ... LOCK IN SHARE MODE (共享锁)
- SELECT ... FOR UPDATE (排它锁)
三、MVCC多版本并发控制
MySQL
的大多数事务存储引擎都实现的都不是简单的行级锁,为了提升并发性能的考虑,一般都同时实现多版本并发控制(MVCC),不仅仅是MySQL
,其实Oracls
、PostgreSQL
等其他诗句哭系统也都实现了MVCC
,但是各自的实现机制不仅相同。
可以认为MVCC
是行级锁的一个变种,但是它在很多情况下能避免加锁操作,因此性能开销更低。一般MVCC
的实现机制是:非阻塞读操作,写操作也是锁的必要的行。
MVCC解决并发哪些问题?
MVCC
用来解决读—写冲突的无锁并发控制,就是为事务分配单向增长的时间戳。为每个数据修改保存一个版本,版本与事务时间戳相关联。读操作只读取该事务开始前的数据库快照。
解决问题如下:
并发读-写时:可以做到读操作不阻塞写操作,同时写操作也不会阻塞读操作。 解决脏读、幻读、不可重复读等事务隔离问题,但不能解决上面的写-写 更新丢失问题。 因此有了下面提高并发性能的组合拳:
- MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突
- MVCC + 乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突
MVCC的实现原理
它的实现原理主要是版本链,undo
日志 ,Read View
来实现的
MVCC和事务隔离级别
上面所讲的Read View
用于支持RC(Read Committed,读提交)
和RR(Repeatable Read,可重复读)
隔离级别的实现。
RR、RC生成时机
- RC隔离级别下,是每个快照读都会生成并获取最新的Read View;
- 而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View,之后的查询就不会重复生成了,所以一个事务的查询结果每次都是一样的。
解决幻读问题
- 快照读:通过
MVCC
来进行控制的,不用加锁。按照MVCC
中规定的“语法”进行增删改查等操作,以避免幻读。 - 当前读:通过
next-key锁
(行锁+gap锁)来解决问题的。
四、总结
本文主要介绍了事务的ACID
特性、事务的隔离级别、MVCC
机制的实现原理和问题解决,MVCC
只在REPETABLA READ
、READ COMMITTED
两个隔离级别下工作,其他两个隔离级别都和MVCC不兼容,简单来说,所谓的MVCC
指的就是的REPETABLA READ
、READ COMMITTED
这两种事务隔离级别环境执行普通SELECT
操作时访问记录的版本链过程,这样子可以使不同事务的读-写、写-读操作并发执行,从而提升系统的性能。
作者:austin
链接:https://juejin.cn/post/7135822777424019486