事务:保证一组数据库操作,要么全部成功,要么全部失败。在MySQL中,事务支持是在引擎层实现的,所以本次分析事务是以 InnoDB 为例。
隔离性与隔离级别
事务有四大特性:ACID(Atomicity、consistency、Isolation、Durability),分别是原子性、一致性、隔离性、持久性。
当多个事务同时执行的时候就有以下问题:
- 脏写(dirty write):事务 B 去修改了事务 A 修改过的值,但是此时事务 A 还没提交,所以事务 A 随时会回滚,导致事务 B 修改的值也没了。
- 脏读(dirty read):事务 B 去查询了事务 A 修改过的数据,但是此时事务 A 还没提交,所以事务 A 随时会回滚导致事务 B 再次查询就读不到刚才事务 A 修改的数据。
- 不可重复读(nonrepeatable read):事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了。也就是当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配。
- 幻读(phantom read):事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了。
注:隔离越严实,效率就会越低
SQL标准的隔离事务级别包括:
- 读未提交(read uncommitted):一个事务没有提交,它做的变更就能被其他事务看到。
- 读提交(read committed):一个事务提交之后,他做的变更才能被别的事务看到。
- 可重复读(repeatable read):一个事务执行中看到的数据,总是跟这个事务在启动时看到的数据是一样的。当前这个事务没提交,其他事务也看不到更变内容。
- 串行化(serializable):对同一记录,“写”会加“写锁”,“读”会加“读锁”。
例如:
insert into T(c) values(1)
运行顺序 | 事务A | 事务B |
---|---|---|
﹀ | 启动事务,查询得到值为 1 | 启动事务 |
﹀ | 查询得到值为1 | |
﹀ | 将 1 改为 2 | |
﹀ | 查询得到值 V1 | |
﹀ | 提交事务B | |
﹀ | 查询得到值 V2 | |
﹀ | 提交事务 A | |
﹀ | 查询的到值 V3 |
在不同的隔离条件下 V1、V2、V3 的值不同:
- 读未提交:V1、V2、V3 都是 2。
- 读提交:V1 是 1,V2、V3 都是 2。
- 可重复读:V1、V2、V3 都是 1。事务在执行期间看到的数据前后一致
- 串行化:在事务 B 执行“将 1 改为 2”时(应该是执行动作之前),会被锁住。直到 A 提交事务后才能执行。所以 V1、V2 是 1,V3 是 2。
在底层实现上,数据库会创建一个视图,访问的时候以视图的逻辑结果为准。
- 读未提交:直接返回记录上的最新值,没有视图概念。
- 读提交:这个视图是在每个 SQL 语句
- 可重复读:这是视图是在事务启动时创建的,整个事务期间都用这个视图。
- 串行化:直接用加锁方式来避免并行访问。(最霸道)
注:Orcale 默认使用的是“读提交”,MySQL 默认使用的是“可重复读”
事务隔离的实现
这里展开说明“可重复读”。
在 MySQL 中,每条记录在更新的时候都会同时记录一条回滚操作。
在下图中,就是一个值从 1 改到了 2、3、4,在回滚日志里面就会有类似的记录。
当前值是 4 ,但是在查询这条记录的时候,不同时刻启动的事务就有不同的 read-view。在图中的 A、B、C 视图里面,这个记录的值是 1 、2、4,同一条记录有着不同的版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要想得到 1,就必须把图中的操作全部回滚。
而且如果同时有另一个事务正在将 4 改成 5,那么这个事务和 read-view A、B、C 是不会冲突的。
当系统中没有这个回滚段更早的 read-view 时,那这个日志文件就会被删除。
所以尽量不要使用长事务。长事务意味着系统中会有很古老的事务视图,会导致占用大量的存储空间。除了对回滚段的印象,长事务还占用锁资源,也可能会拖垮整个库。
事务的启动方式
MySQL事务的启动方式有以下几种:
- 显示启动事务语句,
begin
或start transaction
。配套的提交的语句是commit
,回滚语句是rollback
。 set autocommit = 0
,这个命令会将这个线程的自动提交关掉。哪怕只执行一个select
语句,这个事务也启动了,不会自动提交。直到你主动执行commit
和rollback
语句,或者断开连接。
建议使用 setautocommit = 1
来显示启动事务。
如果纠结“多一次交互”问题。对于一个需要频繁使用事务的业务,第二种方法在事务开始时不会执行一次 begin
,减少语句的交互。建议使用 commit work and chain
语法。
对于 commit work and chain
会提交事务并自动启动下一个事务,省去了下次执行 begin
语句。同时,从程序开发的角度明确知道了每个语句是否处于事务中。