事务定义
事务:一个最小的不可再分的工作单元;一个事务通常对应一个完整的业务,例如银行账户转账业务,该业务就是一个最小的工作单元
一个完整的业务需要一组的DML( insert、update、delete)语句共同联合完成
事务只和DML语句有关,或者说DML语句才有事务。这个和业务逻辑有关,业务逻辑不同,DML语句的个数不同
事务特性
一个事务都必须包含四条基本特性,这四条特性一般称为ACID
- (Atomicity)原子性: 事务是最小的执行单位,不允许分割。原子性确保动作要么全部成功,要么完全全失败;
- (Consistency)一致性: 执行事务前后,数据保持一致;
- (Isolation)隔离性: 并发访问数据库时,一个事务不被其他事务所干扰。
- (Durability)持久性: 一个事务被提交之后。对数据库中数据的改变是持久的,即使数据库发生故障。
接下来就介绍下,MySQL在innoDB引擎下是事务特性的
隔离性
为什么要先说隔离性呢?这是因为隔离性是事务最基础的特性,会涉及隔离级别、锁、MVCC等多个概念。
不同的隔离级别是为了解决不同的问题。也就是脏读、幻读、不可重复读。
- 脏读: 脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
- 不可重复读:是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。
- 幻读:第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样,幻读是数据行记录变多了或者少了。
MySQL不同的隔离级别,可能存在的问题如下表
那么不同的隔离级别是怎么保证隔离性呢?答案是 锁 和 MVCC。
MySQL中的锁从粒度上来说分为表锁、页锁、行锁。
表锁有意向共享锁(IS)、意向排他锁(IX)、自增锁等。
行锁的种类共享锁(S)、共享锁 (X),行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。
三种行锁算法
- Record Lock记录锁:单个行记录上的锁。
- Gap Lock间隙锁,间隙锁就会对记录之间的间隙加锁,防止数据插入。就是我们在使用实时读(SELECT FOR … UPDATE)或者更新,为了防止读的过程中有新的数据插入,会对我们读的数据的左右区间进行加锁,防止其他事务插入数据,所以间隙锁之间是不排斥的,间隙锁排斥的只是插入数据的操作。
3.Next-Key Lock临键锁,会锁记录以及记录之间的间隙,就是 record lock 和 gap lock的组合,就是会对索引记录加记录锁 + 索引记录前面间隙上的锁”,就是对要更新的数据的左右两个端点加间隙锁,
锁和MVCC
大致介绍了下锁,可以看到。有了锁,当某事务正在写数据时,其他事务获取不到写锁,就无法写数据,一定程度上保证了事务间的隔离。但前面说,加了写锁,为什么其他事务也能读数据呢,不是获取不到读锁吗?这就是依靠MVCC(Multi-Version Concurrency Control)多版本的并发控制实现的。
Innodb 在存储每一行数据有一些额外的字段:DATA_TRX_ID和DATA_ROLL_PTR。
- DATA_TRX_ID:数据行版本号。用来标识最近对本行记录做修改的事务 id。
- DATA_ROLL_PTR:指向该行回滚段的指针。该行记录上所有旧版本,在 undo log 中都通过链表的形式组织。
ReadView
在每一条 SQL 开始的时候被创建,有几个重要属性:
- trx_ids: 当前系统活跃(未提交)事务版本号集合。
- low_limit_id: 创建当前 read view 时“当前系统最大事务版本号+1”。
- up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”
- creator_trx_id: 创建当前read view的事务版本号;
此时查询
- DATA_TRX_ID <up_limit_id :说明数据在当前事务之前就存在了,显示。
- DATA_TRX_ID >= low_limit_id:说明该数据是在当前read view 创建后才产生的,数据不显示。
- 根据 DATA_ROLL_PTR 从 undo log 中找到历史版本,找不到就空。
- up_limit_id <DATA_TRX_ID <low_limit_id :就要看隔离级别了。
RR 级别的幻读
有了锁和 MVCC , 事务的隔离性得到解决。这里要引申一下,默认的 RR 的级别,解决了幻读吗?幻读通常针对的是 INSERT, 不可重复度则针对 UPDATE 。
快照读
普通的SELECT语句都是普通读,也就是读取的数据都是事务开始时那个状态的数据,普通读的幻读问题主要是通过MVCC来解决的,具体可以看上面的MVCC中的查询操作。
实时读
SELECT *** FOR UPDATE 在查询时会先申请X锁
SELECT *** LOCK IN SHARE MODE 在查询时会先申请S锁
就是实时读,就是读取的是实时的数据,而不快照数据,读的时候会加Next-Key Lock锁住当前的记录,以及左右两个区间的间隙,这样在读的时候就不能往我们的查询范围插入数据了。
原子性
前面有提到 undo log 回滚日志。隔离性的MVCC其实就是依靠它来实现的,原子性也是。实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。
当事务对数据库进行修改时,InnoDB会生成对应的 undo log;如果事务执行失败或调用了 rollback,导致事务需要回滚,便可以利用 undo log 中的信息将数据回滚到修改之前。undo log 属于逻辑日志,记录的是sql执行相关的信息。当发生回滚时,InnoDB 会根据 undo log 的内容做与之前相反的工作:
- 对于每个 insert,回滚时会执行 delete;
- 对于每个 delete,回滚时会执行insert;
- 对于每个 update,回滚时会执行一个相反的 update,把数据改回去。
以update操作为例:当事务执行update时,其生成的undo log中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到update之前的状态。
持久性
持久性依靠的是 redo log。MySQL 里经常说到的 WAL(Write-Ahead Logging) 技术 ,它的关键点就是先写日志,再写磁盘。
在做数据更新操作时,先将对数据页的更改记录到redo log,然后再去更新内存中的数据页,在下次查询数据页或者空闲时间,将操作记录更新到磁盘。这样可以将随机I/O改为顺序I/O。
优点是减少磁盘I/O次数,即便发生故障也可以根据redo log来将数据恢复到最新状态。
缺点是会造成内存脏页,后台线程会自动对脏页刷盘,或者是淘汰数据页时刷盘,此时会暂时查询操作,影响查询。
redo log 有两个特点:
- 大小固定,循环写
- crash-safe
对于redo log 是有两阶段的:commit 和 prepare 如果不使用“两阶段提交”,数据库的状态就有可能和用它的日志恢复出来的库的状态不一致.
两段提交制是什么?
更新时,先改内存中的数据页,将更新操作写入redo log日志,此时redo log进入prepare状态,然后通知MySQL Server执行完了,随时可以提交,MySQL Server将更新的SQL写入bin log,然后调用innodb接口将redo log设置为提交状态,更新完成。
可能你会疑问还有个 binlog 也是写操作并用于数据的恢复,有啥区别呢。
- 层次:redo log 是 innoDB 引擎特有的,server 层的叫 binlog(归档日志)
- 内容:redolog 是物理日志,记录“在某个数据页上做了什么修改”;binlog 是逻辑日志,是语句的原始逻辑,如“给 ID=2 这一行的 c 字段加 1 ”
- 写入:redolog 循环写且写入时机较多,binlog 追加且在事务提交时写入
对于语句 update T set c=c+1 where ID=2;
- [执行器先找引擎取 ID=2 这一行。ID 是主键,直接用树搜索找到。如果 ID = 2 这一行所在数据页就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,再返回。]
- [执行器拿到引擎给的行数据,把这个值加上 1,N+1,得到新的一行数据,再调用引擎接口写入这行新数据。]
- [引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。]
- [执行器生成这个操作的 binlog,并把 binlog 写入磁盘。]
- [执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成]
崩溃恢复时的判断规则(以redolog是否commit或者binlog是否完整来确定)
- 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交;
- 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整: a. 如果是,则提交事务; b. 否则,回滚事务。
一致性
一致性是事务追求的最终目标,前面提到的原子性、持久性和隔离性,其实都是为了保证数据库状态的一致性。当然,上文都是数据库层面的保障,一致性的实现也需要应用层面进行保障。
也就是你的业务,比如购买操作只扣除用户的余额,不减库存,肯定无法保证状态的一致。
总结
MySQL事务应该大家都知道,但是实现原理可能就不是那么清楚,希望本文能对事务的了解有所帮助。
今天多学一点知识,明天就少说一句求人的话