使用讯息系统避免分布式事务
使用消息系统避免分布式事务
首先举个栗子:比如通过支付宝向余额宝转账1000元,这样一种生活中很平凡的事情,却可以引出很多问题:如果系统宕机挂掉,交易没有完成?那么数据就出现了不一致。等等类似的事情,在各类系统中都能找到类似情形。那么也可以换句专业的说法:当一个表update之后,如何保证另外一个与之关联的表也能完成update。
1、本地事务:
支付宝账户表:A id uid amount
余额宝账户表:B id uid amount
用户id:uid = 100
执行上面的过程分两部分:
1、支付宝账户表-1000:update A set amount=amount - 1000 where uid = 100;
2、余额宝账户表+1000:update B set amount=amount + 1000 where uid = 100;
如何保证两部分都能够完成,保证两部分“收支平衡”
对数据库熟悉的同学会那么我们使用transaction(事务)不就解决了
BEGIN TRANSACTION
update A set amount=amount - 1000 where uid = 100;
update B set amount=amount + 1000 where uid = 100;
END TRANSACTION
COMMIT;
OK,上面的操作是没有问题的
那么对spring熟悉的同学也知道 其实在spring只需要使用注解就OK了
@Transactional(rollbackFor=Exception.class)
public void updateAmount(){
updateA();
updateB();
}
是的,没有错的。能够完成。但是这些情况只是针对系统规模小,数据表在一个数据库实例上的;那么系统规模大,对应的数据表分布在不同的数据库实例上,分布在不同的物理节点上,前面采用的本地事务的方式就无用武之地了。
2、分布式事务(两阶段提交协议)
两阶段提交协议经常用来实现分布式事务;一般需要两个角色:协调器C和若干个事务执行者Si;那么事务执行者多半是具体的数据库,同时协调器和事务执行器可在一台机器上。
1、应用程序application发起一个请求到TC(事务协调器)
2、TC(事务协调器)将消息写到本地日志,再向所有的SI(事务执行者)的发送消息。
3、Si(事务执行者)接受到消息之后,执行本地事务但是不commit,如果成功返回yes否则no;同样返回 之前仍要进行日志记录。
4、Tc(事务协调器)接受到所有执行器返回的结果,如果所有的执行全部返回yes,那么发送commit消息给各个执行器,本地事务执行commit;若是有一个返回no,那么tc就会发送abort消息给各个执行器
注:tc和si把发送和接收到的消息存放到本地日志里,主要为了故障恢复复用,如若某一个si从故障中恢复后,先检查本地日志的内容,如果已接收到commit则本地事务执行器commit;若是abort,则回滚;
若是yes则在tc询问,确定下一步;若是什么都没有则可能前面执行已经崩溃,需要回滚。
熟悉java的同学可以看:http://acen-chen.iteye.com/blog/1055481
采用分布式事务,也满足了我们前面的需求,同时新的问题随之而来
1、两阶段提交涉及多个节点的网络通信,通信时间如果过长
2、事务的相对时间长了,那么锁定资源的时间也就长了
那么在高并发的服务中,就会存在严重的性能问题。
3、消息队列
在高并发的环境中,我们一般会采用消息队列来避免分布式事务的执行
这和实际生活中,我们去饭店吃饭很类似,首先点单,这时服务员给你一张小票,等待服务器给你端上你的饭菜。
在使用消息队列我们需要做到可靠凭证的保存(分布式事务的消息),有如下两种方式
方式1、支付宝完成扣钱的动作时,并记录消息数据,消息数据和业务数据在同一个数据库实例
BEGIN TRANSACTION
update A set amount=amount - 1000 where uid = 100;
INSERT INTO MESSAGE(UID,AMOUNT,STATUS) VALUES(1,1000,1)
END TRANSACTION
COMMIT;
那么我们可以将支付宝完成扣钱操作的消息通过及时服务发给余额宝,余额宝完成处理返回成功消息,支付宝收到消息,清除消息表中对应的消息记录,即完成本次扣钱操作。
方式2、
1)支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消 息数据,而不真正发送,只有消息发送成功后才会提交事务;
2)当支付宝扣款事务被提交成功后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送该消息;
3)当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。在得到取消发送指令后,该消息将不会被发送;
4)对于那些未确认的消息或者取消的消息,需要有一个消息状态确认系统定时去支付宝系统查询这个消息的状态并进行更新。为什么需要这一步骤,举个例子:假设在第2步支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而导致消息不能被发送。
优点:消息数据独立存储,降低业务系统与消息系统间的耦合;
缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口
------------------------------------------------------------------------------------------------------------------------------------
那么如上的消息队列也就解决了我们实际业务中的高并发情况下分布式事务处理性能低下的问题
在使用消息队列防止重复投递消息
解决方法很简单,增加消息应用状态表(message_apply),通俗来说就是个账本,用于记录消息的消费情况,每次来一个消息,在真正执行之前,先去消息应用状态表中查询一遍,如果找到说明是重复消息,丢弃即可,如果没找到才执行,同时插入到消息应用状态表(同一事务)。
for each msg in queue
Begin transaction
select count(*) as cnt from message_apply where msg_id=msg.msg_id;
if cnt==0 then
update B set amount=amount+10000 where userId=1;
insert into message_apply(msg_id) values(msg.msg_id);
End transaction
commit;
首先举个栗子:比如通过支付宝向余额宝转账1000元,这样一种生活中很平凡的事情,却可以引出很多问题:如果系统宕机挂掉,交易没有完成?那么数据就出现了不一致。等等类似的事情,在各类系统中都能找到类似情形。那么也可以换句专业的说法:当一个表update之后,如何保证另外一个与之关联的表也能完成update。
1、本地事务:
支付宝账户表:A id uid amount
余额宝账户表:B id uid amount
用户id:uid = 100
执行上面的过程分两部分:
1、支付宝账户表-1000:update A set amount=amount - 1000 where uid = 100;
2、余额宝账户表+1000:update B set amount=amount + 1000 where uid = 100;
如何保证两部分都能够完成,保证两部分“收支平衡”
对数据库熟悉的同学会那么我们使用transaction(事务)不就解决了
BEGIN TRANSACTION
update A set amount=amount - 1000 where uid = 100;
update B set amount=amount + 1000 where uid = 100;
END TRANSACTION
COMMIT;
OK,上面的操作是没有问题的
那么对spring熟悉的同学也知道 其实在spring只需要使用注解就OK了
@Transactional(rollbackFor=Exception.class)
public void updateAmount(){
updateA();
updateB();
}
是的,没有错的。能够完成。但是这些情况只是针对系统规模小,数据表在一个数据库实例上的;那么系统规模大,对应的数据表分布在不同的数据库实例上,分布在不同的物理节点上,前面采用的本地事务的方式就无用武之地了。
2、分布式事务(两阶段提交协议)
两阶段提交协议经常用来实现分布式事务;一般需要两个角色:协调器C和若干个事务执行者Si;那么事务执行者多半是具体的数据库,同时协调器和事务执行器可在一台机器上。
1、应用程序application发起一个请求到TC(事务协调器)
2、TC(事务协调器)将消息写到本地日志,再向所有的SI(事务执行者)的发送消息。
3、Si(事务执行者)接受到消息之后,执行本地事务但是不commit,如果成功返回yes否则no;同样返回 之前仍要进行日志记录。
4、Tc(事务协调器)接受到所有执行器返回的结果,如果所有的执行全部返回yes,那么发送commit消息给各个执行器,本地事务执行commit;若是有一个返回no,那么tc就会发送abort消息给各个执行器
注:tc和si把发送和接收到的消息存放到本地日志里,主要为了故障恢复复用,如若某一个si从故障中恢复后,先检查本地日志的内容,如果已接收到commit则本地事务执行器commit;若是abort,则回滚;
若是yes则在tc询问,确定下一步;若是什么都没有则可能前面执行已经崩溃,需要回滚。
熟悉java的同学可以看:http://acen-chen.iteye.com/blog/1055481
采用分布式事务,也满足了我们前面的需求,同时新的问题随之而来
1、两阶段提交涉及多个节点的网络通信,通信时间如果过长
2、事务的相对时间长了,那么锁定资源的时间也就长了
那么在高并发的服务中,就会存在严重的性能问题。
3、消息队列
在高并发的环境中,我们一般会采用消息队列来避免分布式事务的执行
这和实际生活中,我们去饭店吃饭很类似,首先点单,这时服务员给你一张小票,等待服务器给你端上你的饭菜。
在使用消息队列我们需要做到可靠凭证的保存(分布式事务的消息),有如下两种方式
方式1、支付宝完成扣钱的动作时,并记录消息数据,消息数据和业务数据在同一个数据库实例
BEGIN TRANSACTION
update A set amount=amount - 1000 where uid = 100;
INSERT INTO MESSAGE(UID,AMOUNT,STATUS) VALUES(1,1000,1)
END TRANSACTION
COMMIT;
那么我们可以将支付宝完成扣钱操作的消息通过及时服务发给余额宝,余额宝完成处理返回成功消息,支付宝收到消息,清除消息表中对应的消息记录,即完成本次扣钱操作。
方式2、
1)支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消 息数据,而不真正发送,只有消息发送成功后才会提交事务;
2)当支付宝扣款事务被提交成功后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才真正发送该消息;
3)当支付宝扣款事务提交失败回滚后,向实时消息服务取消发送。在得到取消发送指令后,该消息将不会被发送;
4)对于那些未确认的消息或者取消的消息,需要有一个消息状态确认系统定时去支付宝系统查询这个消息的状态并进行更新。为什么需要这一步骤,举个例子:假设在第2步支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而导致消息不能被发送。
优点:消息数据独立存储,降低业务系统与消息系统间的耦合;
缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口
------------------------------------------------------------------------------------------------------------------------------------
那么如上的消息队列也就解决了我们实际业务中的高并发情况下分布式事务处理性能低下的问题
在使用消息队列防止重复投递消息
解决方法很简单,增加消息应用状态表(message_apply),通俗来说就是个账本,用于记录消息的消费情况,每次来一个消息,在真正执行之前,先去消息应用状态表中查询一遍,如果找到说明是重复消息,丢弃即可,如果没找到才执行,同时插入到消息应用状态表(同一事务)。
for each msg in queue
Begin transaction
select count(*) as cnt from message_apply where msg_id=msg.msg_id;
if cnt==0 then
update B set amount=amount+10000 where userId=1;
insert into message_apply(msg_id) values(msg.msg_id);
End transaction
commit;