1.什么是事务
事务是一个最小的,不可再分的工作单元,也就是在一个事务中的操作要么同时成功,要么同时失败
2.事务的基本特性(ACID)
(1)原子性 A (Atomicity) :一个事务是一个不可分割的工作单位,一个事务中的所有操作,要么同时成功,要么同时失败,不可能成功一半,失败一半。当事务在执行过程中如果失败的话,会进行回滚操作
(2)一致性 C (Consistency) :事务在执行完后的结果是和预期数据一致的,符合逻辑的;其他三个特性都是为了保证一致性的成立
(3)隔离性 I(Isolation) :事务与事务之间是相互隔离的,互不影响的
(4)持久性 D(Durability) :在事务成功完成之后,会立即将更改的数据写入数据库中,实现持久性的操作,接下来发生的错误都无法改变这一结果
3.事务特性(ACID)的实现与保证
(1)原子性的实现与保证
原子性的实现的关键是undo log,即回滚日志,在这个日志里面记录了一些事务需要回滚的信息,也就是一些反向执行的sql语句
例如:当我们要delete一条语句时,undo log里记录这一行的数据的一个insert语句,在事务失败需要回滚的时候会执行这个insert语句,其他的修改语句同理
(2)一致性的实现与保证
一致性的保证要从两个层面来说
在数据库层面,一致性其实是由其他三大特性来保证的,一致性是目的,其他三大特性是手段。也就是说,只要其他三大特性得到保证,那么数据库层面的一致性就能得到保证,那三大特性只要有一个没有保证,那么一致性就得不到保证
在业务代码层面,如果你的代码不符合日常的逻辑或者没有一些约束,那么数据的一致性也无法得到保证
例如:当A向B转账100元,但是你的代码只写了A的账户 -100 元,而B的账户 +50元或者不给B的账户加钱,那这就是不符合日常逻辑的代码,自然保证不了一致性
还有当A向B转账100元,但A的账户只有50元,那么就不应该转账成功,这个时候就需要程序员对数据做一些约束
(3)隔离性的实现与保证
1.事务的隔离级别
(1)未提交读(Read uncommitted):表示一个事务可以读到另一个事务还没有提交的数据,可能发生脏读、不可重复读等。
(2)已提交读(Read committed):也叫不可重复读,表示一个事务只能等另一个事务提交才能读到它的数据。当一个事务读取一个数据后,其他事务对这个数据进行操作修改,之后这个事务再去读取这个数据的话,前后两次读取到的数据不一致,也就是不可重复读
(3)可重复读(Repeatable read)(默认级别):事务两次读取到的结果一致,在这个隔离级别下,MySQL的幻读其实是可以防止的,因为MySQL里有间隙锁,它会把这个范围锁起来,其他事务不可以修改,这就防止了幻读的发生
(4)可串行化(Serializable):在这个隔离级别下,事务是按顺序,串行化执行的,这样虽然可以防止脏读,幻读,不可重复读的问题发生,但是它的效率是非常低的,不建议使用
2.隔离性的保证
隔离性的实现与保证其实是通过锁和MVCC来实现的
(1)什么是MVCC?
MVCC(Multi Version Concurrency Control):多版本并发控制,是指在数据库中为了实现高并发的数据访问,对数据进行多版本控制,并通过事务的可见性来保证事务能看到自己应该看到的数据版本
作用是当某条记录正在被修改,则可以并发读取该数据的历史版本,而不必阻塞等待锁的释放。其实现是使用undo log 和ReadView实现的,可以读取undo log里的历史版本,ReadView则用来控制哪个版本是对当前事务可见的
在MVCC的并发控制中,读操作可以分为两类:快照读(snapshot read)与当前读(current read)
快照读:其实就是普通读,读取的记录的可见版本,不用加锁。(select)
当前读:读取的是记录的最新版本,并且当前读返回的记录都会加上锁,保证其他的事务不会再并发修改这条记录。(insert、update、delete、select for update)
所以MySQL为什么写不阻塞读?
答案其实是因为MySQL利用MVCC的方式来读历史版本
ReadView就是事务进行快照读操作的时候产生的读视图(Read View),在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;在RR的隔离级别下,只有同一个事务中的第一次快照读才会才会创建Read View,之后的快照读获取的是同一个Read View,所以在RR隔离级别下能读取到历史版本其实是因为它产生的是同一个ReadView
(4)持久性的实现与保证
持久性是由WAL(write ahead log 预写log) + redo log保证的,redo log有两部分组成,redo log Buffer(在内存中)与redo log file(在磁盘中)。
Buffer Pool是在内存里,存的有一些脏页数据,也就是当我们进行一些修改的操作,例如update,insert等操作时,在内存里的数据和在磁盘上的数据不一致,所以叫脏页数据。那正常来说,我们在执行完上述这些操作后,执行commit,数据就直接写入磁盘更新数据不就是行了吗?但是这样的话会有一些问题;那就是频繁的进行IO操作,性能就会降低
所以MySql的操作就是先存到redo log Buffer(内存)里,相当于做一个缓冲,当redo log Buffer里打上一个commit标志的时候,然后才会写入redo log file(磁盘)中,当数据库宕机,数据丢失的时候,会从redo log中恢复。
(1)那么真正的数据是在什么时候写入磁盘呢?其实是依据checkpoint择时刷新脏页,也就是写入磁盘,这个机制叫Double Write(双写机制)
redo log落盘机制(写入磁盘):
InnoDB提供了三种落盘机制,通过配置一个
innodb_flush_log_at_trx_commit 的参数来控制
配置值 | 描述 |
---|---|
0 | 表示当事务提交时,由主线程写入redo log Buffer里,之后再由另外的线程每隔固定的时间从redo log Buffer刷新到磁盘。 |
1 | 默认参数,表示事务提交时,立即会通过主线程写入缓存并刷新到磁盘。 |
2 | 表示当事务提交时,有主线程写入OS Buffer(操作系统缓存),之后再由其他线程每隔固定的时间刷新到磁盘。 |
当设置为0时,性能最高,但是会丧失事务的一致性
(2)那么当我们还没有打上commit标志时,数据库就挂了,该如何回滚呢?
如下图,事务为了回滚(RollBack),会在undo log里面会记录一些原始值,除了有一些基本数据,还会有RowID,事务ID和回滚指针三个值
当我们再进行一次事务操作时,更新了数据,那么它的事务ID就会+1,回滚指针就会指向前面的一条记录,此时若是commit没有成功,那么它就会回滚到上一条记录
如果commit成功后,此时再来一次事务的话,它的事务ID继续+1,回滚指针依然会指向上一条记录