mysql事务

事务

常见问题

脏写

现在有两个事务A和B,A和B同时在修改一个数据,A先更新,B再更新,但是B回滚了,那么A就是没有写进去,这个数据还是原来的值,这样的情况 称之为脏写

脏读

有两个事务A和B,A读取了B已经修改了,但是还没有提交事务的数据,之后B事务回滚了,就会造成A读取的数据和数据库中存储的数据不一样,这样的情况 称之为脏读

不可重复读

就是每次读取的数据,都和上次读取的数据不一样

比如一个事务A已经开始事务,读取到了一个数据的值,然后同时一个事务B修改了这数据并且事务提交,那么事务A再次过来查询,就是发现刚才的数据值发生变化了,变成数据B了,如果此时,事务A还没有提交,又过来一个事务C修改了这个数据并且提交,那么事务A再次来查询这个数据就变成了数据C,这种 在一个事务多次查询同一个值,但是查询结果每次都有变化的现象被 称为 不可重复读。

不可重复默认就避免了脏读的问题,因为只有在别的事务已经提交之后,当前在查询的事务才能看到新的值

幻读

当一个事务,用一个一样的sql进行多次查询之后,结果每次都会看到比上次多一些数据,这种现象被称为 幻读

长事务

尽量不要使用长事务

长事务意味着系统里面会存在很老的事务视图。由于这些事务随时可能访问数据库里面的任何数据,所以这个事务提交之前,数据库里面它可能用到的回滚记录都必须保留,这就会导致大量占用存储空间。

在 MySQL 5.5 及以前的版本,回滚日志是跟数据字典一起放在 ibdata 文件里的,即使长事务最终提交,回滚段被清理,文件也不会变小。我见过数据只有 20GB,而回滚段有 200GB 的库。最终只好为了清理回滚段,重建整个库。

查询尝事务

1
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60

级别

未提交

read uncommitted

这个级别的事务,是不允许发生脏写的,也就是说不可能两个数据在没有提交的情况下去更新同一行数据的值,但是在这种隔离的级别下,可能发生脏读,不可重复读,幻读

读提交(RC)

read commited

很明显,意思就是一个事务在没有提交的情况下,其他事务是读取不到这个事务的修改之后的值的。

可重复读(RR)

mysql默认的事务级别

这个级别下,不会发生脏写、脏读和不可重复读的问题,因为你一个事务多次查询一个数据的值,那么别的事务修改了这个值并且提交了,但是你还是不会读到其他事务修改过的值,你的事务一旦开始,多次查询一个值,会一直读到同一个值

串行化

这个级别的意思就是不会有事务并发执行了,一个一个执行

一般生产不会选择这个级别,因为这样数据库的性能太差了

MVCC

undolog版本链

image-20210331111047071

上图看起来很简单,无非就是一个新的事务来了,更新这个值,就在undolog版本链上增加一行数据,值更新,txr_id更新为书屋id,roll_pointer指向上一条数据的值

ReadView

执行一个事务的时候,就会生成一个ReadView

四个参数

  • m_ids 这个就是说此时有那些事务在mysql里执行还没有提交
  • min_trx_id 就是m_ids里最小的值
  • max_trx_id 就是myslq下一个要生成的事务di,就是最大的事务id
  • creator_trx_id 就是当前事务的事务id

执行过程

数据库里有一行数据

image-20210331123304477

有两个并发的事务过来了,一个是A,事务id=45,一个B,事务id=59,B是去更新这行数据的,A是去查询这个行数据的

  1. A事务生成一个ReadView,

    • m_ids 45,59
    • min_id 45
    • max_id 59
    • creator_trx_id 45
  2. A开始查询数据,A要进行判断,判断当前这行的数据的txr_id是否小于ReadView中的min_trx_id,小于最min_trx_id说明你事务开始之前这个事务已经执行完成了,所以可以看到这个事务的结果,也就是这行数数据

  3. image-20210331195633044

    B把这个数据更新了,于是就把B的事务id作为txr_id,B的值也放上去,然后roll_pointer指向原始值

  4. 此时A再过来查,还是先和当前txr_id比较,发现trx_id在m_ids之中,就知道这个事务是和自己的事务并发执行的,所以这个结果是读不到的, 于是就顺着链表往下查找,发现32是小于min_trx_id的,知道这行数据是在自己事务之前执行完成的,所以可以读到这行数据

  5. 最终读到的数据就是原始值

RC

RC的实现其实就是根据undolog版本链和ReadView来构成的,其实每次查询的时候都生成一个ReadView,这样就可以保证,虽然两个事务同时开启,其中一个写的数据先执行成功,另外一个读的事务可以读到修改后的值

重点在于:m_ids中代表这还活跃的时候,当发现这个事务的id在min_trx_id和max_trx_id之间,但是不在m_ids中的时候,表示的意思是 两个事务虽然同时开始执行,但是其中一个先提交了,所以另外一个事务可以看到这个事务已经提交的值

RR

rr通过undolog版本链和ReadView解决了不可重复读和幻读的问题

解决不可重复读和幻读问题,主要是靠 只 生成一次的ReadView来解决的

还用上面的那个例子来说明

刚开始是有一条原始数据 trx_id为32,然后事务A和事务B同时启动,事务A是查询,事务B是更新数据,在事务A第一次查询的时候,生成一次ReadView

  • m_ids 45,59
  • min_id 45
  • max_id 59
  • creator_trx_id 45

事务A第一次查询,在B事务还没有提交的时候,读取到的是原始值,因为32小于min_id

事务A第二次查询,在B事务已经提交的时候,此时trx_id变为了59,59在 在min_ids中,也在min_id和max_id之间,表示事务A和事务B是同时启动的,所以即使事务B提交了,但是A还是读取不到B修改后的值,读取到的还是原始值

重点在于:事务id 在min_ids中,也在min_id和max_id之间,表示事务A和事务B是同时启动的,并发启动,无论你事务是否提交,都只能读取到之前已经提交的数据

幻读,也是靠只 生成一次的ReadView来解决的,无论你修改多少回,事务A查询的这个ReadView永远不变,在B之后修改的时候id,肯定要大于B的事务id59,而大于max_id的意思是在之后才执行的事务,所以是读取不到的,所以就往前面找,往前找就到了上面不可重复读的情况,再往上就读取到了 原始数据

启动方式

两种

  • 显式启动事务语句, begin 或 start transaction。配套的提交语句是 commit,回滚语句是 rollback。
  • set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。

begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才真正启动。如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。

事务传播

事务传播 - Propagation

REQUIRED

使用当前的事务,如果当前没有事务,则自己新建一个事务,子方法是必须运行在一个事务中的;

如果当前存在事务,则加入这个事务,成为一个整体。

举例:领导没饭吃,我有钱,我会自己买了自己吃;领导有的吃,会分给你一起吃。

SUPPORTS

如果当前有事务,则使用事务;如果当前没有事务,则不使用事务。

举例:领导没饭吃,我也没饭吃;领导有饭吃,我也有饭吃。

MANDATORY

该传播属性强制必须存在一个事务,如果不存在,则抛出异常

举例:领导必须管饭,不管饭没饭吃,我就不乐意了,就不干了(抛出异常)

REQUIRES_NEW

如果当前有事务,则挂起该事务,并且自己创建一个新的事务给自己使用;

如果当前没有事务,则同 REQUIRED

举例:领导有饭吃,我偏不要,我自己买了自己吃

NOT_SUPPORTED

如果当前有事务,则把事务挂起,自己不适用事务去运行数据库操作

举例:领导有饭吃,分一点给你,我太忙了,放一边,我不吃

NEVER

如果当前有事务存在,则抛出异常

举例:领导有饭给你吃,我不想吃,我热爱工作,我抛出异常

NESTED

如果当前有事务,则开启子事务(嵌套事务),嵌套事务是独立提交或者回滚;

如果当前没有事务,则同 REQUIRED。

但是如果主事务提交,则会携带子事务一起提交。

如果主事务回滚,则子事务会一起回滚。相反,子事务异常,则父事务可以回滚或不回滚。

举例:领导决策不对,老板怪罪,领导带着小弟一同受罪。小弟出了差错,领导可以推卸责任。