聊一聊MySQL事务

一、事务为何物

事务(Transaction)是保障程序中一组操作的原子性的约束,它使事务中的所有操作都指向同一个结果,也就是要么所有的操作都执行成功,要么所有的操作都执行失败,不允许出现其他结果。例如银行转账,从A账户扣除金额,向B账户添加金额,这两个数据库操作的总和构成一个完整的逻辑过程,不可拆分,这个过程被称为一个事务。在MySQL中,目前只有InnoDB引擎支持事务。

二、事务的特性

数据库管理系统在写入或更新数据的过程中,为保证事务是正确可靠的,需要具备四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。

  • 原子性(Atomicity):一个事物中的所有操作,要么全部完成,要么全部失败,不会在中间某个环节结束。若事务在执行过程中发生异常,所有的操作都会被回滚到事务开始前的状态,就像这个事务从没执行过一样。
  • 一致性(Consistency):事务操作的数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定,也就是在事务开始之前和结束之后,数据库的完整性没有被破坏。
  • 隔离性(Isolation):数据库允许多个事务并发执行,隔离性是为了防止多个事务并发执行导致数据的不一致,事务之间是相互隔离的。事务隔离有四种级别:未提交读(Read UnCommitted)、已提交读(Read Commited)、可重复读(Repeatable Read)、串行化(Serializable)
  • 持久性:事务成功提交之后,对数据的修改是永久性的,即便系统故障也不会丢失。

三、为什么要有四种隔离级别

SQL标准定义了4种隔离级别用来限定不同的事务场景,按照隔离级别从低到高为:读未提交、读已提交、可重复读、串行化,级别越高,所支持的并发度越低。

不同的隔离级别会造成不同的影响,体现在数据上就是脏读、不可重复读和幻读。

  • 脏读:A事务读取了B事务中未提交的数据,在A事务提交之前,B事务进行了回滚,此时A事务中的数据就不正确了,所以被定义为脏数据。
  • 不可重复读:A事务在第一次读取之后到第二次读取之前,B事务对该数据进行了修改,导致A事务两次读取的数据不一致,这就是不可重复读
  • 幻读:幻读一般发生在范围查询的情况下,A事务第一次读取一批数据,在第二次读取之前,B事务向数据库中插入了新的符合A事务查询条件的数据,此时A事务第二次读取出来的数据条数不一致,这种情况对于A事务来说就是出现了幻读。

事务隔离级别

  • 读未提交(Read UnCommitted):该隔离级别下的事务可以看到其他事务未提交的执行结果,会引起脏读、不可重复读和幻读,在实际应用中几乎不会使用该级别的事务。
  • 读已提交(Read Committed):这是大多数数据库系统的默认隔离级别(如Oracle、阿里云的MySQL)等,但不是官方MySQL默认的。它不允许事务看到未提交的事务中的数据,使事务只能看见已经提交的事务所做的改变。该隔离级别会引起不可重复读和幻读。
  • 可重复读(Repeatable Read):这是官方MySQL的默认事务隔离级别,它确保同一事务多次读取的数据的一致性,解决了不可重复读的问题。该隔离级别解决的主要是对数据库进行UPDATE操作造成的数据改变,但还是会引起幻读的情况发生,在InnoDB存储引擎下默认提供MVCC(多版本并发控制)机制解决了幻读的问题。
  • 串行化(Serializable):这是最高的隔离级别,串行的意思也就是每次只允许一个事务对数据进行操作,事务按照先来后到的规则进行排队一次执行,这样在事务之间就不会相互冲突,从而解决了幻读的问题。但是串行化执行事务的方式会严重影响事务的执行效率,高并发操作下会造成事务堆积和超时,一般在实际应用中很少使用,虽然它很安全。

通过上面我们了解了事务隔离级别,也知道每种隔离级别所解决的事情,做一下汇总:

事务隔离级别 脏读 不可重复读 幻读
读未提交 Y Y Y
读已提交 N Y Y
可重复读 N N Y
串行化 N N N

四、如何查看和设置数据库的隔离级别

  1. 查看数据库当前的事务隔离级别

    1
    select @@tx_isolation

    image-20200427173518185

  2. 修改数据库的事务隔离级别

    修改语句格式:

    1
    set [global | session] transaction isolation level [read uncommitted | read committed | repeatable read | serializable]

    session:当前session内的事务

    global:应用于之后新创建的session,已经存在的session不受影响

    1
    set session transaction isolation level read committed

    修改成功之后,我们再看一下当前的隔离级别已经被修改为RC了。

    image-20200427174522929

五、小🌰

  1. 创建一张表备用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    create table cc_isolation_test
    (
    id int auto_increment primary key,
    name varchar(30) null
    ) engine=innodb default charset=utf8
    comment '事务隔离级别测试表';

    # 插入一条数据
    insert into cc_isolation_test(name) values('cc');
  2. RU级别

    • 修改session的事务隔离级别为RU

      打开两个session窗口,将事务隔离级别均修改为RU。

      1
      2
      3
      4
      # 修改隔离级别为RU
      set session transaction isolation level read uncommitted ;
      # 验证
      select @@tx_isolation;

      image-20200427184245389

    • 脏读验证

      1. 在两个窗口中均开启一个事务,在A事务中进行查询操作,在B事务中进行更新操作但不提交

      2. A事务:进行查询操作

        1
        2
        start transaction;
        select * from cc_isolation_test;

        这时查到的数据为正常数据:

        image-20200427185221292
      3. B事务:进行更新操作但不提交

        1
        2
        start transaction;
        update cc_isolation_test set name='cc1' where id=1;

        执行完之后可以看一下我们的表中,数据是未被修改的,因为B事务尚未提交

        image-20200427190836436
      4. A事务:再次进行查询操作

        1
        select * from cc_isolation_test;
        image-20200427190607659

        查询出来的数据中,name竟然变成了cc1,也就是说A事务中读取到了B事务中尚未提交的数据,如果此时B事务回滚,A事务中name的值仍然是读到的cc1,也就出现了脏数据,所以RU级别下会出现脏读的问题。

      5. 图解

        事务A 事务B
        start transaction; start transaction;
        select * from cc_isolation_test;
        update cc_isolation_test set name=‘cc1’ where id=1;
        select * from cc_isolation_test;
        rollback;
        commit;
    • 不可重复读验证

      上面演示脏读的过程中,在A事务中对数据进行了两次读取,且两次读取到的name的值不一致,所以RU也造成了不可重复读的问题。

    • 幻读验证

      1. A事务:进行查询操作

        1
        2
        start transaction;
        select * from cc_isolation_test;
        image-20200427185221292
      2. B事务:进行插入操作但不提交

        1
        2
        start transaction;
        insert into cc_isolation_test(name) values('cc1');

        事务未提交,我们的表中还没出现插入的新数据

        image-20200427190836436
      3. A事务:再次进行查询操作

        1
        select * from cc_isolation_test;

        查询出来两条数据,和之前查询的条数不一样,但是我们数据库中仅仅只有一条数据,这就是所谓的幻读,此时若将B事务回滚掉,A事务拿着B事务未提交的数据继续操作,定会出现问题。

      4. 图解

        事务A 事务B
        start transaction; start transaction;
        select * from cc_isolation_test;
        insert into cc_isolation_test(name) values(‘cc1’);
        select * from cc_isolation_test;
        rollback;
        commit;
  3. RC级别

    • 修改session的事务隔离级别为RC

      1
      2
      3
      4
      # 修改隔离级别为RC
      set session transaction isolation level read committed ;
      # 验证
      select @@tx_isolation;
    image-20200427215936571
    • 脏读验证

      操作步骤和RU的一致,但是结果却不相同,我们在B事务中对id=1的数据进行修改但是不提交事务,在A事务中是读取不到B事务对该条数据的修改,所以RC级别不会出现脏读的问题。

    • 不可重复读验证

      1. A事务:第一次查询

        1
        2
        start transaction;
        select * from cc_isolation_test where id=1;
        image-20200427193013689
      2. B事务:修改数据并提交

        1
        2
        3
        start transaction ;
        update cc_isolation_test set name='cc1' where id=1;
        commit ;
      3. A事务:第二次查询

        1
        2
        start transaction;
        select * from cc_isolation_test where id=1;
        image-20200427193112958

        在A事务中读取到了B事务提交的数据,与第一次读取到的数据不一致,也就是说每次读取都是从数据库中读取最新的数据,这也证明了再RC级别下会出现不可重复读的问题。

      4. 图解

        事务A 事务B
        start transaction; start transaction;
        select * from cc_isolation_test where id=1;
        update cc_isolation_test set name=‘cc1’ where id=1;
        commit;
        select * from cc_isolation_test where id=1;
        commit;
    • 幻读验证

      1. A事务:第一次查询

        1
        2
        start transaction;
        select * from cc_isolation_test;
        image-20200427193423746
      2. B事务:修改数据并提交

        1
        2
        3
        start transaction ;
        insert into cc_isolation_test(name) values('cc2');
        commit ;
      3. A事务:第二次查询

        1
        select * from cc_isolation_test;
        image-20200427193523968

        两次查询的数据条数不同,在A事务中读取到了B事务新插入的数据,相对于第一次查询结果来说,出现了幻读的问题。

      4. 图解

        事务A 事务B
        start transaction; start transaction;
        select * from cc_isolation_test;
        insert into cc_isolation_test(name) values(‘cc2’);
        commit;
        select * from cc_isolation_test;
        commit;
  4. RR级别

    • 修改session的事务隔离级别为RR

      1
      2
      3
      4
      # 修改隔离级别为RC
      set session transaction isolation level read committed ;
      # 验证
      select @@tx_isolation;
      image-20200427210838009
    • 脏读验证

      操作步骤和RC的一致,但是结果却不相同,我们在B事务中对id=1的数据进行修改但是不提交事务,在A事务中是读取不到B事务对该条数据的修改,所以RR级别不会出现脏读的问题。

    • 不可重复读验证

      1. A事务:第一次查询

        1
        2
        start transaction;
        select * from cc_isolation_test where id=1;
        image-20200427193112958
      2. B事务:修改数据并提交

        1
        2
        3
        start transaction ;
        update cc_isolation_test set name='cc2' where id=1;
        commit ;
      3. A事务:第二次查询

        1
        select * from cc_isolation_test where id=1;
        image-20200427193112958

        通过两次读取之后发现在B事务提交前后读取到的数据是一致的,这样证明了RR级别是支持重复读的,nice~

      4. 图解

        事务A 事务B
        start transaction; start transaction;
        select * from cc_isolation_test where id=1;
        update cc_isolation_test set name=‘cc2’ where id=1;
        commit;
        select * from cc_isolation_test where id=1;
        commit;
    • 幻读验证

      1. A事务:进行查询

        1
        2
        start transaction;
        select * from cc_isolation_test where id=1;
      2. B事务:插入数据并提交

        1
        2
        3
        start transaction ;
        insert into cc_isolation_test(id, name) values(1, 'cc2');
        commit ;
      3. A事务:插入数据

        1
        insert into cc_isolation_test(id, name) values(1, 'cc2');

        主键冲突了,但是在A事务中确实没查询到id=1的数据,其实这个时候数据库中已经有了id=1的数据,但在A事务中却没有查询到,这就是幻读。

      4. 图解

        事务A 事务B
        start transaction; start transaction;
        select * from cc_isolation_test where id=1;
        insert into cc_isolation_test(id, name) values(1, ‘cc2’);
        commit;
        insert into cc_isolation_test(id, name) values(1, ‘cc2’);
        commit;
  5. Serializable级别

    • 修改session的事务隔离级别为Serializable

      1
      set session transaction isolation level serializable;
      image-20200427215912469
    • 操作

      先开启事务A,进行查询,但不提交;再开启事务B,然后进行插入操作,会发现操作被阻塞了,如下图中insert语句最后的时间就是等待的时间,事务B必须在事务A提交或回滚之后才能继续执行,这也就是串行化的意义:同时只能有一个事务处于执行中,其他线程都要等待。并发度最低但安全性最高。

      image-20200427215828454

      图解:

      事务A 事务B
      start transaction; start transaction;
      select * from cc_isolation_test;
      insert into cc_isolation_test(name) values(‘cc2’);
      commit;
      select * from cc_isolation_test;
      commit;