文章目录
学习来源: MySQL45讲
1、什么是事务?
- 简单的说,事务就是要
保证一组数据库的操作,要么全部成功,要么全部失败
。 - 在MySQL中,事务支持是
在引擎层实现的
。MyISAM是不支持事务的,InnoDB支持事务
。 - 事务的四大特性:
- 原子性(A):指事务包含的所有操作要么全部成功,要么全部失败回滚。
- 一致性(C):指事务必须使数据库从一个一致性状态变为另一个一致性状态。
- 隔离性(I):当多个用户并发访问数据库的时候,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
- 持久性(D):是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
2、隔离性与隔离级别
- 当数据库上有多个事务同时执行的时候,就可能出现脏读(读到了另一个事务修改但没提交的数据)、不可重复读(同一个事务前后两次读同一个数据不一致,一般是更新)、幻读的问题(一个事务读取表的时候由于另一个事务插入或者删除数据导致其前后两次数据读取不一致)。为了解决,就有了隔离级别的概念。
- 隔离的越严,效率就越低。
- SQL标准的事务隔离级别包括:
- 读未提交(read uncommitted):一个事务还没有提交,变更就可以被其他事务看到。
- 读已提交(read committed):一个事务
提交
之后,做的变更会被其他事务看到。 - 可重复读(repeatable read):
是MySQL的默认事务隔离级别
,确保同一事务的多个实例在并发读取数据的时候,会看到同样的行数。 - 串行化(serializable ):对于同一行记录,写会加锁,读也会加锁。出现读写锁冲突的时候,后访问的事务必须等待前一个事务执行完成才能继续执行。
2.1、隔离级别演示
讨论在不同隔离级别下,事务A会有哪些不同的返回结果,即:V1、V2、V3的返回值分别是什么?
- 读未提交:V1的值是2,虽然B还没提交,但是此时结果已经被A看到了,那么V2、V3也都是2.
- 读已提交:V1的值是1,当提交事务B才能被A看见,那么V2为2、V3也为2。
- 可重复读:V1、V2的值为1,但是当提交事务A的时候,再查询,此时V3值为2。V2值为1的原因是
因为事务在执行期间看到的数据前后必须一致
。只有提交了事务,才能看见值的变化。 - 可串行化:当事务B执行“将1改为2”的时候,会被锁住,只有当事务A提交之后,事务B才能继续执行,那么V1、V2为1,V3为2。
2.2. 视图
- 实现上,数据库里面
会创建一个视图
,访问的时候是以视图的逻辑结果为准的
。 - 在
可重复读的隔离级别
下,这个视图
是在事务启动的时候创建的
,整个事务存在期间都用这个视图。 - 在
读已提交的隔离级别
下,这个事务
是在每个sql语句开始执行的时候创建的
。 - 在
读未提交的隔离级别
下,直接返回记录上的最新值
,没有视图概念
。 - 在
串行化的隔离级别
下,是直接用加锁
的方式避免并行访问
。
2.2.1、可重复读的场景
- 设想在管理一个个人银行账户表。一个表存了账户余额,一个表存了账单明细。月底需要做数据校对,即:判断上个月的余额和当余额的差额,是否与本月账单明细一致。那么,希望在校对的过程中,即使用户发生一笔新的交易,也不影响校对结果。
- 此时可重复读的隔离级别,因为
视图
是在事务启动的时候创建的
,可以认为视图是静态的
,不受其他事务更新的影响
。
2.3、事务隔离的实现
- 具体展开可重复读。
- 在MySQL中,实际上
每条记录在更新的时候都会同时记录一条回滚操作
。记录上最新值,通过回滚操作
,都可以得到前一个状态的值
。 - 假设一个值从1被按顺序改成了2、3、4。在
回滚日志
里面会有类似下面的记录
- 当前值是4,但是在查询这条记录的时候,
不同时刻启动的事务会有不同的read-view
。同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(mvcc)
。- 对于read-view A,要得到1,就必须将当前值一次执行图中
所有
的回滚操作。- 即使现在有
另外一个事务正在将4改成5
,那这个事务与read-view A、B、C对应的事务是不会冲突
的。
2.3.1、回滚日志
- 了解到上述情况之后,知道回滚日志在每条记录更新的时候都会进行记录。那么
回滚日志什么时候删除
呢,不可能一直保留。回滚日志在不需要的时候就删除
。即:系统会判断
,当没有事务再需要到这些回滚日志
的时候,那么回滚日志就会被删除
。 - 那么
什么时候不需要
?答:系统没有比这个回滚日志更早的read-view的时候
。 - 那么也就是为什么建议尽量不要使用长事务。
长事务
就意味着系统里面存在很老的事务视图
。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间
。- 除了对回滚段的影响,
长事务还占用锁资源
,也可能会拖垮整个库。
3、事务的启动
- 上面说了长事务存在潜在风险,所以还是尽量避免。
- MySQL事务的启动方式:
- 显示启动事务:begin或者start transaction。那么配套的提交就是commit。回滚就是rollback。
- set autocommit = 0,这个命令会将这个
线程的自动提交关闭
。即:如果只执行了一个select语句,这个事务就启动
了,并且不会自动提交
。这个事务会持续存在直到主动执行commit或者rollback语句,或者断开连接
。
- 建议使用方法:
- 如果考虑多一次交互问题,可以使用commit work and chain语法(提交事务并开启下一个事务,少执行一个begin语句)。
- 使用set autocommit=1,通过显示语句方式启动事务,用begin显示启动事务,执行commit表示提交事务。
- 可以在information_schema库的innodb_rx这个表中查询长事务。下面这个语句用于查找持续时间超过60s的事务。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
4、补充:MVCC
学习地址:MVCC
- 在MySQL InnoDB引擎下RC、RR基于MVCC(多版本并发控制)进行并发事务控制。
- MVCC是基于“数据版本”对并发事务进行访问。
- 举例:
RR隔离级别:事务D在两次查询的时候读出的结果是张三。
RC隔离级别:事务D在第一次查询的时候读出的是张三,第二次查询的时候是张小三。
4.1、undo log版本链
- trx_id记录的是最后一次的进行更新的事务编号。因为三次事务执行完成之后,最终数据是张老三,执行的trx_id是3,所以第一个就记录的trx_id是3。
- DB_ROLL_PTR是一个指针信息,指向上一次进行版本变化的时候数据是什么。
- 最开始的数据是没有trx_id以及DB_ROLL_PTR的。
4.2、回滚日志删除
- undolog版本链是不会立即删除的,MySQL确保版本链数据不再被其它并行事务引用之后再进行删除。
4.3、ReadView
- readview就是“快照读”SQL执行的时候MVCC提取数据的依据。
快照读
就是最普通的查询语句。(select…)当前读
指执行下列语句的时候进行数据读取的方式:insert、update、delete、select… for update(加排它锁)、select … lock in share mode(加共享锁)。- 共享锁:又称为读锁,多个事务对于同一个事务对于同一个数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
- 排它锁:又称为写锁,一个事务获取了一个数据行的排它锁,其他事务就不能再获取改行的其他说,包括共享锁和排它锁,但是获取排它锁的事务是可以对数据进行读取和修改的。
- readview四个字段:
m_ids:记录哪些事务还没有被提交。只要提交了就没有处于当前活跃的事务编号集合中。
4.3.1、RC下的readview
-
在该隔离级别下,每执行一次快照读的时候生成readView。
-
readview生成之后决定如何从版本链中进行数据提取
- 第一个readview:
- 在undo log版本链中,trx_id=3满足在区间[2,5],但是3在m_ids中,说明事务还没有被提交,不允许访问。
- 代入到第二行。判断依旧不满足条件,再向下。
- 查找到第三行,此时trx_id=1满足<min_trx_id(2),说明事务已经提交了,可以进行访问。那么此时的select语句(第一次查询)就会返回id=1088,姓名=张三。(即:一号事务提交的数据)
- 第二个readview:
- 同理:此时select查询语句就是id=1088,姓名=张小三。(即:二号事务提交的数据)。
- 此时就出现了不可重复读的现象。
- 第一个readview:
4.3.1、RR下的readview
- 仅在第一次执行快照读的时候生成readview,后续快照读会复用。(也有例外)。
- RR下生成的readview
- 因为生成的版本链没有变化,以及readview前后规则相同、判断规则也一样,所以读出来的数据一定是一致的。
- 所以两次快照读出来的都是id=1088,姓名=张三。
- RR能使用MVCC避免幻读,但又不完全能。因为MVCC不是采用锁的方式对事务、数据进行隔离。
- 连续多次快照读,readview会产生复用,没有幻读问题,例外:当两次快照读之前存在当前读(insert、update、delete、select… for update、select … lock in share mode),readview就会重新生成,这样就会导致幻读。
因为事务2进行了update(当前读),就会重新生成一个readview,之后的快照读就会重新生成readview,当前数据就会重新读取。导致事务2这里读取出来多了一行数据,产生了幻读问题。