赞
踩
目录
本地事务,是指传统的单机数据库事务,必须具备ACID原则;
所谓的原子性就是说,在整个事务中的所有操作,要么全部完成,要么全不做,没有中间状态.对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就行从来没有执行过一样
事务的执行必须保证系统的一致性,在事务开始之前和事务结束之后,数据库的完整性没被破坏
就拿转账为例,A有500元,B有500元,如果在一个事务里A成功转给B500元,那么不管发生什么,那么最后A账户和B账户的数据之和必须是1000元.
所谓的隔离性就是说,事务与事务之间不会互相影响,一个事务的中间状态不会被其他事务感知.数据库保证隔离性包括4中不同的隔离级别
所谓的持久性,就是说一旦事务提交了,那么事务对数据所做的变更就完全保存在了数据库中,即使发生停电,系统宕机也是如此.
因为在传统项目中,项目部署基本是单点式:即单个服务器和单个数据库.这种情况下,数据本上神的事务机制就能保证ACID原则,这样的事务就是本地事务.
概况来讲,单个服务与单个数据库的架构中,产生的事务都是本地事务.
在数据库系统中,既有存放数据的文件,也有存放日志的文件.日志内存中也是有缓存Log buffer,也有磁盘文件log file.
MySQL中的日志文件.有这么两种与事务有关:undo日志和redo日志.
数据库事务具备原子性(Atomicity),如果事务执行失败,需要包数据回滚.
事务同时还是具备持久性(Durabiity)
其中原子性和持久性就要考undo和redo日志来实现.
Undo Log的原理很简单,为了满足事务的原子性,在操作任何数据之前,首先将数据备份到Undo Log. 然后进行数据的修改. 如果出现了错误,或者用户执行了ROLLBACK语句,系统可以利用Undo Log中的备份将数据恢复到事务开始之前的状态.
数据库写入数据到磁盘之前,会把数据先缓存在内存中,事务提交时才会写入磁盘中.
用Undo Log 实现原子性和持久化的事务简化过程.
假设有A、B两个数据,值分别为1,2.
A.事务开始。
B.记录A=1到undo log。
C.修改A=3.
D.记录B=2到undo log。
E.修改B=4.
F.将undo log写到磁盘。
G.将数据写到磁盘。
H.事务提交。
事务提交之前,会把修改数据到磁盘前,也就是说事务提交了,数据肯定持久化了。
每次对数据库修改,都会把修改前数据记录在undo log,那么需要回滚室,可以读取undo log,回复数据。
若系统在G和H之间崩溃,此时事务并未提交,需要回滚。而undo log已经被持久化,可以根据undo log来恢复数据。
若在系统G之前崩溃,此时数据并未持久化到硬盘,依然保持在事务之前的状态。
缺陷: 每个事务提交前数据和Undo Log写入磁盘,这样会导致大量的磁盘IO,因此性能很低。
如果能将数据缓存一段时间,就能减少IO性能。但是这样就会丧失事务的持久性。因此引入了另外一种机制来实现持久化,即Redo Log。
和Undo Log相反,Redo Log记录的是新数据的备份。在事务提交前,只要将Redo Log持久化即可,不需要将数据持久化,减少了IO的次数。
先来看下基本原理
Undo+Redo事务的简化过程
假设有A、B两个数据,值分别为1,2。
A.事务开始。
B.记录A=1到undo log buffer。
C.修改A=3。
D.记录A=3到Redo log buffer。
E.记录B=2到undo log buffer。
F.修改B=4。
G.记录B=4到redo log buffer。
H.将undo log 写入 redo log buffer。
I.将redo log写入磁盘。
J.事务提交
安全性能问题
如何保证原则性?
如果在事务提交前故障,通过undo log日志恢复数据。如果undo log都还没写入,那么数据就尚未持久化,无需回滚
如何保证持久化?
大家会发现,这里并没投出现数据的持久化,因为数据已经写入redo log,儿redo log持久化到了硬盘,因此只要到了步骤 I 以后,事务是可以提交的。
内存中的数据库数据何时持久化到磁盘?
因为redo log已经持久化,因此数据库数据写入磁盘与否影响不大,不过为了避免出现脏数据(内存中与磁盘不一致),事务提交后也将内存数据刷入磁盘(也可以按照固设定的频率刷新内存数据到磁盘中)。
redo log何时写入磁盘
redo log会在事务提交之前,或redo log buffer 满了的时候写入磁盘。
这里存在两个问题:
问题1:之前是写undo和数据库数据写到硬盘,现在是写undo 和 redo到磁盘,似乎没有减少IO次数
因此事务提交前,只需要对redo log持久化即可。
另外redo log并不是写入一次就持久化一次,redo log在你日常中也有自己的缓存池:redo log buffer。每次写redo log都是写入到buffer ,在提交时一次性持久化到磁盘,减少IO次数。
问题2:redo log数据是写入内存buffer中,当buffer满或者事务提交时,将buffer数据写入磁盘。
redo log中记录的数据,有可能包含尚未提交事务,如果此时数据库崩溃,那么如何完成数据恢复?
数据恢复有两种策略:
Inodb引擎采用的时第二种方案,因此undo log要在redo log前持久化。
分布式事务,就是值不是在单个服务或单个数据架构下,产生的事务:
随着业务员数据规模的快速发展,数据量越来越大,单库单表逐渐成为瓶颈。所以我们对数据库进行了水平拆分,将原单库单表拆分数据库分片,于是就产生了跨数据库事务问题。
在业务发展初期,"一块大饼"的但业务系统架构,能满足基本的业务需求。但是随着业务的快速发展,系统的访问量和业务复杂程度都在快速增长,单个系统架构逐渐成为业务发展瓶颈,解决业务系统的高耦合,可伸缩问题的需求越来越强烈。
入下图所示,按照面向服务(SOA)的架构设计原则,将单业务系统拆分城多个业务系统,降低了各系统之间的耦合度,使不同的业务系统专注自身业务,更有利于业务发展和系统容量的伸缩。
在数据库水平拆分,服务垂直拆分之后,一个业务操作通常要跨多个数据库,服务才能完成。在分布式网络环境下,我们无法保证所有的服务,数据库都百分百可用,一定会出现部分服务、数据库执行成功,另一部分执行失败的问题。
当出现部分业务操作成功、部分业务操作失败时,业务数据就会出现不一致。
例如电商Hayes种比较常见的下单付款的案例,包括下面几个行为:
完成上面的操作需要访问三个不同的微服务和三个不同的数据库。
在分布式环境下,肯定会出现部分操作成功、部分操作失败的问题,比如:订单生成了,库存也扣减了,但是用户账户的余额不足,这就造成数据不一致。
订单的创建、库存的扣减、账户扣款在每一个服务和数据库内时一个本地服务,可以保证ACID原子。
但是当我们把三件失去看做一个事情时,要满足保证“业务”的原子性,要么所有操作全部成功,要么全部失败,不允许出现部分成功部分失败的现象,这就是分布式下的事务了。
此时ACID难以满足,这就是分布式事务要解决的问题。
为什么在分布式系统下,事务的ACID原子难以满足?
这要从CAP定理和BASE理论说起。
什么时CAP定理?
1998年,加州大学的计算机科学家Eric Brewer提出,分布式系统有三个指标。
- Consistency(一致性)
- Acailability(可用性)
- Partition tolerance(分区容错性)
它们的第一个字母分别时C、A、P。
Eric Brewer说,这三个指标不可能同时做到。这个结论叫做CAP定理。
2.1.1Partition tolerance(分区容错性)
先看partiton tolerance,中文叫做“分区容错”。
大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思时,区间通信可能失败。比如,一台服务器放在上海,一台服务器放在北京,这就是两个区,他们之间可能因网络问题无法通信。
如图:
上图中,G1和G2时两台跨区的服务器。G1向G2发送一条消息,G2可能无法收到。系统设计的时候必须考虑到这种情况。
一般来说,分布式系统 ,分区容错无法比避免,因此可以认为CAP的P总是成立。根据CAP定理,剩下的C和A无法同时做到。
2.1.2ConsistenCy(一致性)
Consistency中文叫做“一致性”。意思是,写操作之后的读操作,必须返回该值。举例来说,某条记录时v0,用户向G1发起一个写的操作,将其改为v1.
接下来,用户的读操作就会得到v1.这叫一致性。
问题时,用户有可能向G2发起读操作,由于G2的值没有发生便变化,因此返回的就是v0.G1和G2读的操作的结果不一致,这就不满足一致性了。
为了让G2也能 变为V1,就要在G1写操作的时候,让G1向G2发送一条消息,要求G2也改成v1.
这样的话,用户向G2发起读操作,也能得到v1.
2.1.3.Availability(可用性)
Availability中文叫做“可用性”,意思是只要收到用户的请求,服务器就必须给出回应(对和错不论)。
用户可以选择向G1或G2发起读操作,不管是那台服务器,只要收到请求,就必须告诉用户,到底是v0还是v1,否则就不满足可用性。
2.1.4Consistency和Availability的矛盾
一致性和可用性,为什么不可能同时成立?
答案很简单,因为可能通信失败(即出现分区容错)。
如果保证G2的一致性,那么G1必须在写操作是,锁定G2的读操作和写操作。只有数据同步后,才能重新开放读写。锁定期间,G2不能读写,没有可用性。
如果保证G2的可用性,那么势必不能锁定G2,所以一致性不成立。
综上所述,G2无法同时做到一致性和可用性,系统设计时只能选择一个目标。如果追求一致性,那么无法保证所有节点的可用性;如果追求所有节点的可用性,那么就无法做到一致性。
2.1.5几点疑问
除非是单点架构
对一致性要求高的场景。例如我们的Zookeeper就是这样的,在服务节点间数据同时,服务对外不可用。
对可用性要求较高的场景。例如Eureka。必须保证注册中心随时可用,不然拉取不到服务就可能出问题。
BASE是三个单词的缩写:
而我们解决分布式事务,就是根据上述理论来实现。
还以上面的下单减库存和扣款为例:
订单服务、库存服务、用户服务及他们对应的数据库就是分布式应用中的三个部分。
这就是强一致,弱可用
这就是高可用,但弱一致(最终一致性)。
由于上面的两种思想,延申出了很多的分布式事务解决方案:
分布式事务的解决手段之一,就是两阶段提交协议(2pc: Two- Phase Commit)
那么到底什么是两阶段提交协议呢?
1994年,X/Open组织(即现在的Open Group)定义了分布式事务处理的DTP模型。该模型包括这样几个角色:
在该模型中,一个分布式事务(全局事务)可以被拆分成多个本地事务,运行在不同的AP和RM上。每个本地事务的ACID很好实现,但是全局事务必须保证其中包含的每一个本地事务都能同时成功,若有一个本地事务失败,则所有的本地失败,则所有其他事务都必须回滚,但问题是,本地事务处理过程中,并不知道其他事务的运行状态。因此,就需要通过CRM来通知各个本地事务,同步事务执行的状态。
因此,各个本地事务的通信必须有统一的标准,否则不同数据库就无法通信。XA就是X/Open DTP中通信中间件与TM件联系的接口规范,定义了用于通知事务开始、提交、终止、回滚等接口,各个数据库厂商都必须实现这些接口。
3.2二阶段提交
二阶提交协议就是根据这一思想衍生的,将全局事务拆分为两个阶段来执行:
这个过程需要一个协调者(coordinator),还有事务的参与者(voter)。
1)正常情况
投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行成功的信息(agree)
提交阶段:协调组发现每个参与者都可以执行事务(agree),于是向各个事务参与者发出commit指令,各个事务参与者提交事务。
2)异常情况
当然,也有异常的时候:
投票阶段:协调组询问各个事务参与者,是否可以执行事务。每个事务参与者执行事务,写入redo和undo日志,然后反馈事务执行结果,但只要有一个参与者返回的是 Disagree,则说明执行失败
提交阶段:协调组发现有一个或多个参与者返回的是 Disagree,认为执行失败,于是向各个事务参与者发出 abort 指令,各个事务参与者回滚事务。
3)缺陷
二阶段提交的问题:
2PC的缺点在于不能处理fall-stop形式的节点falure。比如下图这种情况。
假设coordinator和vorer3都在Commit这个阶段crash了,二voter1和voter2没有收到commit消息,这时候voter1和voter2就陷入了一个困境,因为他们并不能判断现在是两个场景中的哪一种:
(1)上轮全票通过让后voter3第一个收到了commit的消息并在commit操作之后crash了
(2)上轮voter3反对所以干脆没有通过。
在准备阶段,提交阶段,每个事务参与者都会锁定本地资源,并等待其它事务的执行结果,阻塞时间较长,资源锁定时间太久,因此执行的效率就比较低了。
面对阶段提交的上述缺点,后来又演变出了三阶段提交,但是依然没有完全解决阻塞和资源锁定的问题,而且引入了一些新的问题,因此实际使用的场景较少。
3.2.1使用场景
对事物有着强一致性要求,对事物执行效率不敏感,并且不希望又太多代码侵入。
TCC模式可以解决2PC中的资源锁定和阻塞问题,减少资源锁定时间。
3.3.1基本原理
它本质是一种补偿的思路。事物运行的过程包括三个方法,
执行分两个阶段:
粗看似乎与两阶段提交没什么区别,但其实差别很大:
try、confirm、cancel都是独立的事物,不受其他参与者的影响,不会阻塞等待它人
try、confirm、cancel由程序员在业务层编写,锁粒度有代码控制
3.3.2实例
我们以之前的下单业务的扣减余额为例来看下三个不同的方法要怎么编写,假设账户A原来的余额是100,需要余额扣减30元。如图:
3.3.3优势和缺点
TCC执行的每一个阶段都会提交本地事务并释放锁,并不需要等待其他事务的执行结果。而如果其他事务执行失败,最后不是回滚,而是执行补偿操作。这样就避免了资源的长期锁定和阻塞等待,执行效率比较高,属于性能比较好的分布式事务方式。
3.3.4使用场景
这种实现方式的思路,其实源于ebay其基本的设计思想是将远程分布式事务拆分成一系列的本地事务。
3.4.1基本原理
一般分为事务的发起者A和事务的其他参与者B:
如图:
这个过程有点像你去学校食堂吃饭:
3.4.2本地消息表
为了避免消息发送失败或丢失,我们可以把消息持久化到数据库中。实现时有简化版本和解耦合版本两种方式。
1)简化版本
原理图:
事务发起者:
优点:
与TCC相比,实现方式较为简单,开发成本低。
缺点:
2)独立消息服务
为了解决上述问题,我们会引入一个独立的消息服务,来完成对消息的持久化,发送,确认、失败重试等一系列行为,大概模型如下:
一次消息发送的时序图:
优点:
解除了事务与业务相关业务的耦合
缺点:
实现起来比较复杂
3.4.3RocketMq事务消息
RocketMq本身自带了事务消息,可以播癌症消息的可靠性,原理其实就是自带了本地消息表,与我们上面讲的思路类似。
3.4.4RabbitMq的消息确认
RabbitMq确保消息不丢失的思路比较奇特,并没有使用传统的本地表,而是利用了消息的确认机制:
消息生产者发送消息到RabbitMQ时,可以设置一个异步的监听器,监听来自MQ的ACK
MQ接收到消息后,会返回一个绘制给生产者:
生产者提前编写好不同绘制的处理方式
失败回执:等待一定时间后重新发送
成功回执:记录日志等行为
消费者需要在监听队列的时候指定手动ACK模式
RabbitMQ把消息投递给消费者后,会等待消费者ACK,接收到ACK后才删除消息,如果没有接收到ACK消息会一直保留在服务端,如果消费者断开连接或异常后,消息会投递给其他消费者。
消费者处理完消息,提交事务后,手动ACK。如果执行过程中抛出异常,则不会ACK业务处理失败,等待吓一条消息
经过上卖弄的两种确认机制,可以保证从确包消息生产者到消费者的消息安全,再结合生产者和消费者两端的本地事务,即可保证一个分布式事务的最终一致性。
3.4.5消息事务的优缺点
总结上面几种模型,消息事务的优缺点如下:
优点:
缺点:
针对事务无法回滚的问题,有人提出说可以再事务参与者执行失败后,再次利用MQ通知消息服务,然后由消息服务通知其他参与者回滚。
2019年1月份,Seta开源了AT模式。AT模式是一种无侵入的分布式事务解决方案。可以看作是对TCC或者二阶段提交模型的一种优化,解决了TCC模式中的代码侵入、编码复杂等问题。
在AT模式下,用户只关注自己的“业务SQL”,用户的“业务SQL”作为一阶段,Seata框架会自动生成事务的二阶段提交和回滚操作。
先看一张流程图:
有没有感觉跟TCC执行很像,都是分两个阶段:
但AT模式底层做的事情可完全不同,而且二阶段根本不需要我们编写,全部由Seata自己实现了。也就是说:我们写的代码和本地事务时代码一样,无需手动处理分布式事务。
那么AT模式如何实现无代码侵入,如何我们自动实现二阶段代码呢?
一阶段
在一阶段,seata会拦截“业务SQL”,首先解析SQL语义,找到“业务SQL” 要更新的业务数据,在业务阶段数据被更新钱,将其保存成“before image”,然后执行“业务 SQL”更新业务数据,在业务数据更新之后,在将其保存成“after image”,最后获取全局行锁,提交事务。以上操作全部在一个数据库事务内完成,这样保证了一阶段的原则性。
这里的before image和after image 类似于数据库的undo和redo日志,但其实时用数据库模拟的。
二阶段提交
二阶段如果时提交的话,因为“业务SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保持的快照数据和行锁删掉,完成数据清理即可。
二阶段回滚:
二阶段如果是回滚的话,Seata就需要回滚一阶段已经执行的“业务SQL”,还原业务数据。回滚方式便是用“before image”还原业务数据;但在还原前首先校验脏写,对比“数据库当前业务数据“和”after image“,如果两份数据完全一致就说明没有脏写。可以还原业务数据,如果不一致就说明有”脏写“,出现在脏写就需要转人工处理。
不过因为有全局锁机制,所以可以降低出现脏写的概率。
AT模式的一阶段,二阶段提交和回滚均由Seata框架自动生成,用户只需要编写”业务SQL“,便能轻松接入分布式事务;AT模式是对业务无任何进入的分布式事务解决方案。
Seata中的几个基本概念:
维护全局和分支事务的状态,驱动全局事务提交或回滚(TM之间的协调者)。
定义全局事务的范围:开始全局事务、提交或回滚全局事务。
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
我们看下面的一个架构图
向TC开启一个全局事务
调用其他微服务
执行本地事务
向TC注册分支事务,并提交本地事务执行结果
一阶段
二阶段
分支都成功:通知分支事务,提交事务
有分支执行失败:通知分支事务,提交事务
提交事务:直接情况 before_image 和after_image 信息,释放全局锁
回滚事务:
场景
以一个示例来说明整个AT分支的工作过程
业务表: product
Fied | Type | key |
---|---|---|
id | bigint(20) | PRI |
name | varchar(100) | |
since | varchar(100) |
AT分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC'
一阶段
过程:
select id, name, since from product where name = 'TXC';
得到前镜像:
id | name | since |
---|---|---|
1 | TXC | 2014 |
select id, name, since from product where id = '1';
得到后镜像:
id | name | since |
---|---|---|
1 | TXC | 2014 |
1.插入回滚日志:把前后镜像数据以及业务SQL相关信息组成一条回滚日志记录,插入好UNDO_LOG表中 .
- {
- "branchId":641789253,
- "undoItems":[{
- "afterImage":{
-
- "rows":[{
- "fields":[{
- "name":"id",
- "type": 4 ,
- "value": 1
- },{
- "name":"name",
- "type": 12,
- "value":"GTS"
- },{
- "name":"since",
- "type":"12",
- "value":"2014"
- }]
- }],
- "tableName":"product"
- },
-
- "beforeImage":{
- "rows":[{
- "fields":[{
- "name":"id",
- "type":4,
- "value":1
- },{
- "name":"name",
- "type":12,
- "value":"TXC"
- },{
- "name":"since",
- "type":12,
- "value":"2014"
- }]
- }],
- "tableName":"product"
- },
- "sqlType":"UPDATE"
- }],
- "xid":"xid:XXX"
- }
二阶段-回滚
1.收到TC的分支回滚请求,开启一个本地事务,执行如下操作.
2.通过XID和Branch ID查找到相应的UNDO LOG记录.
3.数据效验:拿UNDO LOG中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务只带的动作做了修改.这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍.
4.根据UNDO LOG中前镜像和业务SQL的相关信息生成并执行回滚的语句;
update product set name= 'TXC' wehre id=1;
1.提交本地事务.并把本地事务的执行结果(即分支事务回滚的结果)上报给TC.
二阶段-提交
与2PC相比:每个分支事务都是独立提交,不互相等地啊,减少了资源锁定和阻塞时间
与TCC相比:二阶段的执行操作全部自动化生成,无代码侵入,开发成本低
与TCC相比,需要动态生成二阶段的反向补偿操作,执行性能略低于TCC
Seata(Simple Extensible Autonomous Transaction Architecture, 简单可扩展自治服务框架)是2019年1月份蚂蚁金服和阿里巴巴共同开源的分布式事务解决方案。Seata开源半年左右,目前已经有接近一万star,社区非常活跃。我们热枕欢迎大家参与到Seata社区建设中,一同将Seta打造成开源分布式事务标杆产品。
ps:seata以后再补吧,码字码了3-4天,累死本宝宝了
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。