事务就是要保证一组数据库操作,要么全部成功,要么全部失败。
在MySQL中,事务支持是在引擎层实现的。MySQL支持多引擎,但并不是所有的引擎都支持事务。比如MySQL原生的 MyISAM 引擎就不支持事务,这也是MyISAM 被 InnoDB 取代的重要原因之一。
1、隔离性与隔离级别
提到事务,肯定会想到ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔离性、持久性),今天要说的是“I”,也就是“隔离性”。
当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。
SQL标准的事务隔离级别包括:
-
读未提交(read uncommitted)
一个事务还没提交时,它做的变更就能被别的事务看到
-
读提交(read committed)
一个事务提交之后,它做的变更才会被其他事务看到
-
可重复读(repeatable read)
一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
-
串行化(serializable)
顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务务必须等前一个事务执行完成,才能继续执行。
其中“读提交”和“可重复读”比较难理解,所以我用一个例子说明这几种隔离级别。假设数据表T中只有一列,其中一行的值为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 |
在不同的隔离级别下,事务A会获得不同的返回结果:
- 读未提交。V1、V2、V3值为2。虽然V1 时,事务B还未提交。
- 读提交。V1为1,V2、V3值为2。事务B的更新在提交后才能被A看到。
- 可重复读。V1、V2为1,V3为2。V2之所以还是1,遵循的是事务在执行期间看到的数据前后必须是一致的。
- 串行化。V1、V2为1,V3为2。事务B在执行“将1改成2”时,会被锁住。直到事务A提交之后,事务B才可以继续执行。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
- RR级别:视图是在事务启动时创建的,整个事务存在期间都用这个视图
- RC级别:在每个SQL语句开始执行的时候创建的
- RU级别:没有视图概念,直接读取记录上最新的值
- Serial级别:直接用加锁的方式来避免并行访问,也没有视图概念
可以通过配置参数transaction-isolation
来指定隔离级别,可以用show variables
来查看当前的值。
mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set (0.00 sec)
存在即合理,每种隔离级别都有自己使用的场景,需要根据具体业务情况来使用。
2、事务隔离的实现
以下是针对“RR-可重复读”隔离级别说明
在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的时候。
基于上面的说明,建议不要使用长事务。
长事务意味着系统里面会存在很老的事务视图。由于这个事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。
除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,这个在后面锁的章节会展开说。
3、事务的启动方式
MySQL的事务启动方式有以下几种:
- 显式启动事务语句,
begin
或start transaction
,配套的提交语句是commit
,回滚语句是rollback
。 -
set autocommit = 0
,这个命令会将这个线程的自动提交关掉。意味着哪怕你只执行一个select
语句,这个事务就启动了,而且不会自动提交。这个事务会持续到你主动commit
或者rollback
,或者断开连接。
有些客户端连接框架会默认连接成功后先执行一个 set autocommit = 0
的命令。这就导致接下来的操作都在事务中,如果是长连接,就导致了意外的长事务。
因此,建议使用set autocommit = 1
,通过显式语句的方式来启动事务。
可以在 information_schema
库的 innodb_trx
这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
网友评论