谈谈数据库事务

什么是事务

事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。

概述

当事务被提交给了DBMS(数据库管理系统),则DBMS需要保证该事务的所有操作成功完成,并且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则数据库中的所有操作都需要回滚,回到事务之前的状态,同时该事务对数据库其他事务的执行无影响,所有事务好像都在独立的运行。

事务的ACID特性

Atomicity 原子性

1
事务作为一个整体被执行,要么全部执行,要么全部不执行(由undo-log保证,事务某处发生错误的时候通过undo-log进行回滚,保证了原子性)

Consistency 一致性()

1
事务应确保数据库的状态从一个一致状态转变为另一个一致状态,一致状态的含义是数据库内的数据应满足完整性约束.

Isolation 隔离性

1
多个事务并发执行时,一个事务的执行应该不影响其他事务的执行。(由MVCC||加锁保证)

Durability 持久性

1
已经提交的事务对数据库的修改应该永久保存在数据库当中。(由redo-log保证,数据库保存数据的时候会先写入)

事务的隔离级别

READ-UNCOMMITED(未提交读)

该隔离级别下的事务会读到其他未提交的事务的数据,这种现象也被称之为脏读

READ-COMMITED(提交读)

一个事务可以读取另一个已经提交的事务,多次读取会产生不一样的结果,此现象被称为不可重复读

REPEATABLE-READ(可重复读)

同一个事务内,同样的查询语句多次执行所获取的结果是一致的,但是可能会出现幻读现象

SERIALIZABLE(序列化)

该隔离级别下,所有事务都是串行执行的。此级别是最高隔离级别,同时性能也是最差的

事务的并发带来的问题

脏读

当一个事务正在访问数据,并且对数据进行了修改,但是该事务还未提交,这时另一个事务访问到了该事务中未提交的数据,产生了脏读

时间 事务1 事务2
t1 开启事务 开启事务
t2 update table set property = ‘newValue’ where id = 1;
t3 select * from table where id = 1
t4 commit commit

假设事务2未更新前是’oldValue’,事务2更改后修改还没有提交,事务1读到了事务2未提交的’newValue’,这时,事务2进行了回滚,导致事务1拿到的数据变为无效数据,因为事务2对该数据的修改还是未提交的,那么事务1此时读到的数据就是脏数据,对脏数据进行的操作可能是不正确的。

幻读

多次读取一定范围内的记录,发现结果不一致(一般指记录增多或记录减少,下述操作在mysql8.0.0版本未能出现,mysql innodb 引擎部分解决了幻读问题,可以尝试在其他数据库执行该操作)

时间 事务1 事务2
t1 开启事务 开启事务
t2 查询所有年龄为20的person,结果为20
t3 新增一个年龄为20的person并commit
t4 查询所有年龄为20的person,结果为21
t5 commit commit

假设事务2插入前,年龄为20的人有20条记录, 事务1第一次查询的时候发现有20个年龄为20的人,第二次查询却发现年龄为20的人多了一个,好像产生了幻觉

不可重复读

多次读取同一条记录,发现该记录列值被修改过

时间 事务1 事务2
t1 开启事务 开启事务
t2 查询张三的年龄,结果为20
t3 更新张三年龄为21
t4 查询张三的年龄,结果为21
t5 commit commit

假设张三原来的年龄为20,发现张三第一次查询的结果20和第二次查询的结果21不一致,这就是不可重复读

各事务隔离级别对应解决的问题

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITED
READ-COMMITED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×

mysql的InnoDB存储引擎解决了RR模式下部分幻读的问题,但是并不能完全避免幻读,解释见下文快照读和当前读部分
github上面有关于这个问题的讨论


并发事务解决方案

锁机制

解决-冲突问题,效率很差

  • 悲观锁 依靠数据库提供的锁机制实现 当一个线程需要对共享资源进行操作的时候,首先对其进行加锁,当该线程持有该资源的时候,其他线程对该资源操作的时候就会阻塞

  • 乐观锁 基于版本号实现
    当一个线程需要操作共享资源的时候,不进行加锁,而是在操作之后,通过一个版本号进行控制,如果在该线程执行操作完成后通过版本号进行判断,如果发现已经被其他线程操作了,则通知该线程操作失败。

MVCC(多版本并发控制)

解决读写冲突问题,通过一种快照机制生成一个数据请求时间节点的数据快照,通过这个快照来进行数据的读取,这样,因为不需要加锁,读写操作都不会相互阻塞(仅作用于RR和RC)。

Mysql InnoDB 引擎在每一行数据都都加了几个隐藏列(参考自mysql官方文档)

1
2
3
4
5
6
7
Internally, InnoDB adds three fields to each row stored in the database. A 6-byte  DB_TRX_ID field indicates the transaction identifier for the last transaction that inserted or updated the row.  
Also, a deletion is treated internally as an
update where a special bit in the row is set to mark it as deleted.
Each row also contains a 7-byte DB_ROLL_PTR field called the roll pointer. The roll pointer points to an undo log record written to the rollback segment. If the row was updated, the
undo log record contains the information necessary to rebuild the content of the row before it was updated.
A 6-byte DB_ROW_ID field contains a row ID that increases monotonically as new rows are inserted.
If InnoDB generates a clustered index automatically, the index contains row ID values. Otherwise, the DB_ROW_ID column does not appear in any index.
  • DB_TRX_ID 6字节的事务id,代表最近一次插入或更新该行记录的事务id
    在inno db存储引擎下,delete操作被视为update操作,做法就是在记录行中有一个特殊的标记位标记该行是否被删除,而并非真删除

  • DB_ROLL_PTR:7字节回滚指针,每次对一行数据进行修改的时候,会生成一条undo log,保留数据修改前的状态,回滚指针即指向该undo log记录

  • DB_ROW_ID: 隐藏的自增id,当建表时候没有指定主键的时候,就会使用该rowId创建聚簇索引,否则聚簇索引中将不包含该DB_ROW_ID内容

read-view(又称快照snapshot)

事务的快照主要是用来存储数据库的事务运行情况,通常用来进行可见性判断。

  • 查看当前所有未提交的活跃事务,存储在数组中记作m_ids
    RR是在第一次快照读操作前生成readView,之后的查询操作复用该readView
    RC是在每一次快照读操作前都生成一个readView

可见性比较算法

  • 如果 DB_TRX_ID < m_ids中最小事务id,则代表被访问的数据版本在生成快照之前就已经提交,当前事务可以获得该条记录的稳定版本

  • 如果 DB_TRX_ID < m_ids中最大事务id,则代表生成该事务的版本在生成快照之后,所以该版本对当前事务是不可见的

  • 如果DB_TRX_ID在最大事务id和最小事务id之间,则需要判断下DB_TRX_ID是否在m_ids当中,如果存在,则说明事务还是活跃的,该版本数据不可访问,如果不存在,说明该事务在生成快照之后已经提交,该数据版本可以被该事务访问

当前读和快照读

  • 快照读 普通的select操作(不包含 select … for update, select … lock in share mode)
  • 当前读
    • select … in share mode
    • select … for update
    • insert
    • update
    • delete
时间 事务1 事务2
t1 开启事务 开启事务
t2 查询年龄为20的人,结果为20
t3 新增一个人李四,id为31
t4 commit
t5 更新李四的年龄为31,可以更新成功,此时再次查询年龄为20的人,结果为21,出现幻读
t6 commit

inno db引擎只能部分避免幻读,这个部分指的是只能避免当前读的幻读,而对于上表中快照读的情况却不能避免

版本链

每次对记录进行改动,都会记录一条undo-log,每条undo-log上面也会有一个roll-ptr属性,指向历史版本的undo-log,可以将这些undo-log串联起来,形成一个版本链,如下图(图片引用自参考文章中《MySQL事务隔离级别和MVCC》
)

MVCC总结

所谓的MVCC,就是指RR,RC这两个事务隔离级别的事务在执行快照读操作时访问版本链的过程,使得不同事务的读-读,读-写操作并发执行,从而提升系统性能

参考文章

MySQL-InnoDB-MVCC多版本并发控制

深入学习MySQL事务:ACID特性的实现原理

mysql官方文档

MySQL事务隔离级别和MVCC