菜鸟笔记
提升您的技术认知

事务隔离:为什么你改了我还看不见?

1. 事务的概念

简单来说,事务就是要保证一组数据库操作,要么全部成功,要么全部失败

在MySQL中,事务是在存储引擎层实现的。MySQL时支持多种存储引擎的系统,但并不是所有存储引擎都支持事务。比如MyISAM引擎就不支持事务,所以这也是MyISAM被InnoDB取代的重要原因。

2. 隔离性和隔离级别

事务的特性包括:原子性、一致性、隔离性、持久性

当有多个事务同时执行时,可能会出现:脏读、不可重复读、幻读等问题
为了解决这些问题,就会涉及到隔离级别的概念。隔离得越严实,效率就会越低。SQL标准的事务隔离级别如下:

  1. 读未提交:一个事务还没提交时,它做的变更就能被其他事务看到
  2. 读提交:一个事务提交之后,它做的变更才会被其他事务看到
  3. 可重复读:一个事务执行过程中,总是跟这个事务在启动时看到的数据是一致的
  4. 串行化:对于同一行记录,写会加写锁,读会加读锁。当出现锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

举个例子:创建表T,其中只有一个字段c,其中一行的值为1

mysql> create table T(c int)engine=InnoDB;
insert into T(c) values(1);

然后分别执行两个事务。

事务A 事务B
启动事务,查询得到值1 启动事务
查询得到值1
将1改成2
查询得到值V1
提交事务B
查询得到值V2
提交事务A
查询得到值V3

对于不同的隔离级别,得到的结果也会不一样

  1. 读未提交:V1=V2=V3=2
                      V1的时候虽然事务B未提交,但是也能够被事务A看到。
  2. 读提交:V1=1,V2=V3=2
                      事务B对于值的修改,只有在提交后才能被事务A看到。
  3. 可重复读:V1=V2=1,V3=2
                      事务A在执行期间,看到的数据前后是一致的。
  4. 串行化”:V1=V2=1,V3=2
                      因为事务A先启动,所有只有当事务A执行完后,事务B才会开始执行。

对于四种隔离级别的实现原理是和视图相关的:

  1. 读未提交:直接返回记录上的最新值,没有视图的概念
  2. 读提交:视图是在每个SQL语句开始执行的时候创建的,期间可以修改视图
  3. 可重复读:视图是在这个事务启动的时候创建的,整个事务期间都使用这个视图
  4. 串行化:直接利用加锁的方式来避免并行访问

每一种隔离级别都是有用的,只是适用于不同的应用场景。

案例:
假设你在管理一个个人银行账户表。一个表存了每个月月底的余额,一个表存了账单明细。
这时候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明
细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结
果。
这时候使用“可重复读”隔离级别就很方便。事务启动时的视图可以认为是静态的,不受其
他事务更新的影响。

3. 事务隔离的实现

下面利用可重复读来说明事务隔离的具体实现。

在MySQL中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值

假设一个值从1被按顺序改成了2、3、4,在回滚日志中就会有类似下面的记录:

现在当前值是4,但是不同时刻启动的事务会有不同的read-view,能够看到不同的值。同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(mvCC)

同时你会发现,即使现在有一个事务将当前值改成5,也不会影响read-view A、read-view B、read-view C。

此外,回滚日志在不再需要的时候会进行删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志的时候会被删除
什么时候才是不需要呢?就是当前系统里没有比这个回滚日志更早的read-view的时候。

基于此,建议尽量不要使用长事务。因为长事务意味着系统里面会存在很老的事务视图。因为这些事务随时可能访问数据库里面的任何数据,所以在这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,会占用大量的空间。

4. 事务的启动方式

事务的启动方式有以下两种:

  1. 显式启动事务语句:begin或start transaction。配套的提交语句是commit,回滚语句是rollback。
  2. set autocommit=0:这个命令会将这个线程的自动提交关闭。意味着你只执行select语句,他也不会提交。这个事务一直持续到你主动执行commit或rollback语句,或断开连接。

通常是建议将set autocommit=1,通过显式的方式来启动事务

如果在需要频繁使用事务的业务,begin会带来多一次交互,那么就可以在autocommit=1的情况下,用begin显式启动事务,如果执行commit则提交事务。如果执行commit work and chain,则是提交事务并启动下一次事务。