1) 事务
事务由一组操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作,要么全都正确执行,要么全都不要执行。
2) 事务的四大特性ACID
3) 脏读、幻读、虚读及不可重复读
不可重复读:包括幻读和虚读两种情况
4) 数据库的四种隔离级别
幻读
。要解决幻读问题,需要将数据库的隔离级别设置为串行化。注:mysql默认的隔离级别是重复读级别,oracle是读提交
5) 乐观锁和悲观锁
在数据库系统中,既有存放数据的文件,也有存放日志的文件。日志在内存中也是有缓存Log buffer,也有磁盘文件log file。
MySQL中的日志文件,有这么两种与事务有关:undo日志与redo日志。
数据库事务具备原子性(atomicity),如果事务执行失败,需要把数据回滚。事务同时还具备持久性(durability),事务对数据所做的变更需要保存到硬盘,不能因为故障而丢失。
事务的原子性可以利用undo日志来实现。
undo log的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到undo log,然后进行数据的修改。如果出现了错误或者用户执行了rollback
语句,系统可以利用undo log中的备份将数据恢复到事务开始之前的状态。
数据库写入数据到磁盘之前,会把数据先缓存到内存中,事务提交时才会写入磁盘中。用undo log实现原子性和持久化的事务的简化过程如下:
假设有A、B两个数据,值分别为1、2:
1) 事务开始
2) 记录A=1到undo log buffer
3) 修改A=3
4) 记录B=2到undo log buffer
5) 修改B=4
6) 将undo log写到磁盘
7) 将数据写到磁盘
8) 事务提交
事务提交前,会把修改数据刷到磁盘,也就是说只要事务提交了,数据肯定持久化了。
每次对数据库修改,都会把修改前数据记录在undo log中,那么需要回滚时,可以读取undo log,恢复数据。
此时事务并未提交,需要回滚。而undo log已经被持久化,可以根据undo log来恢复数据。
此时数据并未持久化到硬盘,依然保持在事务之前的状态。
缺陷: 每个事务提交前将数据和undo log写入磁盘,这样会导致大量的磁盘IO,因此性能很低。
如果能够将数据缓存一段时间,就能减少IO提高性能,但是这样就会丧失事务的持久性,因此引入了另外一种机制来实现持久化,即redo log。
和undo log相反,redo log记录的是新数据的备份。在事务提交前,只要将redo log持久化即可,不需要将数据持久化,减少了IO的次数。
先来看一下基本原理,undo + redo事务的简化过程:
假设有A、B两个数据,值分别为1,2:
1) 事务开始;
2) 记录A=1到undo log buffer;
3) 修改A=3;
4) 记录A=3到redo log buffer;
5) 记录B=2到undo log buffer;
6) 修改B=4;
7) 记录B=4到redo log buffer;
8) 将undo log写入磁盘;
9) 将redo log写入磁盘;
10) 事务提交
如果在事务提交前故障,通过undo log日志恢复数据。如果undo log都还没写入,那么数据就尚未持久化,无需回滚。
大家会发现,这里并没有出现数据的持久化。因为数据已经写入redo log,而redo log持久化到了硬盘,因此只要到了步骤9)以后,事务是可以提交的。
因为redo log已经持久化,因此数据库数据写入磁盘与否影响不大,不过为了避免出现脏数据(内存中与磁盘不一致),事务提交后也会将内存数据刷入磁盘(也可以按照设定的频率刷新内存数据到磁盘中)。
redo log会在事务提交之前,或者redo log buffer满了的时候写入磁盘。
这里存在两个问题:
1) 问题1:之前是写undo和数据库数据到硬盘,现在是写undo和redo到磁盘,似乎没有减少IO次数
因此事务提交前,只需要对redo log持久化即可。
另外,redo log并不是写入一次就持久化一次,redo log在内存中也有自己的缓冲池redo log buffer
。每次写redo log都是写入到buffer,在提交时一次性持久化到磁盘,减少IO次数。
2) 问题2:redo log数据是写入内存buffer中,当buffer满或者事务提交时,将buffer数据写入磁盘。redo log中记录的数据,有可能包含尚未提交的事务,如果此时数据库崩溃,那么如何完成数据恢复?
数据恢复有两种策略:
InnoDB引擎采用的是第二种方案,因此undo log要在redo log前持久化。
当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几个子系统:商品系统、订单系统、支付系统、积分系统等。整个下单的过程如下:
1) 用户通过商品系统浏览商品,他看中了某一项商品,便点击下单
2) 此时订单系统会生成一条订单
3) 订单创建成功后,支付系统提供支付功能
4) 当支付完成后,由积分系统为该用户增加积分
上述2)、3)、4) 需要在一个事务中完成。对于传统单体应用而言,实现事务非常简单,只需将这三个步骤放在一个方法A中,再用spring的@Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证这些步骤要么全部执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系统之间,通过某项黑科技,实现分布式事务的支持。
在一个分布式系统中,最多只能满足C、A、P中的两个需求:
BASE是三个单词的缩写:
如下图所示,订单服务、库存服务、用户服务及他们对应的数据库就是分布式应用中的三个部分。
由上面的两种思想,延伸出了很多的分布式事务解决方案:
1) 正常情况
如上图所示,正常情况下可分为两阶段:
协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行成功的信息(agree)。
协调组发现每个参与者都可以执行事务(agree),于是向各个事务参与者发出commit指令,各个事务参与者提交事务。
2) 异常情况
如上图所示,异常情况的处理方式为:
3) 缺点
2PC的缺点在于不能处理Fail-stop形式的节点failure。比如下图这种情况:
假设cordinator和voter3都在commit这个阶段crash了,而voter1和voter2没有收到commit消息。这时候voter1和voter2就陷入了一个困境。因为他们并不能判断现在是两个场景中的哪一种:
4) 阻塞问题
在准备阶段、提交阶段,每个事物参与者都会锁定本地资源,并等待其它事务的执行结果,阻塞时间较长,资源锁定时间太久,因此执行的效率就比较低了。
TCC模式可以解决2PC中的资源锁定和阻塞问题,减少资源锁定时间。它本质是一种补偿的思路,事务运行过程包括三个方法:
执行分两个阶段:
粗看似乎与两阶段提交没什么区别,但其实差别很大:
以下单业务中的扣减余额为例来看下怎么编写,假设账户A原来余额是100,需要余额扣减30元。如图:
1) 一阶段(try)
- 余额检查,并冻结用户部分金额,此阶段执行完毕,事务已经提交。
- 检查用户余额是否充足,如果充足,冻结部分余额
- 在账户表中添加冻结金额字段,值为30,余额不变
2) 二阶段
- 提交(Confirm):真正的扣款,把冻结金额从余额中扣除,冻结金额清空
- 修改冻结金额为0,修改余额为100-30 = 70元
- 补偿(Cancel):释放之前冻结的金额,并非回滚
- 余额不变,修改账户冻结金额为0
1) 优势
TCC执行的每一个阶段都会提交本地事务并释放锁,并不需要等待其它事务的执行结果。而如果其它事务执行失败,最后不是回滚,而是执行补偿操作。这样就避免了资源的长期锁定和阻塞等待,执行效率比较高,属于性能比较好的分布式事务方式。
2) 缺点
3) 使用场景
一般分为事务的发起者A和事务的其它参与者B:
这个过程有点像你去学校食堂吃饭:
几个注意事项:
那么问题来了,我们如何保证消息发送一定成功?如何保证消费者一定能收到消息?
参看如下简化图:
事务发起者
事务接收者
额外的定时任务
定时扫描表中超时未消费的消息,重新发送
优点
缺点
为了解决上述问题,我们会引入一个独立的消息服务,来完成对消息的持久化、发送、确认、失败重试等一系列行为,大概的模型如下:
一次消息发送时序图:
事务发起者A的基本执行步骤:
消息服务本身提供下面的接口:
事务参与者B的基本步骤:
优点:
缺点:
RabbitMQ确保消息不丢失的思路比较奇特,并没有使用传统的本地表,而是利用了消息的确认机制:
经过上面的两种确认机制,可以确保从消息生产者到消费者的消息安全,再结合生产者和消费者两端的本地事务,即可保证一个分布式事务的最终一致性。
基本原理:
有没有感觉跟TCC的执行很像,都是分两个阶段:
但AT模式底层做的事情可完全不同,而且第二阶段根本不需要我们编写,全部有Seata自己实现了。也就是说:我们写的代码与本地事务时代码一样,无需手动处理分布式事务。
1) 一阶段
在一阶段,Seata 会拦截“业务 SQL”,首先解析SQL语义,找到“业务 SQL”要更新的业务数据,在业务数据被更新前,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,再将其保存成“after image”,最后获取全局行锁,提交事务。以上操作全部在一个数据库事务内完成,这样保证了一阶段操作的原子性。
这里的before image
和after image
类似于数据库的undo和redo日志,但其实是用数据库模拟的
2) 二阶段
二阶段如果是提交的话,因为“业务 SQL”在一阶段已经提交至数据库, 所以 Seata 框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。
二阶段如果是回滚的话,Seata 就需要回滚一阶段已经执行的“业务 SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前要首先要校验脏写,对比“数据库当前业务数据”和 “after image”,如果两份数据完全一致就说明没有脏写,可以还原业务数据,如果不一致就说明有脏写
,出现脏写就需要转人工处理。
不过因为有全局锁机制,所以可以降低出现脏写
的概率。
AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。
注:Seata的详细流程不做赘述。
[参看]:
https://ivanzz1001.github.io/records/post/distribute-systems/2018/05/30/distribute-transaction
喜欢,在看