事务的定义
应用程序将多个读写操作组合成一个逻辑单元的一种方式,即事务中的所有读写操作被视作单个操作来执行:整个事务要么成功(提交(commit))要么失败(中止(abort),回滚(rollback))。
事务不是天然存在的;它们是为了简化应用编程模型而创建的。
使用事务,应用程序可以自由地忽略某些潜在的错误情况和并发问题,数据库会替应用处理好这些问题,即安全保证(safety guarantees),包含ACID
一般流程
begin;// 开启一个事务
// DO SOMETHING
// 如 insert into a select 10,10;
COMMIT ; 提交事务
ROLLBACK ; 回滚该事务
复制
ACID
为保证事务是正确可靠的,所必须具备的四个特性
Atomicity(原子性):一个事务(transaction)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态
Consistency(一致性):在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这表示写入的数据必须完全符合所有的预设约束、触发器、级联回滚等
Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)
Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。在分布式环境中,持久性意味着数据已成功复制到一些节点,为了提供持久性保证,数据库必须等到这些写入或复制完成后,才能报告事务成功提交
事务并发可能出现的情况
Dirty Read (脏读)
一个事务读到另一个事务未提交的数据
出现脏读跟事务的隔离级别是有关系的,脏读会在事务的读未提交级别出现
上图中的2个事务,会话1事务读到的age为10,是会话2事务还未提交的数据,这就是脏读
Non-Repeatable Read(不可重复读)
一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值
不可重复读在读未提交和读提交隔离级别都可能会出现
上图的2个事务,会话1事务第一次读到的是未修改的age,第二次读到的是会话2事务修改的age10,这就是不可重复度
Phantom(幻读)
一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来
幻读在读未提交、读提交、可重复读隔离级别都可能会出现
上图的2个事务,会话1事务第一次读到未修改的数据,第二次读到了会话2事务插入的新数据,这就是幻读
隔离级别
READ UNCOMMITTED(读未提交)
该隔离级别的事务会读到其它未提交事务的数据。如果一个事务已经开始写数据,则另外一个事务则不允许同时进行写操作,但允许其他事务读此行数据
READ COMMITTED(读提交)
一个事务可以读取另一个已提交的事务,但是多次读取会造成不一样的结果,即不可重复读问题,但不允许脏读取。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行
REPEATABLE READ(可重复读)
该隔离级别是 MySQL 默认的隔离级别,在同一个事务里, select 的结果是事务开始时时间点的状态,因此,同样的 select 操作读到的结果会是一致的,但是有时可能出现幻读数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务
SERIALIZABLE(序列化)
提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行
事务隔离级别和可能出现的并发问题
事务实现
对数据库ACID的阐述并不特指MySQL,而是从SQL规范的维度进行描述的。在MySQL中,事务实际上是存储引擎层面的概念,大部分场景下我们都是使用InnoDB引擎,因此分析它的实现
对于InnoDB来说,事务ACID特性的实现方式如下
MVCC
当前读与快照读
1. 当前读
像lock in share mode(共享锁);for update, update, insert ,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
2. 快照读
像不加锁的select操作就是快照读,即不加锁的非阻塞读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
说白了MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作
MVCC实现
1.隐藏列
在Innodb引擎中,每个数据表都会有两个隐藏列(其实准确来说是三个,还有一个叫隐藏id,因为innodb必须要有主键,如果建表时没有显式指定的话,就会生成这个隐藏id作为主键,当然这个隐藏id和mvcc没有关系,真正和mvcc有关系的其实是两个隐藏列),分别是trx_id(创建版本号)和roll_pointer(回滚指针)。其中创建版本号其实就是创建该行数据的事务id。这些隐藏列对我们客户端来说是不可见的。
2.undo log
当事务对数据行进行一次更新操作时,会把旧数据行记录在一个叫做undo log的记录中,然后将原来数据行中的回滚指针指向undo log记录的这行数据。然后再在原来数据表中进行一次更新操作,如果这次更新操作回滚了,那么就可以根据回滚指针去undo log中查找之前的数据进行复原。如果后续还有更新操作的话,就会在undo log中和之前的数据行形成一条链表,链表头就是最新的数据,这条链表就叫做版本链
3.ReadView
当进行查询操作时,事务会生成一个ReadView(一致性视图),ReadView是一个事务快照,准确来说是当前时间点系统内活跃的事务列表,也就是说系统内所有未提交的事务,都会记录在这个Readview内,事务就根据它来判断哪些数据是可见的,哪些是不可见的:
trx_id大于事务id,说明该版本由之后启动的事务生成,不可见
trx_id等于事务id,说明该版本由事务本身创建,可见
trx_id小于事务id
trx_id在ReadView的事务列表中,说明该版本在启动时还未提交,不可见
trx_id不在ReadView的事务列表中,说明该版本在启动时已提交,可见
读已提交和可重复读的实现
实际上就是生成ReadView的时机不同
对读已提交来说,事务中的每次读操作都会生成一个新的ReadView,也就是说,如果这期间某个事务提交了,那么它就会从ReadView中移除。这样确保事务每次读操作都能读到相对比较新的数据
而对可重复读来说,事务只有在第一次进行读操作时才会生成一个ReadView,后续的读操作都会重复使用这个ReadView。也就是说,如果在此期间有其他事务提交了,那么对于可重复读来说也是不可见的,因为对它来说,事务活跃状态在第一次进行读操作时就已经确定下来,后面不会修改了
幻读的解决
前面提到在可重复读的场景下,可能会出现幻读,实际上在 Innodb 中并不会出现,Innodb 已经在可重复读隔离级别下解决了幻读的问题
解决幻读也是通过锁,叫做间隙锁(Gap Lock),间隙锁阻塞在一个区间内插入数据的操作,间隙锁之间不会有冲突关系 。Innodb 把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key锁
间隙锁中会出现死锁的情况
间隙锁是在可重复读隔离级别下才会生效的。所以,如果把隔离级别设置为读提交的话,就没有间隙锁了。但同时,要解决可能出现的数据和日志不一致问题,需要把 binlog 格式设置为 row