事务就是要保证一组数据库操作,要么全部成功,要么全部失败。在 MySQL 中,事务支持是在引擎层实现的。所以,MySQL 是一个支持多引擎的系统,但并不是所有的引擎都支持事务。比如 MySQL 原生的 MyISAM 引擎就不支持事务,这也是 MyISAM 被 InnoDB 取代的重要原因之一。
事务特性
-
A(Atomicity):原子性
原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。 -
C(Consistency):一致性
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。 -
I(Isolation):隔离性
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。 -
D(Durability):持久性
一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
隔离级别
- 读未提交(read uncommitted):是指一个事务还没提交时,它做的变更就能被别的事务看到
- 读提交(read committed):是指一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化(serializable ):是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
下面举个例子说明:
mysql> create table T(c int) engine=InnoDB;
insert into T© values(1);
事务A | 事务B |
---|---|
启动事务查询得到值1 | 启动事务 |
查询得到值1 | |
将1改为2 | |
查询得到值V1 | |
提交事务B | |
查询得到值V2 | |
提交事务A | |
查询得到值V3 |
分析不同隔离级别下,对应变量的值:
- 隔离级别是“读未提交”:V1=2,V2=2,V3=2 。此时事务 B 虽然还没有提交,但是结果已经被 A 看到了
- 隔离级别是“读提交”:V1=1, V2=2, V3=2。此时事务 B 的更新在提交后才能被 A 看到。
- 隔离级别是“可重复读”:V1=1, V2=1, V3=2。此时V2等于1,是因为事务在执行期间看到的数据前后必须是一致的
- 隔离级别是“串行化”:V1=1, V2=1,V3=2。此时在事务 B 执行“将 1 改成 2”的时候,会被锁住。直到事务 A 提交后,事务 B 才可以继续执行。
Mysql在隔离级别的实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
- “可重复读”隔离级别:视图是在事务启动时创建的,整个事务存在期间都用这个视图
- “读提交”隔离级别:视图是在每个 SQL 语句开始执行的时候创建的
- “读未提交”隔离级别:直接返回记录上的最新值,没有视图概念
- “串行化”隔离级别:直接用加锁的方式来避免并行访问
查看数据库隔离级别
mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED |
+-----------------------+----------------+
虽然数据库有多种隔离级别,但是存在即合理,每种隔离级别都有自己的使用场景,你要根据自己的业务情况来定。比如“可重复度”的场景如下:
比如某网站的金币有余额和消费明细两张表,到了月底你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。这时候使用“可重复读”隔离级别就很方便。事务启动时的视图可以认为是静态的,不受其他事务更新的影响。
事务隔离的实现
事务隔离的实现是由多版本并发控制,通过undo log版本链和read-view实现事务隔离(MVCC)。
另外在 MySQL 中,实际上每条记录在更新的时候都会同时记录一条回滚操作。记录上的最新值,通过回滚操作,都可以得到前一个状态的值。即除了记录变更记录,还会记录一条变更相反的回滚操作记录,前者记录在redo log,后者记录在undo log。
假设一个值从 1 被按顺序改成了 2、3、4,在回滚日志里面就会有类似下面的记录。
这一点其实就是一个版本迭代记录,类似功能的实现的还有GIT等等这种版本管理工具,只有将每次修改都记录下来,才能方便回滚到指定的每次提交。
当前值是 4,但是在查询这条记录的时候,不同时刻启动的事务会有不同的 read-view。如图中看到的,在视图 A、B、C 里面,这一个记录的值分别是 1、2、4,同一条记录在系统中可以存在多个版本,就是数据库的多版本并发控制(MVCC)。对于 read-view A,要得到 1,就必须将当前值依次执行图中所有的回滚操作得到。【在可重复读隔离级别中,表中的数据其实已经改变,在前面的视图里,需要查找某条记录时,是通过取当前数据,再取视图对应的回滚段回滚到该视图的值。】
同时你会发现,即使现在有另外一个事务正在将 4 改成 5,这个事务跟 read-view A、B、C 对应的事务是不会冲突的。【因为有行锁保证不会出现一个事务把4改到5,然后另外一个事务回滚的情况】
回滚日志删除时机
在不需要的时候才删除。也就是说,系统会判断,当没有事务再需要用到这些回滚日志时,回滚日志会被删除。【没有其它事物线程还在使用当前版本的undo时候,purge进程进行回收。】
什么时候才不需要了呢?就是当系统里没有比这个回滚日志更早的 read-view 的时候。【当没有比回滚日志更早的读视图(读视图在事务开启时创建)的时候,这个数据不会再有谁驱使它回滚了,这个回滚日志也就失去了用武之地,可以删除了】
长事务弊端
长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。【因为长事务的存在会导致undolog一直存在不被删除即长事务导致对应的事务视图长时间存在,且对应的回滚日志也是会一直存在的】
在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。5.7版本支持单独配置undo log的路径和表空间文件。
除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库。
事务的启动方式
- 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
- set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
比如有些客户端连接框架会默认连接成功后先执行一个 set autocommit=0 的命令。这就导致接下来的查询都在事务中,如果是长连接,就导致了意外的长事务。因此建议你总是使用 set autocommit=1, 通过显式语句的方式来启动事务。
如果觉得提交事务,然后又开启事务存在“多一次交互”的问题,对于一个需要频繁使用事务的业务,第二种方式每个事务在开始时都不需要主动执行一次 “begin”,减少了语句的交互次数。如果你也有这个顾虑,我建议你使用 commit work and chain 语法。【即提交事务并开启下一个事务(少执行一个begin语句)】
在 autocommit 为 1 的情况下,用 begin 显式启动的事务,如果执行 commit 则提交事务。如果执行 commit work and chain,则是提交事务并自动启动下一个事务,这样也省去了再次执行 begin 语句的开销。同时带来的好处是从程序开发的角度明确地知道每个语句是否处于事务中。
查找持续时间超过 60s 的事务
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
避免长事务的影响
从应用开发端着手:
- 确认是否使用了 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(或更大的值)。如果真的出现大事务导致回滚段过大,这样设置后清理起来更方便。
innodb_undo_tablespaces是控制undo是否开启独立的表空间的参数
为0表示:undo使用系统表空间,即ibdata1
不为0表示:使用独立的表空间,一般名称为 undo001 undo002,存放地址的配置项为:innodb_undo_directory
一般innodb_undo_tablespaces 默认配置为0,innodb_undo_directory默认配置为当前数据目录