您的位置:首页 > 其它

分布式事务会面临的问题

2015-06-11 07:30 155 查看
回顾:spring事务

一、Propagation :

  key属性确定代理应该给哪个方法增加事务行为。这样的属性最重要的部份是传播行为。有以下选项可供使用:

PROPAGATION_REQUIRED--支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择。

PROPAGATION_SUPPORTS--支持当前事务,如果当前没有事务,就以非事务方式执行。

PROPAGATION_MANDATORY--支持当前事务,如果当前没有事务,就抛出异常。

PROPAGATION_REQUIRES_NEW--新建事务,如果当前存在事务,把当前事务挂起。

PROPAGATION_NOT_SUPPORTED--以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

PROPAGATION_NEVER--以非事务方式执行,如果当前存在事务,则抛出异常。

二、Isolation Level(事务隔离等级):

1、Serializable:最严格的级别,事务串行执行,资源消耗最大;

2、REPEATABLE READ:保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。

3、READ COMMITTED:大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。

4、Read Uncommitted:保证了读取过程中不会读取到非法数据。隔离级别在于处理多事务的并发问题。

* 更新:基于Best Efforts 1PC模式的事务

与分布式事务采用的两阶段提交不同,Best Efforts 1PC模式采用的是一阶段端提交,牺牲了事务在某些特殊情况(当机、网络中断等)下的安全性,却获得了良好的性能,特别是消除了对水平伸缩的桎酷。Distributed transactions in Spring, with and without XA一文对Best Efforts 1PC模式进行了详细的说明,该文提供的Demo代码更是直接给出了在Spring环境下实现一阶段提交的多数据源事务管理示例。不过需要注意的是,原示例是基于spring
3.0之前的版本,如果你使用spring 3.0+,会得到如下错误:java.lang.IllegalStateException: Cannot activate transaction synchronization - already active,如果使用spring 3.0+,你需要参考spring-data-neo4j的实现

参考:http://blog.csdn.net/bluishglc/article/details/7793172

1. spring 管理分布式JTA - 可以解决多个系统,多个数据库的事务管理。利用ATOMIKOS 配置UserTransactionManager和UserTransactionImp 到spring JtaTransactionManager 中 transactionManager和userTransaction属性中。

[html] view
plaincopy





<bean id="atomikosTransactionManager"

class="com.atomikos.icatch.jta.UserTransactionManager"

init-method="init" destroy-method="close">

<!-- when close is called, should we force transactions to terminate or not? -->

<property name="forceShutdown">

<value>true</value>

</property>

</bean>

<!-- Also use Atomikos UserTransactionImp, needed to configure Spring -->

<bean id="atomikosUserTransaction"

class="com.atomikos.icatch.jta.UserTransactionImp">

<property name="transactionTimeout">

<value>300</value>

</property>

</bean>

<!-- Configure the Spring framework to use JTA transactions from Atomikos -->

<bean id="springJTATransactionManager"

class="org.springframework.transaction.jta.JtaTransactionManager">

<property name="transactionManager">

<ref bean="atomikosTransactionManager" />

</property>

<property name="userTransaction">

<ref bean="atomikosUserTransaction" />

</property>

</bean>

refer to: http://blog.chinaunix.net/uid-20787846-id-3509987.html

2. 但是前提是,这些系统都属于你的JTA control范围内。

比如下面的场景(比如SOA系统的分布式事务场景):System A 是自己的系统,System B是外部系统(或者是另一个网络域的service2),双方只通过webservice(或者RPC)进行调用。系统A处理自己的业务,开启事务,完成后通过webservice给B发请求。B开始处理事务,如果B处理成功,开始通知A,但是由于网络原因,A在timeout内,未能收到来自B的response。那A就会事务回滚或者失败,那么两个系统的一致性就被破坏了。

note: 先不要简单的说,那就把system A和system B整合到一起嘛。让他们置于分布式JTA的管理下。好吧,直白的说,system B是其他的公司做的,是甲方,根本不可能为了你system A做大的修改。

所以是不是可以这样说?当分布式JTA 没有对跨系统的数据源进行控制时,那此时就无法在这样的场景下应用分布式JTA的事务管理?

目前的方案:

1. 对于A这边,timeout的事务,A系统会做记录,然后轮询B系统(B提供了查询状态的API),是否那边也是失败的,如果是失败的。则不管,若是成功的,则通知管理员,利用后台管理程序 把A系统对应的状态,改回来。

2. 因为A是业务的发起方,所以还是跟甲方谈一谈,B系统针对A系统,开放一个API,进行之前调用数据的回滚。即 A调用B --> B成功 --> A等待response直到timeout, A事务失败--> A调用B的API,B通过开启新事务进行数据回滚。

补充:

1. 针对SOA系统中的事务处理: 大规模SOA系统的分布式事务处理 重点看“复合模式2:可靠消息”

2. 使用业务事务(参考ebay最佳实践)或者利用消息队列和消息应用状态(这里的消息应用状态跟消息中间件的消息无关,实际是对应我的项目中的账单状态,由transactionid来区分)消除分布式事务:把2个分布式事务原子化,第一步处理service
A的事务,存储消息到队列,第二步轮询消息,执行service B的事务,更新消息(即下文中transaction的)应用状态。

Good to review: 针对手机支付项目,笔记本流程图中对消息结合业务事务的方式 做出了说明: 两端的message_applied, confirmed状态维护,以及“确认系统”处理异常情况。

【转】由于数据量的巨大,大部分Web应用都需要部署很多个数据库实例。这样,有些用户操作就可能需要去修改多个数据库实例中的数据。传统的解决方法是使用分布式事务保证数据的全局一致性,经典的方法是使用两阶段提交协议。

长期以来,分布式事务提供的优雅的全局ACID保证麻醉了应用开发者的心灵,很多人都不敢越雷池一步,想像没有分布式事务的世界会是怎样。如今就如MySQL和PostgreSQL这类面向低端用户的开源数据库都支持分布式事务了,开发者更是沉醉其中,不去考虑分布式事务是否给系统带来了伤害。

事实上,有所得必有所失,分布式事务提供的ACID保证是以损害系统的可用性、性能与可伸缩性为代价的。只有在参与分布式事务的各个数据库实例都能够正常工作的前提下,分布式事务才能够顺利完成,只要有一个工作不正常,整个事务就不能完成。这样,系统的可用性就相当于参加分布式事务的各实例的可用性之积,实例越多,可用性下降越明显。从性能和可伸缩性角度看,首先是事务的总持续时间通常是各实例操作时间之和,因为一个事务中的各个操作通常是顺序执行的,这样事务的响应时间就会增加很多;其次是一般Web应用的事务都不大,单机操作时间也就几毫秒甚至不到1毫秒,一但涉及到分布式事务,提交时节点间的网络通信往返过程也为毫秒级别,对事务响应时间的影响也不可忽视。由于事务持续时间延长,事务对相关资源的锁定时间也相应增加,从而可能严重增加了并发冲突,影响到系统吞吐率和可伸缩性。

正是由于分布式事务有以上问题,eBay在设计上就不采用分布式事务,而是通过其它途径来解决数据一致性问题。其中使用的最重要的技术就是消息队列和消息应用状态表。

举个例子。假设系统中有以下两个表

user(id, name, amt_sold, amt_bought)

transaction(xid, seller_id, buyer_id, amount)

其中user表记录用户交易汇总信息,transaction表记录每个交易的详细信息。

这样,在进行一笔交易时,若使用事务,就需要对数据库进行以下操作:

begin;

INSERT INTO transaction VALUES(xid, $seller_id, $buyer_id, $amount);

UPDATE user SET amt_sold = amt_sold + $amount WHERE id = $seller_id;

UPDATE user SET amt_bought = amt_bought + $amount WHERE id = $buyer_id;

commit;

即在transaction表中记录交易信息,然后更新卖家和买家的状态。

假设transaction表和user表存储在不同的节点上,那么上述事务就是一个分布式事务。要消除这一分布式事务,将它拆分成两个子事务,一个更新transaction表,一个更新user表是不行的,因为有可能transaction表更新成功后,更新user失败,系统将不能恢复到一致状态。

解决方案是使用消息队列。如下所示,先启动一个事务,更新transaction表后,并不直接去更新user表,而是将要对user表进行的更新插入到消息队列中。另外有一个异步任务轮询队列内容进行处理。

begin;

INSERT INTO transaction VALUES(xid, $seller_id, $buyer_id, $amount);

put_to_queue “update user(“seller”, $seller_id, amount);

put_to_queue “update user(“buyer”, $buyer_id, amount);

commit;

for each message in queue

begin;

dequeue message;

if message.type = “seller” then

UPDATE user SET amt_sold = amt_sold + message.amount WHERE id = message.user_id;

else

UPDATE user SET amt_bought = amt_bought + message.amount WHERE id = message.user_id;

end

commit;

end

上述解决方案看似完美,实际上还没有解决分布式问题。为了使第一个事务不涉及分布式操作,消息队列必须与transaction表使用同一套存储资源,但为了使第二个事务是本地的,消息队列存储又必须与user表在一起。这两者是不可能同时满足的。

如果消息具有操作幂等性,也就是一个消息被应用多次与应用一次产生的效果是一样的话,上述问题是很好解决的,只要将消息队列放到transaction表一起,然后在第二个事务中,先应用消息,再从消息队列中删除。由于消息队列存储与user表不在一起,应用消息后,可能还没来得及将应用过的消息从队列中删除时系统就出故障了。这时系统恢复后会重新应用一次这一消息,由于幂等性,应用多次也能产生正确的结果。

但实际情况下,消息很难具有幂等性,比如上述的UPDATE操作,执行一次和执行多次的结束显然是不一样的。解决这一问题的方法是使用另一个表记录已经被成功应用的消息,并且这个表使用与user表相同的存储。假设增加以下表 message_applied(msg_id)记录被成功应用的消息,则产生最终的解决方案如下:

begin;

INSERT INTO transaction VALUES(xid, $seller_id, $buyer_id, $amount);

put_to_queue “update user(“seller”, $seller_id, amount);

put_to_queue “update user(“buyer”, $buyer_id, amount);

commit;

for each message in queue

begin;

SELECT count(*) as cnt FROM message_applied WHERE msg_id = message.id;

if cnt = 0 then

if message.type = “seller” then

UPDATE user SET amt_sold = amt_sold + message.amount WHERE id = message.user_id;

else

UPDATE user SET amt_bought = amt_bought + message.amount WHERE id = message.user_id;

end

INSERT INTO message_applied VALUES(message.id);

end

commit;

if 上述事务成功

dequeue message

DELETE FROM message_applied WHERE msg_id = message.id;

end

end

我们来仔细分析一下:

1、消息队列与transaction使用同一实例,因此第一个事务不涉及分布式操作;

2、message_applied与user表在同一个实例中,也能保证一致性;

3、第二个事务结束后,dequeue message之前系统可能出故障,出故障后系统会重新从消息队列中取出这一消息,但通过message_applied表可以检查出来这一消息已经被应用过,跳过这一消息实现正确的行为;

4、最后将已经成功应用,且已经从消息队列中删除的消息从message_applied表中删除,可以将message_applied表保证在很小的状态(不清除也是可以的,不影响系统正确性)。由于消息队列与message_applied在不同实例上,dequeue message之后,将对应message_applied记录删除之前可能出故障。一但这时出现故障,message_applied表中会留下一些垃圾内容,但不影响系统正确性,另外这些垃圾内容也是可以正确清理的。

虽然由于没有分布式事务的强一致性保证,使用上述方案在系统发生故障时,系统将短时间内处于不一致状态。但基于消息队列和消息应用状态表,最终可以将系统恢复到一致。使用消息队列方案,解除了两个数据库实例之间的紧密耦合,其性能和可伸缩性是分布式事务不可比拟的。

当然,使用分布式事务有助于简化应用开发,使用消息队列明显需要更多的工作量,两者各有优缺点。个人观点是,对于时间紧迫或者对性能要求不高的系统,应采用分布式事务加快开发效率,对于时间需求不是很紧,对性能要求很高的系统,应考虑使用消息队列方案。对于原使用分布式事务,且系统已趋于稳定,性能要求高的系统,则可以使用消息队列方案进行重构来优化性能。

注: 本文取材于eBay的工程师Dan Pritchet写的这篇文章 ,并转载至http://wangyuanzju.blog.163.com/blog/static/1302920086424341932。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: