什么是事务?
事务是逻辑上的一组操作,要么都执行,要么都不执行。
事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
事务的四大特性(ACID)
- 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 一致性(Consistency): 执行事务后,数据库从一个正确的状态变化到另一个正确的状态;
- 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
- 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
并发事务带来哪些问题?
在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
- 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
- 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
- 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
- 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
不可重复读和幻读区别:
不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。
事务隔离级别有哪些?MySQL的默认隔离级别是?
SQL 标准定义了四个隔离级别:
- READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、丢失修改、不可重复读、幻读。
- READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生。
- REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。
- SERIALIZABLE(可串行化): 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 |
|---|---|---|---|
| READ-UNCOMMITTED | √ | √ | √ |
| READ-COMMITTED | × | √ | √ |
| REPEATABLE-READ | × | × | √ |
| SERIALIZABLE | × | × | × |
MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重复读)。
InnoDB 存储引擎在 分布式事务 的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。
事务的实现
redo log(持久性)
redo log叫做重做日志,用来实现事务的持久性。它记录的是数据被修改后的信息。该日志文件由两部分组成:重做日志缓冲区和重做日志文件,前者存在于内存中,后者存在于磁盘中。当事务提交后会把所有的修改信息都提交到该日志中。
Mysql为了提升性能,不会把每次的修改都实时同步到磁盘,而是存放到缓存池中,把这个当做缓存来用。然后使用后台线程去做缓存池和磁盘之间的同步。如果缓存池和磁盘之间还没来得及同步,就发生了宕机或者断电,则使用redo log。每次执行完一条语句,都会生成一个重做日志文件,这个重做日志文件会被放到重做日志缓冲区,然后重做日志缓冲区会将这些日志文件持久化到磁盘。系统重启后根据读取的redo log恢复最新数据。
undo log(原子性)
undo log叫做回滚日志,用于记录数据修改前的信息。undo log主要记录数据的逻辑编号,为了在发生
错误时回滚之前的操作,需要将之前的操作记录下来,才可以回滚。
undo log 记录事务修改之前版本的数据信息,因此假如由于系统错误或者rollback操作而回滚的话可以
根据undo log的信息来进行回滚到没被修改前的状态。undo log实现了事务的原子性。
锁技术(隔离性)
当有多个读请求来读取表中数据,并且也有写请求来修改表中的数据,就必须采取一种措施开控制并
发,不然就可能造成不一致。
使用读写锁的组合,就可以实现事务的隔离性。
MVCC(多版本并发控制)(隔离性)
对于使用InnoDB的存储引擎来说,它的聚簇索引记录中都包含两个必要的隐藏列:
- 事务id(trx_id:) 每次对某条聚簇索引记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列
- 回滚指针(roll_pointer): 每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
每次对记录进行改动,都会记录一条undo日志。每条undo日志也都有一个roll_pointer属性,这些undo日志串成一个链表,称之为版本链。版本链的头节点是当前记录最新的值
对于未提交读的隔离级别,直接读取记录的最新版本就好了,对于串行化的隔离级别,使用加锁的方式
来访问记录。对于提交读和可重复读的隔离级别,就需要用到版本链。
读视图(ReadView):包含当前系统中的活跃读写事务,把它们的事务ID放到一个列表中,这个列表成为m_ids。
在访问某条记录时,根据以下规则判断记录的某个版本是否可见:
- 如果被访问版本的trx_id属性值小于m_ids列表中的最小事务id,表明生成该版本的事务在生成ReadView前已经提交,所以该版本可以被当前事务访问
- 如果被访问版本的trx_id大于m_ids中的最大事务id,表明该版本的事务在生成ReadView后才生成,所以该版本不可以被当前事务访问,需要根据版本链找到之前的版本,然后继续判断可见性
- 如果被访问版本的trx_id属性值在m_ids列表中最大的事务id和最小事务id之间,那就需要判断一下
trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
如果某个版本的数据对当前事务不可见,就顺着版本链找到下一个版本的数据,直到满足上述条件。如果最后一个版本也不可见,就意味着该条记录对该事务不可见,查询结果不包含该记录。
对于提交读,每次读取数据前都生成一个ReadView。
对于可重复读,在第一次记读取数据时生成一个ReadView,之后的读操作都重复这个ReadView。
总结
- 原子性:undo log
- 持久性:redo log
- 隔离性:读写锁+mvcc
- 一致性:原子性+持久性+隔离性
参考文献:
正确的理解MySQL的MVCC及实现原理

