MYSQL官方文档:https://dev.mysql.com/doc/refman/8.0/en/mysql-acid.html
事务的四大特性(ACID)
- 原子性(
Atomicity
)
事务是一个不可分割的单位,事务中的所有SQL等操作要么都发生,要么都不发生。 - 一致性(
Consistency
)
事务发生前和发生后,数据的完整性必须保持一致。 - 隔离性(
Isolation
)
事务和事务之间应该有一定的隔离措施来确保数据安全,在不同的隔离级别下,隔离性有不同的表现。 - 持久性(
Durability
)
一个事务一旦被提交,它对数据库中的数据改变就是永久性的。如果出了错误,事务也不允许撤销,只能通过“补偿性事务”
对于 ACD 比较好理解,但是隔离性却要看不同的隔离级别。
隔离级别
事务的隔离级别指的是两个事务之间的隔离状态,显然也是两个会话。
查看mysql隔离级别:select @@tx_isolation
设置当前会话的的事务级别:
set session transaction isolation level read uncommitted
set session transaction isolation level repeatable read
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 RU(read-uncommitted) | 是 | 是 | 是 |
读已提交 RC(read-committed) | 否 | 是 | 是 |
可重复读 RR(repeatable-read)默认 | 否 | 否 | 是 |
串行化 SE(serializable) | 否 | 否 | 否 |
名词解释
- 读未提交:一个事务还没有提交时,它做的变更就能被别的事务看到。
- 读已提交:一个事物提交之后,它做的变更才会被其他事务看到,推荐。
- 可重复读:一个事物执行过程中看到的数据,总是跟这个事务在
启动时
看到的数据是一致的,相当于事务启动时,就保存了当前数据库的快照,此事务读的时此快照而不是最新的数据。 - 可重复读的使用场景:假设你在管理一个个人银行账户表,一个表存了每个月月底的余额,一个表存了账单明细,这时候你要做数据校对,也就是判断上个月的余额和当前余额的差额,是否与本月的账单明细一致。你一定希望在校对过程中,即使有用户发生了一笔新的交易,也不影响你的校对结果。这时候使用“可重复读”隔离级别就很方便,事务启动时的视图可以认为是静态的,不受其他事务更新的影响。
- 串行化:在这种级别下事务之间绝对安全,对于同一行记录,写会加“写锁”,读会加“读锁”,当出现锁冲突时,后访问的事务需要等前一个事务执行完成,才能继续执行。比如事务A执行
select * from test
,但是没提交,此时事务B向test表插入一条记录会报错,表被锁了插入失败。 - 脏读:事务A读取了事务B更新的数据(但是B未提交),然后B回滚操作,那么A读取到的数据是脏数据,脏读出现在
RU
级别,不被允许。 - 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
- 不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。
- 大部分应用中应该设置为
read-committed
,不可脏读,幻读也无所谓,然后就是不可重复读,这一点很重要,因为有的时候我们在一个事务中会先去插入或修改一些数据,然后在这个事务中又去读它们,所以需要不可重复读。
补充
- MYSQL MVCC实现机制参考链接:https://blog.csdn.net/whoamiyang/article/details/51901888
- 关于next-key 锁可以参考链接:https://blog.csdn.net/bigtree_3721/article/details/73731377
事务隔离的实现
理解了事务隔离级别,我们再来看看事务隔离具体是怎么实现的,这里我们展开说明“可重复读”。在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 的时候,一个事务对应一个 read-view ,事务一旦提交或回滚,对应的 read-view 就会被删除,而回滚日志是系统异步去清理的。
基于上面的说明,我们来讨论一下为什么建议你尽量不要使用长事务。
长事务意味着系统里面会存在很老的事务视图,由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。
在MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小,作者见过数据只有20GB,而回滚段有200GB 的库,最终只好为了清理回滚段,重新建个库。
除了对回滚段的影响,长事务还占用锁资源,也可能拖垮整个库,这个我们会在后面讲锁的时候展开。
合理使用事务
既然事务如此消耗资源,那么没必要开事务就别开了,我看到有些人查询操作也要在事务中进行,实际上只要极少数的场景可能需要这么做(比如:可重复读),但是也没必要,因为可以通过其他途径来解决。然后要设置 autocommit 为 1 ,减少事务的持续时间。
你可以在 information_schema 库的 innodb_trx 这个表中查询长事务,比如下面这个语句,用于查找持续时间超过 60s 的事务。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60;