03 | 事务
事务的概念
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。
事务的几大特性
俗称 “ACID” (原子性,一致性,隔离性,持久性)
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务前后数据的完整性必须保持一致。
隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
隔离性与隔离级别
当数据库上有多个事务同时执行的时候,就可能出现 脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read) 的问题,为了解决这些问题,就有了“隔离级别”的概念。
前件:隔离的越严实,效率就会越低,但安全性会提高。所以很多时候需要根据业务需求做平衡。
SQL 标准的事务隔离级别包括:
- 读未提交(read uncommitted)
- 读未提交是指,一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交(read committed)
- 读提交是指,一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读(repeatable read)
- 可重复读是指,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化(serializable )
- 串行化,顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
以图上例子来说。在不同的隔离级别下,事务 A 会有哪些不同的返回结果,也就是图里面 V1、V2、V3 的返回值分别是什么。
- 隔离级别为:”读未提交”:
- v1,v2,v3 均等于 2
- 隔离级别为:”读提交”
- v1=1,v2,v3=2
- 隔离级别为:”可重复读”
- v1,v2=1,v3=2(A 事务结束了)
- 隔离级别为:”串行化”
- V1、V2 值是 1,V3 的值是 2。
- RC 级别下,MVCC 视图会在每一个语句前创建一个,所以在 RC 级别下,一个事务是可以看到另外一个事务已经提交的内容,因为它在每一次查询之前都会重新给予最新的数据创建一个新的 MVCC 视图。
- RR 级别下,MVCC 视图实在开始事务的时候就创建好了,这个视图会一直使用,直到该事务结束。 这里
- 要注意不同的隔离级别他们的一致性事务视图创建的时间点是不同的。
- RU:没有视图的概念,直接返回最小行数据。
- RC:在每一行 SQL 语句执行的时候创建。
- RR:在事务启动的时候创建。
- Serial:通过锁来实现数据访问,没有视图的概念。
- V1、V2 值是 1,V3 的值是 2。
我们可以看到在不同的隔离级别下,数据库行为是有所不同的。
Oracle 数据库的默认隔离级别其实就是“读提交”,因此对于一些从 Oracle 迁移到 MySQL 的应用,为保证数据库隔离级别的一致,你一定要记得将 MySQL 的隔离级别设置为“读提交”。
配置的方式是,将启动参数 transaction-isolation 的值设置成 READ-COMMITTED。你可以用 show variables 来查看当前的值。
如何查看当前数据库的隔离级别
show variables like 'transaction_isolation';
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 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。
有数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。
事务的启动方式
- 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
- set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
- 有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。总是使用 set autocommit=1, 通过显式语句的方式来启动事务。
commit work and chain
- 定义:提交事务并自动开启下一个事务
- 同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。
- 你可以在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
问题
事务的概念是什么?
- 一种机制,保证一组 SQL 语句要么全部成功,要么全部失败。
- mysql 的事务隔离级别读未提交, 读已提交, 可重复读, 串行各是什么意思?
- 读未提交:其他事务还未提交时,他做的数据改变就能被我方事务看到。
- 读已提交:其他事务提交之后,其做的数据改变才能被我方事务看到
- 在每一行 SQL 语句执行前,创立一个最新数据的视图
- 可重复读:其他事务修改数据,我方事务不去读取,依旧保持事务启动时的数据状态
- 通过事务启动时的视图来实现
- 串行化:我方事务不提交,其他事务就不能执行,被锁住
- 读已提交, 可重复读是怎么通过视图构建实现的?
- 可重复读的使用场景举例? 对账的时候应该很有用?
- 事务隔离是怎么通过 read-view(读视图)实现的?
- 并发版本控制(MCVV)的概念是什么, 是怎么实现的?
- 使用长事务的弊病? 为什么使用长事务可能拖垮整个库?
- 可能有非常久远的回滚日志
- 事务的启动方式有哪几种?
- 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
- set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
- 有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。总是使用 set autocommit=1, 通过显式语句的方式来启动事务。
- commit work and chain 的语法是做什么用的?
- 提交事务的同时自动开启下一个事务,简化过程,同时也方便开发人员知道语句是否在事务中
- 怎么查询各个表中的长事务?
- 如何避免长事务的出现?
- 首先,从应用开发端来看:
- 确认是否使用了 set autocommit=0。这个确认工作可以在测试环境中开展,把 MySQL 的 general_log 开起来,然后随便跑一个业务逻辑,通过 general_log 的日志来确认。一般框架如果会设置这个值,也就会提供参数来控制行为,你的目标就是把它改成 1。
- 确认是否有不必要的只读事务。有些框架会习惯不管什么语句先用 begin/commit 框起来。我见过有些是业务并没有这个需要,但是也把好几个 select 语句放到了事务中。这种只读事务可以去掉。
- 业务连接数据库的时候,根据业务本身的预估,通过 SET MAX_EXECUTION_TIME 命令,来控制每个语句执行的最长时间,避免单个语句意外执行太长时间。
- 其次,从数据库端来看:
- 监控 information_schema.Innodb_trx 表,设置长事务阈值,超过就报警 / 或者 kill;Percona 的 pt-kill 这个工具不错,推荐使用;
- 在业务功能测试阶段要求输出所有的 general_log,分析日志行为提前发现问题;
- 如果使用的是
MySQL 5.6 或者更新版本,把 innodb_undo_tablespaces 设置成 2(或更大的值)。如果真的出现大事务导致回滚段过大, 这样设置后清理起来更方便 。