`五. 事务到底是隔离的还是不隔离的

在 MySQL 中, 事务支持是在引擎层实现的.

1. 事务隔离

  • Atomicity 原子性
  • Consistency 一致性
  • Isolation 隔离性
  • Durability 持久性

1.1. 隔离性与隔离级别(ACID)

问题: 当数据库上有多个事务同时执行的时候, 就可能出现脏读(dirty read)不可重复读(non-repeatable read)幻读(phantom read的问题, 为了解决这些问题, 就有了“隔离级别”的概念.

SQL 标准的事务隔离级别包括:读未提交(read uncommitted)读提交(read committed)可重复读(repeatable read)串行化(serializable)

  • 读未提交: 一个事务还没提交时, 它做的变更就能被别的事务看到.
  • 读提交 : 一个事务提交之后, 它做的变更才会被其他事务看到.
  • 可重复读(mysql默认级别): 一个事务执行过程中看到的数据, 总是跟这个事务在启动时看到的数据是一致的. 当然在可重复读隔离级别下, 未提交变更对其他事务也是不可见的.
    • 在“可重复读”隔离级别下, 这个视图是在事务启动时创建的, 整个事务存在期间都用这个视图,
    • 在“读提交”隔离级别下, 这个视图是在每个 SQL 语句开始执行的时候创建的
  • 串行化 : 顾名思义是对于同一行记录, “写”会加“写锁”, “读”会加“读锁”. 当出现读写锁冲突的时候, 后访问的事务必须等前一个事务执行完成, 才能继续执行.

  • 若隔离级别是“读未提交”: 则 V1 的值就是 2. 这时候事务 B 虽然还没有提交, 但是结果已经被 A 看到了. 因此, V2、V3 也都是 2.
  • 若隔离级别是“读提交”: 则 V1 是 1, V2 的值是 2. 事务 B 的更新在提交后才能被 A 看到. 所以, V3 的值也是 2.
  • 若隔离级别是“可重复读”: 则 V1、V2 是 1, V3 是 2. 之所以 V2 还是 1, 遵循的就是这个要求:事务在执行期间看到的数据前后必须是一致的. (即视图)
  • 若隔离级别是“串行化”: 则在事务 B 执行“将 1 改成 2”的时候, 会被锁住. 直到事务 A 提交后, 事务 B 才可以继续执行. 所以从 A 的角度看, V1、V2 值是 1, V3 的值是 2.

数据库里面会创建一个视图, 访问的时候以视图的逻辑结果为准.

  • “读未提交”隔离级别下, 直接返回记录上的最新值, 没有视图概念;
  • 在“读提交”隔离级别下, 这个视图是在每个 SQL 语句开始执行的时候创建的.
  • 在“可重复读”隔离级别下, 这个视图是在事务启动时创建的, 整个事务存在期间都用这个视图.
  • “串行化”隔离级别下直接用加锁的方式来避免并行访问.
# 查看事务的级别
show variables like 'transaction_isolation'
# 查看持续时间超过60s的事务
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

1.2. 事务的启动方式

  1. 显式启动事务语句, beginstart transaction. 配套的提交语句是 commit, 回滚语句是 rollback.
  2. set autocommit=0, 这个命令会将这个线程的自动提交关掉. 意味着如果你只执行一个 select 语句, 这个事务就启动了, 而且并不会自动提交. 这个事务持续存在直到你主动执行 commitrollback 语句, 或者断开连接.

1.3 事务启动的时机

begin/start transaction 命令并不是一个事务的起点, 在执行到它们之后的第一个操作 InnoDB 表的语句, 事务才真正启动。如果你想要马上启动一个事务, 可以使用 start transaction with consistent snapshot 这个命令。

在 MySQL 里, 有两个“视图”的概念:

  • 一个是 view。它是一个用查询语句定义的虚拟表, 在调用的时候执行查询语句并生成结果。创建视图的语法是 create view … , 而它的查询方法与表一样。
  • 另一个是 InnoDB 在实现 MVCC 时用到的一致性读视图, 即 consistent read view, 用于支持 RC(Read Committed, 读提交)和 RR(Repeatable Read, 可重复读)隔离级别的实现。

2. 如何避免长事务对业务的影响?

2.1从应用开发端来看

  • 确认是否使用了 set autocommit=0. 这个确认工作可以在测试环境中开展, 把 MySQL 的 general_log 开起来, 然后随便跑一个业务逻辑, 通过 general_log 的日志来确认. 一般框架如果会设置这个值, 也就会提供参数来控制行为, 你的目标就是把它改成 1.

  • 确认是否有不必要的只读事务. 有些框架会习惯不管什么语句先用 begin/commit 框起来. 我见过有些是业务并没有这个需要, 但是也把好几个 select 语句放到了事务中. 这种只读事务可以去掉.

  • 业务连接数据库的时候, 根据业务本身的预估, 通过 SET MAX_EXECUTION_TIME 命令, 来控制每个语句执行的最长时间, 避免单个语句意外执行太长时间. (为什么会意外?在后续的文章中会提到这类案例)

2.2 从数据库端来看

  • 监控 information_schema.Innodb_trx 表, 设置长事务阈值, 超过就报警 / 或者 kill;
  • Perconapt-kill 这个工具不错, 推荐使用;
  • 在业务功能测试阶段要求输出所有的 general_log, 分析日志行为提前发现问题;
  • 如果使用的是 MySQL 5.6 或者更新版本, 把 innodb_undo_tablespaces 设置成 2(或更大的值). 如果真的出现大事务导致回滚段过大, 这样设置后清理起来更方便.