事务四大特性

  1. 数据库事务具有ACID四大特性。ACID是以下4个词的缩写:
  • 原子性:一个事物里所有的操作,要么全成功,要么全失败 。
  • 一致性:事务开始和结束后,数据库的完整性不会被破坏 。
  • 隔离性:不同事务之间的操作互不影响。
  • 持久性:事务提交后,对数据的修改是永久性的,即使系统故障也不会丢失 。

隔离级别

未提交读(READ UNCOMMITTED/RU)

  1. 当前事物读取到了其它事物修改了但未提交事物的数据,称为读未提交。又叫脏读。
执行顺序 Session 1 Session 2
1 Begin;
2 Begin;
3 Update user set name =’张三’ Where id = 1
4 Select * From user Where id = 1(读到name为张三)
5 Rollback;

如上所示:Session1先开启事务去读取Id为1的数据,读到的结果是张三。但是Session2在Session1之后开启了事物并把id=1的数据name改为 张三。如果此时Session2执行回滚操作了,那么此时Session1读取到了不存在的数据。这就叫脏读。

读已提交(READ COMMITTED/RC)

  1. 读已提交指的是在一个事物里多次读取数据产生不一样的结果。又叫不可重复读。
执行顺序 Session 1 Session 2
1 Begin;
2 Select From user Where id = 1(读到name为*张三) Begin;
3 Update user set name =’李四’ Where id = 1
4 Commit;
5 Select From user Where id = 1(读到name为*李四)

如上所示:Session1在同一个事物第一次读取id=1的数据结果为张三。此时Session2把张三改为李四并提交事物。此时Session1再去读取就读到的结果是李四。在同一个事物对同一个数据读取多次,得到的结果不一样就称为不可重读。

可重复读(REPEATABLE READ/RR)

  1. 在一个事物里对同一个数据读取多次得到结果是一样的,其它事物对当前数据的修改当前事物是不可见的,称为可重复读。
执行顺序 Session 1 Session 2
1 Begin;
2 Select From user Where id = 1(读到name为*张三) Begin;
3 Update user set name =’李四’ Where id = 1
4 Commit;
5 Select From user Where id = 1(读到name为*张三)
  1. MySQL数据库InnoDB存储引擎的默认隔离级别。

串行化(SERIALIZABLE)

  1. 所有SQL语句都顺序执行,如果当前事物里有SQL在其它事物中正在被操作,那么当前事物处于等待状态。
执行顺序 Session 1 Session 2
1 Begin;
2 Begin; Update user set name =’李四’ Where id = 1
3 Select From user Where id = 1*(等待中)
4 Commit;
5 Select From user Where id = 1(读到name为*李四)
  1. 此隔离级别安全性最高,同时性能也是最差的。
  1. //查看当前事务级别:
  2. SELECT @@tx_isolation;
  3. //设置read uncommitted级别:
  4. set session transaction isolation level read uncommitted;
  5. //设置read committed级别:
  6. set session transaction isolation level read committed;
  7. //设置repeatable read级别:
  8. set session transaction isolation level repeatable read;
  9. //设置serializable级别:
  10. set session transaction isolation level serializable;

并发版本控制MVCC

  1. MVCC全程**(Multiversion Concurrency Control)**:指的是在MySQLInnodb中的一个支持高并发的特性。只有读读之间可以并发,读写,写读,写写都要阻塞。但是在引入版本之后,只有写写直接会进行阻塞,其它都可以并行这样大幅度提高了InnoDB的并发度。在内部实现中,InnoDB通过undo log保存每条数据的多个版本,并且能够找回数据历史版本提供给用户读,每个事务读到的数据版本可能是不一样的。在同一个事务中,用户只能看到该事务创建快照之前已经提交的修改和该事务本身做的修改。
  2. MVCC **Read Committed(读已提交)**和 **Repeatable Read(可重复读)**两个隔离级别下工作。
  3. MySQLInnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 **"行级锁+MVCC"**一起实现的,正常读的时候不加锁,的时候加锁。而 MVCC 的实现依赖于:
  • Undo log
    回滚日志,其存储的是事务旧版本的日志记录,当一个事物读取数据时,如果该数据对当前事物不可见,那么就会顺着这个日志链路往上找,直至找到对当前事物可见的行。而Undo log又分为两类:
    Inser undo log:事务对insert新记录时产生的undo log, 只在事务回滚时需要, 并且在事务提交后就可以立即丢弃。
    update undo log:事务对记录进行delete和update操作时产生的undo log,不仅在事务回滚时需要,快照读也需要,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。

    Purge线程:为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下旧记录的deleted_bit,并不真正将旧记录删除。
    为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。

  • Read View
    读视图,主要是用来做可见性判断的, 里面保存了“对本事务不可见的其他活跃事务”。其中包括几个变量
    low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID
    up_limit_id:活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id
    trx_ids:Read View创建时其他未提交的活跃事务ID列表。意思就是创建Read View时,将当前未提交事务ID记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。注意:Read View中trx_ids的活跃事务,不包括当前事务自己和已提交的事务(正在内存中)
    creator_trx_id:当前创建事务的ID,是一个递增的编号

  • 隐藏字段

在Innodb的表中,每个表都会有三个隐藏字段:

  1. **DB_TRX_ID**:表示最近一次对本记录行作修改(insert | update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除 位,将行表示为deleted。并非真正删除。
  2. **DB_ROLL_PTR**:回滚指针,指向当前记录行的undo log信息
  3. **DB_ROW_ID**:主键ID,如果当前表有指定主键,那么就不会有该字段。

记录行修改的具体流程

  1. 假设有这样一条SQL
  1. select * from user where id = 1
  2. --------------
  3. id name
  4. --------------
  5. 1 GC
  6. #现在事物A要把GC改为小安
  7. update user set name='小安' where id = 1
  1. 则执行流程如下:
  1. 事物A对该行数据加排他锁
  2. 然后把该行数据拷贝到undo log中作为旧版本
  3. 修改该行记录中name列值为小安,并且修改DB_TRX_ID=2,把回滚指针指向undo log的旧版本
  4. 提交事务,释放排他锁

MySQL事物 - 图1

  1. #现在事物B要把小安改为xiaoan
  2. update user set name='xiaoan' where id = 1

则执行流程如下:

  1. 事物B对该行数据加排他锁
  2. 然后把该行数据拷贝到undo log中作为旧版本
  3. 修改该行记录中name列值为小安,并且修改DB_TRX_ID=3,把回滚指针指向undo log的旧版本
  4. 提交事务,释放排他锁

MySQL事物 - 图2

可见性比较算法

  1. innodb中,创建一个新事务后,执行第一个select语句的时候,innodb会创建一个快照(read view)快照中会保存系统当前不应该被本事务看到的其他活跃事务id列表(即trx_ids)。当用户在这个事务中要读取某个记录行的时候,innodb会将该记录行的DB_TRX_ID与该Read View中的一些变量进行比较,判断是否满足可见性条件。
  1. 假设当前事务要读取某一个记录行,该记录行的DB_TRX_ID(即最新修改该行的事务ID)为trx_idRead View的活跃事务列表trx_ids中最早的事务IDup_limit_id,将在生成这个Read Vew时系统出现过的最大的事务ID+1记为low_limit_id(即还未分配的事务ID)。

具体的比较算法如下:

  1. 如果 trx_id < up_limit_id, 那么表明“最新修改该行的事务”在“当前事务”创建快照之前就提交了,所以该记录行的值对当前事务是可见的。跳到步骤5。
  2. 如果 trx_id >= low_limit_id, 那么表明“最新修改该行的事务”在“当前事务”创建快照之后才修改该行,所以该记录行的值对当前事务不可见。跳到步骤4。
  3. 如果 up_limit_id <= trx_id < low_limit_id, 表明“最新修改该行的事务”在“当前事务”创建快照的时候可能处于“活动状态”或者“已提交状态”;所以就要对活跃事务列表trx_ids进行查找

    • 如果在活跃事务列表trx_ids中能找到 id 为 trx_id 的事务,表明①在“当前事务”创建快照前,“该记录行的值”被“id为trx_id的事务”修改了,但没有提交;或者②在“当前事务”创建快照后,“该记录行的值”被“id为trx_id的事务”修改了(不管有无提交);这些情况下,这个记录行的值对当前事务都是不可见的,跳到步骤4;
    • 在活跃事务列表中找不到,则表明“id为trx_id的事务”在修改“该记录行的值”后,在“当前事务”创建快照前就已经提交了,所以记录行对当前事务可见,跳到步骤5。
  4. 在该记录行的 DB_ROLL_PTR 指针所指向的undo log回滚段中,取出最新的的旧事务号DB_TRX_ID, 将它赋给trx_id,然后跳到步骤1重新开始判断。
  5. 将该可见行的值返回。

当前读和快照读

  1. **快照读(snapshot read)**:普通的 select 语句(不包括 select ... lock in share mode, select ... for update)
  2. **当前读(current read)** select ... lock in share modeselect ... for updateinsertupdatedelete 语句(这些语句获取的是数据库中的最新数据
  3. 只靠 MVCC 实现RR隔离级别,可以保证可重复读,还能防止部分幻读,但并不是完全防止。
  4. 比如事务A开始后,执行普通select语句,创建了快照;之后事务B执行insert语句;然后事务A再执行普通select语句,得到的还是之前B没有insert过的数据,因为这时候A读的数据是符合快照可见性条件的数据。这就防止了**部分**幻读,此时事务A是**快照读**。
  5. 但是,如果事务A执行的不是普通select语句,而是select ... for update等语句,这时候,事务A是**当前读**,每次语句执行的时候都是获取的最新数据。也就是说,**在只有MVCC时**,A先执行 select ... where nid between 1 and 10 for update;然后事务B再执行 insert nid = 5 …;然后 A 再执行 select ... where nid between 1 and 10 for update,就会发现,多了一条B insert进去的记录。这就产生幻读了,所以单独靠MVCC并不能完全防止幻读。

RR和RC的Read View产生区别

  • 在innodb中的Repeatable Read级别, 只有事务在begin之后,执行第一条select(读操作)时, 才会创建一个快照(read view),将当前系统中活跃的其他事务记录起来;并且事务之后都是使用的这个快照,不会重新创建,直到事务结束。
  • 在innodb中的Read Committed级别, 事务在begin之后,执行每条select(读操作)语句时,快照会被重置,即会重新创建一个快照(read view)

例子:

MySQL事物 - 图3