事务就是一组原子性的 SQL 查询,或者说一个独立的工作单元,如果其中有任何一条语句因为崩溃或其他原因无法执行,那么所有的语句都不会执行。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败;
一个良好的事务处理系统,必须具备 ACID 特性:
atomicity(原子性) :要么全执行,要么全都不执行;
consistency(一致性):在事务开始和完成时,数据都必须保持一致状态;
isolation(隔离性) :事务处理过程中的中间状态对外部是不可见的;
durability(持久性) :事务完成之后,它对于数据的修改是永久性的。
对于mysql数据库而言,只有InnoDB存储引擎支持事务,所以我们重点介绍InnoDB的事务;
一、 redo log 机制
InnoDB 采用 redo log 机制来保证事务更新的一致性和持久性;
Redo log 称为重做日志,用于记录事务操作变化,记录的是数据被修改之后的值;
Redo log 由两部分组成:内存中的重做日志缓冲(redo log buffer)和重做日志文件(redo log file)
每次数据更新会先更新 redo log buffer,然后根据 innodb_flush_log_at_trx_commit 来控制 redo log buffer 更新到 redo log file 的时机;
innodb_flush_log_at_trx_commit 有三个值可选:
0:事务提交时,在事务提交时,每秒触发一次 redo log buffer 写磁盘操作,并调用操作系统 fsync 刷新 IO 缓存;
1:事务提交时,InnoDB 立即将缓存中的 redo 日志写到日志文件中,并调用操作系统 fsync 刷新 IO 缓存;
2:事务提交时,InnoDB 立即将缓存中的 redo 日志写到日志文件中,但不是马上调用 fsync 刷新 IO 缓存,而是每秒只做一次磁盘 IO 缓存刷新操作。
二、Binlog
二进制日志(binlog)记录了所有的 DDL(数据定义语句)和 DML(数据操纵语句),但是不包括 select 和 show 这类操作,Binlog 有以下几个作用:
恢复:数据恢复时可以使用二进制日志
复制:通过传输二进制日志到从库,然后进行恢复,以实现主从同步
审计:可以通过二进制日志进行审计数据的变更操作
可以通过参数 sync_binlog 来控制累积多少个事务后才将二进制日志 fsync 到磁盘:
sync_binlog=0,表示每次提交事务都只write,不fsync
sync_binlog=1,表示每次提交事务都会执行fsync
sync_binlog=N(N>1),表示每次提交事务都write,累积N个事务后才fsync
只要 innodb_flush_log_at_trx_commit 和 sync_binlog 都为 1(通常称为:双一),就能确保 MySQL 机器断电重启后,数据不丢失,想要数据库达到最安全的状态,可以将 innodb_flush_log_at_trx_commit 和 sync_binlog 都设置为 1。
三、MVCC(Multi-Version Concurrency Control,多版本并发控制)
隐藏列:
-- 对于 InnoDB ,每行记录除了我们创建的字段外,其实还包含 3 个隐藏的列:
-- ROW ID:隐藏的自增 ID,如果表没有主键,InnoDB 会自动以 ROW ID 产生一个聚集索引树。
-- 事务 ID:记录最后一次修改该记录的事务 ID。
-- 回滚指针:指向这条记录的上一个版本。
Undo log:
Undo log 是逻辑日志,将数据库逻辑地恢复到原来的样子,所有修改都被逻辑的取消了。
-- 也就是如果是 insert 操作,其对应的回滚操作就是 delete;
-- 如果是 delete,则对应的回滚操作是 insert;
-- 如果是 update,则对应的回滚操作是一个反向的 update 操作
-- Undo log 的作用除了回滚操作,
-- Undo log 的另一个作用是 MVCC,InnoDB 存储引擎中 MVCC 的实现是通过 Undo log 来完成的。当用户读取一行记录时,若该记录已经被其它事务占用,当前事务可以通过 Undo log 读取之前的行版本信息
Read View
Read View 是指事务进行快照读操作的那一刻,产生数据库系统当前活跃事务列表的一个快照
-- Read View 规则帮我们判断当前版本的数据是否可见
-- 我们分析下当查询一条记录时,大致的步骤:
-- 获取事务本身的事务 ID;
-- 获取 Read View;
-- 查询得到的数据,然后与 Read View 中的事务版本号进行比较;
-- 如果能查询,则直接查询对应的记录;如果不能直接查询,则通过 Undo Log 中获取历史快照;
-- 最终返回结果。
-- 对于不可见的记录,都是通过查询 Undo log 来查询老的记录。
MVCC, 即多版本并发控制,
MVCC 的实现,是通过保存数据在某个时间点的快照来实现的,也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的
MVCC 最大的好处是读不加锁,读写不冲突,极大地增加了 MySQL 的并发性
MySQL 是通过 Read View 判断是否能直接查询到对应的记录,如果需要查询一些被其它事务正在更新的行,则要取出 Undo log 中历史版本的记录;
MVCC 实现的原理大致是:
InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 Undo log 中。如果要执行更新操作,会将原记录放入 Undo log 中,并通过隐藏的回滚指针指向 Undo log 中的原记录。其它事务此时需要查询时,就是查询 Undo log 中这行数据的最后一个历史版本;
四、事务的隔离级别
MySQL 有四种隔离级别,我们来看一下这四种隔离级别的基本定义:
-- Read uncommitted(读未提交,简称:RU): 在该隔离级别,所有事务都可以看到其它未提交的事务的执行结果。可能会出现脏读。
-- Read Committed(读已提交,简称: RC):一个事务只能看见已经提交事务所做的改变。因为同一事务的其它实例在该实例处理期间可能会有新的 commit,所以可能出现幻读。
-- Repeatable Read(可重复读,简称:RR):这是 MySQL 的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。消除了脏读、不可重复读,默认也不会出现幻读。
-- Serializable(串行):这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。
drop procedure if exists insert_t21; /* 如果存在存储过程insert_t21,则删除 */
delimiter ;;
create procedure insert_t21() /* 创建存储过程insert_t21 */
begin
drop table if exists t21;
CREATE TABLE `t21` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) NOT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_c` (`a`)
) ENGINE=InnoDB CHARSET=utf8mb4;
insert into t21(a,b) values (1,1),(2,2);
end;;
delimiter ; /* 存储过程insert_t21主要功能创建测试表 t21,并写入数据 */
1 Read uncommitted 读未提交
ID | session1 | session2 |
---|---|---|
1 | call insert_t21(); /* 运行存储过程 insert_t21 */ |
|
2 | set session transaction_isolation=‘READ-UNCOMMITTED’; | set session transaction_isolation=‘READ-UNCOMMITTED’; |
3 | begin; | begin; |
4 | select * from t21 where a=1; |
|
5 | insert into t21(a,b) values (1,3); | |
6 | select * from t21 where a=1; |
|
7 | commit; | commit; |
2 Read Committed 读已提交
ID | session1 | session2 |
---|---|---|
1 | call insert_t21 (); /* 运行存储过程 insert_t21 */ | |
2 | set session transaction_isolation=‘READ-COMMITTED’; | set session transaction_isolation=‘READ-COMMITTED’; |
3 | begin; | begin; |
4 | select * from t21 where a=1; |
|
5 | insert into t21(a,b) values (1,3); | |
6 | select * from t21 where a=1; |
|
7 | commit; | |
8 | select * from t21 where a=1; |
|
9 | commit; |
3 Repeatable Read 可重复读
ID | session1 | session2 | |
---|---|---|---|
1 | call insert_t21 (); /* 运行存储过程 insert_t21 */ | ||
2 | set session transaction_isolation=‘REPEATABLE-READ’; | set session transaction_isolation=‘REPEATABLE-READ’; | |
3 | begin; | begin; | |
4 | select * from t21 where a=1; |
||
5 | insert into t21(a,b) values (1,3); | ||
6 | select * from t21 where a=1; |
||
7 | commit; | ||
8 | select * from t21 where a=1; |
||
9 | commit; | ||
10 | select * from t21 where a=1; |
4 Serializable 串行
ID | session1 | session2 |
---|---|---|
1 | call insert_t21 (); /* 运行存储过程 insert_t21 */ | |
2 | set session transaction_isolation=‘SERIALIZABLE’; | set session transaction_isolation=‘SERIALIZABLE’; |
3 | begin; | begin; |
4 | select * from t21 where a=1; |
|
5 | insert into t21(a,b) values (1,3); (等待) |
|
6 | select * from t21 where a=1; |
|
7 | commit; | session1 提交后,第 5 步中的写入操作执行成功 |
8 | commit; | |
9 | select * from t21 where a=1; |
总的来说,建议在 RC 和 RR 两个隔离级别中选一种,如果能接受幻读,需要并发高点,就可以配置成 RC,如果不能接受幻读的情况,就设置成 RR 隔离级别;
五、分布式事务
分布式事务是指一个大的事务由很多小操作组成,小操作分布在不同的服务器上或者不同的应用程序上。分布式事务需要保证这些小操作要么全部成功,要么全部失败。MySQL 从 5.0.3 开始支持分布式事务;
分布式事务使用两阶段提交协议:
第一阶段:所有分支事务都开始准备,告诉事务管理器自己已经准备好了;
第二阶段:确定是 rollback 还是 commit,如果有一个节点不能提交,则所有节点都要回滚
MySQL 中分布式事务按实现方式可以分为两种:MySQL 自带的分布式事务和结合中间件实现分布式事务,
考虑到实际工作中很少用到自带的分布式事务,目前主流的分布式实现还是结合中间件实现分布式处理的,所以重点讲解中间件的分布式事务;
具体实现方式可以拿上面网上购书的例子来说:
订单业务程序处理完增加订单的操作后,将减库存操作发送到消息队列中间件中(比如:Rocketmq),订单业务程序完成提交。然后库存业务程序检查到消息队列有减对应商品库存的信息,就开始执行减库存操作。库存业务执行完减库存操作,再发送一条消息给消息队列中间件:内容是已经减掉库存;