内容
- 事务
- 事务基本知识
- 事务特性如何实现的?
- 快照读和当前读
- 分布式事务
- XA规范
- mysql基于XA实现的分布式事务
- mysql如何保证数据不丢失?
一 事务
1.1 事务基本知识
- 事务的特性:原子性、一致性、隔离性、持久性
- 并发事务可能的问题:脏读、不可重复读、幻读
- 事务隔离级别:读未提交、读提交、可重复读、串行化
读未提交:一个事务还未提交,它所做的变更就可以被别的事务看到;
读提交:一个事务提交之后,它所做的变更才可以被别的事务看到;
可重复读:一个事务执行过程中看到的数据是一致的。未提交的更改对其他事务是不可见的;
串行化:对应一个记录会加读写锁,出现冲突的时候,后访问的事务必须等前一个事务执行完成才能继 - 事务隔离性实现:每条记录在更新的时候都会同时记录一条回滚操作。同一条记录在系统中可以存在多个版本,这就是数据库的多版本并发控制(MVCC)。
- 回滚日志什么时候删除?系统会判断当没有事务需要用到这些回滚日志的时候,回滚日志会被删除。
什么时候不需要了?当系统里么有比这个回滚日志更早的read-view的时候。 - 为什么尽量不要使用长事务。长事务意味着系统里面会存在很老的事务视图,在这个事务提交之前,回滚记录都要保留,这会导致大量占用存储空间。除此之外,长事务还占用锁资源,可能会拖垮库。
- 事务启动方式:一、显式启动事务语句,begin或者start transaction,提交commit,回滚rollback;二、set autocommit=0,该命令会把这个线程的自动提交关掉。这样只要执行一个select语句,事务就启动,并不会自动提交,直到主动执行commit或rollback或断开连接。
- 事务使用建议:如果考虑多一次交互问题,可以使用commit work and chain语法。在autocommit=1的情况下用begin显式启动事务,如果执行commit则提交事务。如果执行commit work and chain则提交事务并自动启动下一个事务。
1.2 事务特性如何实现的?
- 原子性、一致性和持久性
原子性、一致性和持久性是通过redo log和undo log来实现的:- 原子性: 实现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的sql语句。InnoDB实现回滚,靠的是undo log:当事务对数据库进行修改时,InnoDB会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
- 持久性:实现持久性是通过redo log完成的,当数据修改时,会把这个操作写入redo log,当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
- 隔离性
隔离性是通过锁机制和mvcc机制实现的,通过mvcc实现一个事务的快照读不受另外一个事务当前读的操作影响,通过锁机制控制两个事务对同一行数据的隔离更新操作;
1.3 快照读与当前读
- mysql中的两个“视图”:
1.view:它是一个用查询语句定义的虚拟表,在调用的时候执行查询语句并生成结果;- 一致性读视图:InnoDB 在实现 MVCC 时用到的一致性读视图,即 consistent read view,用于支持 RC(Read Committed,读提交)和 RR(Repeatable Read,可重复读)隔离级别的实现。
- 事务的启点?
1.事务的启动是在执行第一个语句的时候才开始的,不是从begin开始;一致性读视图是执行第一个快照读的时候建立的;
2.如果用start transaction with consistent snapshot代替begin启动一个事务,那么一致性读视图是在执行这个命令的时候建立的 - mysql如何实现“秒”级快照,即使一个库里有几百G数据?
- 在RR隔离级别时,在一个事务内,当执行第一个快照读时会建立一个一致性快照读,之后本事务内所有的查询都用这个视图,以保证事务之间的隔离性。
- RR隔离级别时,事务启动时会向事务管理器申请一个事务id,这个事务ID全局递增,每行数据的每个版本都会用这个事务id标识,是事务启动时声明:“以我启动的时刻为准,如果一个数据版本是在我启动之前生成的,就认;如果是我启动以后才生成的,我就不认,我必须要找到它的上一个版本”。
- 每行数据会有多个版本,各个版本是通过undo日志计算出来的,在数据更新时会记录一个逆向操作的undo日志,通过undo日志就可以推算出各个版本的数据,每个版本是通过事务id来标识的
- 当前读:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。除了 update 语句外,select 语句如果加锁,也是当前读,例如:查询语句 select * from t where id=1 加上 lock in share mode 或 for update,就是当前读;
二 分布式事务
2.1 XA协议
分布式事务处理的XA规范如下所示:
可知XA规范中分布式事务有AP,RM,TM组成:
其中应用程序(Application Program ,简称AP):AP定义事务边界(定义事务开始和结束)并访问事务边界内的资源。
资源管理器(Resource Manager,简称RM):Rm管理计算机共享的资源,许多软件都可以去访问这些资源,资源包含比如数据库、文件系统、打印机服务器等。
事务管理器(Transaction Manager ,简称TM):负责管理全局事务,分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚、失败恢复等。
-
XA协议是使用了二阶段协议的,其中:
- 第一阶段TM要求所有的RM准备提交对应的事务分支,询问RM是否有能力保证成功的提交事务分支,RM根据自己的情况,如果判断自己进行的工作可以被提交,那就就对工作内容进行持久化,并给TM回执OK;否者给TM的回执NO。RM在发送了否定答复并回滚了已经的工作后,就可以丢弃这个事务分支信息了。
- 第二阶段TM根据阶段1各个RM prepare的结果,决定是提交还是回滚事务。如果所有的RM都prepare成功,那么TM通知所有的RM进行提交;如果有RM prepare回执NO的话,则TM通知所有RM回滚自己的事务分支。
2.2 mysql基于XA实现的分布式事务
在MySQL数据库分布式事务中,MySQL是XA事务过程中的资源管理器(RM)存在的,TM是连接MySQL服务器的客户端。MySQL数据库是作为RM存在的,在分布式事务中一般会涉及到至少两个RM,所以我们说的MySQL支持XA协议是说mysql作为RM来说的,也就是说MySQL实现了XA协议中RM应该具有的功能;
MySQL中只有InnoDB引擎支持XA协议
mysql中xa事务使用的基本语法如下:
看一个基本的使用例子:
mysql> xa start '111';
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t values(1, "a");
Query OK, 1 row affected (0.00 sec)
mysql> insert into t values(2, "b");
Query OK, 1 row affected (0.00 sec)
mysql> xa end '111';
Query OK, 0 rows affected (0.00 sec)
mysql> xa prepare '111';
Query OK, 0 rows affected (0.00 sec)
mysql> xa commit '111';
Query OK, 0 rows affected (0.01 sec)
看一下binlog[row格式],start、end和commit都会产生一个query event, prepare会产生一个XA PREPARE event
log.000016 | 887 | Query | 1 | 981 | XA START X'313131',X'',1 |
| binlog.000016 | 981 | Table_map | 1 | 1036 | table_id: 345 (test.t) |
| binlog.000016 | 1036 | Write_rows | 1 | 1079 | table_id: 345 flags: STMT_END_F |
| binlog.000016 | 1079 | Table_map | 1 | 1134 | table_id: 345 (test.t) |
| binlog.000016 | 1134 | Write_rows | 1 | 1177 | table_id: 345 flags: STMT_END_F |
| binlog.000016 | 1177 | Query | 1 | 1269 | XA END X'313131',X'',1 |
| binlog.000016 | 1269 | XA_prepare | 1 | 1308 | XA PREPARE X'313131',X'',1 |
| binlog.000016 | 1308 | Gtid | 1 | 1385 | SET @@SESSION.GTID_NEXT= '38c9a80a-ac85-11ea-8fbf-0242c0a80a02:28' |
| binlog.000016 | 1385 | Query | 1 | 1480 | XA COMMIT X'313131',X'',1
- 当xa start开启事务后,DML也会在对应的RM上创建undo以及read view。
- 当xa prepare 时会将子事务置于PREPARED状态,此时子事务已经完成事务提交前的所有准备工作(获得锁,并将PREPARED状态记录到共享表空间中,会将xa start到xa end之间操作记录在binlog中)。
- 当xa commit 时会在binlog中记录xa commit xid, 并将innodb中PREPARED状态转化为COMMITED状态。
- 当xa commit one phase 时会同时进行prepare和commit 两种操作,是在TM发现全局的分布式事务只涉及一个RM时进行的(因为不需要等待其他RM的反馈结果)。
- 当xa rollback在xa prepare前时,因为没有写binlog和redo,只会释放undo, read view以及lock。
- 当xa rollback 在xa prepare之后时,除了需要释放undo, read view以及lock,还需要binlog中记录xa rollback xid(使得从库不会提交该事务)以及innodb中将PREPARED状态转化为ROLLBACK状态。
2.3 mysql如何保证数据不丢失?
总的来说,MySQL是通过binlog和redo log两者来保证数据不丢失的,mysql 采用WAL的机制,内部也通过两阶段提交(内部XA)来保证binlog和redo log的写入一直性,看一下两阶段提交流程图:
在回顾一下更新数据的基本流程:
- 当一个事务更新数据时,会把相应的redo log写入到redo log buffer中,redo log buffer是一快内存,然后出于prepare阶段,如果 innodb_flush_log_at_trx_commit 设置成 1,那么 redo log 在 prepare 阶段就要持久化一次到磁盘;
- 然后写入binlog;
- 最后把事务状态置于commit状态
问题:如果在时刻B时,即已经把写入binlog了,但是在更改状态为commit时,数据库crash了,崩溃恢复时如何处理?
mysql是这样处理的:
- 如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,则直接提交;
- 如果 redo log 里面的事务只有完整的 prepare,则判断对应的事务 binlog 是否存在并完整:
- a. 如果是,则提交事务;
- b. 否则,回滚事务。
问题二:MySQL 怎么知道 binlog 是完整的?
- 一个事务的 binlog 是有完整格式的:statement 格式的 binlog,最后会有 COMMIT;row 格式的 binlog,最后会有一个 XID event。另外,在 MySQL 5.6.2 版本以后,还引入了 binlog-checksum 参数,用来验证 binlog 内容的正确性。对于 binlog 日志由于磁盘原因,可能会在日志中间出错的情况,MySQL 可以通过校验 checksum 的结果来发现。所以,MySQL 还是有办法验证事务 binlog 的完整性的。
总结:mysql 通过两阶段提交,内部binlog和redo log处理板块也通过XA事务的方式来两保证两者的处理一致性来保证数据库发生异常crask时数据也不会丢失的。
引用:
- 《丁奇45讲》
- MySQL 中基于 XA 实现的分布式事务
- MySQL XA 介绍