Java分布式事务

Java分布式原理和应用:http://nesta2001zhang.iteye.com/blog/1146509

JTA( Java  Transaction API)允许应用 程序 执行分布式事务处理--在两个或多个 网络 计算机资源上访问并且更新数据。JDBC 驱动 程序的JTA支持极大地增强了数据访问能力。

本文的目的是要提供一个关于的Java事务处理API(JTA)的高级的概述,以及与分布式事务相关的内容。一个事务处理定义了一个工作逻辑单元,要么 彻底成功要么不产生任何结果。 一个分布式事务处理只是一个在两个或更多网络资源上访问和更新数据的事务处理,因此它在那些资源之间必然是等价的。在本文 中,我们主要关心的是如何处理关系数据库系统。

访问数据库

最好把分布式事务处理中包含的组件看作是独立的过程,而不是考虑它们在一个特定的 电脑 中的位置。这些组件中的一些可以保存在单机中,或者也可在好几台机器之间分布。 下面例子中的图表可以显示在一台特定的电脑上的组件,但是这些操作之间的关系是必须首要考虑的。

最简单的例子:用于本地数据库事务处理的应用程序

关系数据库访问的最简单的形式仅仅包括应用程序、资源管理程序和资源适配器。应用程序只不过是发送请求到数据库并且从数据库中获取数据的最终用户访问点,我们讨论的资源管理程序是一个关系数据库管理系统(RDBMS),比如Oracle或者SQL Server。所有的实际数据库管理都是由这个组件处理的。

资源适配器是外部空间之间的通信管道组件,或者是请求翻译器,在本例中,是应用程序和资源管理程序。在我们的讨论中,这是一个JDBC驱动程序。下面的描述是资源管理程序本地事务处理的一个描述,也就是说,一个事务处理被被限制在一个特定的企业数据库。

应用程序发送一个用于JDBC驱动程序数据的请求,然后翻译这个请求并把它通过网络发送到数据库中。 数据库把数据发送回驱动程序,然后把翻译的结果发送回应用程序,如下图所示:

这个例子说明了在一个简化的系统中的基本的信息流;然而,今天的企业使用的应用程序服务器都添加了其他的组件到这个过程处理中。

应用程序服务器

应用程序服务器是事务处理操作的另一个组件。应用程序服务器处理大部分的应用程序操作并且获得最终用户应用程序的一些负载。基于前面的例子,我们可以看出应用程序服务器在事务处理上添加了另一个操作层:

到目前为止,我们的例子说明了单个的本地事务处理,并且描述了分布式事务处理 模型 的五个组件中的四个。第五个组件,事务管理程序只有当事务将要被分配的时候才会开始被考虑。

分布式事务处理和事务管理程序

像我们前面所提到的,一个分布式事务处理是一个在两个或更多 网络 资源上访问和更新数据的事务处理。

这些资源可以由好几个位于一个单独服务器上的不同的关系型数据库管理系统组成,比如说Oracle、SQL Server和Sybase;它们也可以包 含存在于若干不同的服务器上的同一种数据库的若干个实例。在任何情况下,一个分布式事务处理包括各种的资源管理程序之间的协同作用。这个协同作用是事务管 理函数。

事务管理程序负责作出要么提交(commit)要么退回(rollback)任何分布式事务处理的决定。一个提交决定应该导致一个成功的事务处理;而退回操作则是保持数据库中的数据不变。 JTA指定一个分布式事务处理中的事务管理程序和另一个组件之间的标准 Java 接口:应用程序,应用程序服务器和资源管理程序。 这个关系被显示在下面的图表中:

在事务管理程序周围的数字框框相应于JTA的三个接口部分:

  • 1—UserTransaction—javax.transaction.UserTransaction接口提供能够编程地控制事务处理范围的应用 程序。 javax.transaction.UserTransaction方法开启一个全局事务并且使用调用线程与事务处理关联。
  • 2—Transaction Manager—javax.transaction.TransactionManager接口允许应用程序服务器来控制代表正在管理的应用程序的事务范围。
  • 3—XAResource—javax.transaction.xa.XAResource接口是一个基于X/Open CAE Specification的行业标准XA接口的Java映射。


注意,一个限制性环节是通过JDBC 驱动 程序的XAResource接口的支持。JDBC驱动程序必须支持两个正常的JDBC交互作用:应用程序和/或应用程序服务器,而且以及JTA的XAResource部分。

编写应用程序水平代码的开发者不会关心分布式事务处理管理的细节。 这是分布式事务处理基本结构的工作—应用程序服务器、事务管理程序和JDBC驱动程 序。应用程序代码中唯一的需要注意的就是当连接处于一个分布式事务范围内的时候,不应该调用一个会影响事务边界的方法。特别的是,一个应用程序不应该调用 Connection方法commit、rollback和setAutoCommit(true),因为它们将破坏分布式事务的基本结构管理。

分布式事务处理

事务管理程序是分布式事务基本结构的基本组件;然而JDBC驱动程序和应用程序服务器组件应该具备下面的特征:驱动程序应该实现JDBC 2.0应用程序接口,包括Optional Package接口XADataSource和XAConnection以及JTA接口XAResource。

应用程序服务器应该提供一个DataSource类,用来实现与分布式事务基本结的交互以及一个连接池模块(用于改善性能)。

分布式事务处理的第一步就是应用程序要发送一个事务请求到事务管理程序。虽然最后的commit/rollback决定把事务作为一个简单的逻辑单元来 对待,但是仍然可能会包括许多事务分支。一个事务分支与一个到包含在分布式事务中的每个资源管理程序相关联。因此,到三个不同的关系数据库管理的请求需要 三个事务分支。每个事务分支必须由本地资源管理程序提交或者返回。事务管理程序控制事务的边界,并且负责最后决定应该提交或者返回的全部事务。 这个决定 由两个步骤组成,称为Two - Phase Commit Protocol。

在第一步骤中,事务管理程序轮询所有包含在分布式事务中的资源管理程序(关系数据库管理)来看看哪个可以准备提交。如果一个资源管理程序不能提交,它将不响应,并且把事务的特定部分返回,以便数据不被修改。

在第二步骤中,事务管理程序判断否定响应的资源管理程序中是否有能够返回整个事务的。如果没有否定响应的话,翻译管理程序提交整个事务并且返回结果到应用程序中。

开发事项管理程序代码的开发者必须与所有三个JTA接口有关:UserTransaction、TransactionManager和XAResource,这三个接口都被描述在

Sun JTA specification中。JDBC驱动程序开发者只需要关心XAResource接口。这个接口是允许一个资源管理程序参与事务 的行业标准X/Open XA协议的Java映射。连接XAResource接口的驱动程序组件负责在事务管理程序和资源管理程序之间担任"翻译"的任 务。

关于是否需要分布式事务:http://merryfeng.blog.51cto.com/900911/200175/

我们不断的拆分schema,说了为了下一步的分库做准备,但是由此带来的代价也是显而易见的,我们的分布式事务在不断的增多。我们期望利用分布式事务来保证数据的一致性,但是其带来的影响也是不容忽视的。

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

如此这般,我们当初分库的目的是为了缓解主库的压力,解决热点资源锁的问题,以期这些问题解决后,能够提高系统的吞吐率。但是当我们schema分离,大量使用分布式事务的时候,新的问题来了,事务时间增长,系统的响应可能仍然无法提高,还有一个就是每个事务节点都可能成为瓶颈,毕竟是一根绳子上的蚱蜢,一个有问题,大家一样的都是死翘翘。我们的目的没有达到,反而是增加了DBA的工作量。

如果大家一定要使用分布式事务,请仔细想想如下问题

1.这步操作一定得在事务当中吗?这步操作如果没完成或者失败了,值得回滚整个事务吗?难道没有优雅的补偿措施或者容错措施?
2.分布式事务涉及到的点,必须的这么多?必须得实时的操作这一大串?不能通过通知类操作去精简掉某些点?
3.在发起分布式事务之后,你是不是做了事务无关的操作,尽管这些操作跟事务无关?(如,读取数据、计算、等用户返回消息、等其他模块的调用返回等等)要知道事务应该尽快结束。
4.你没有把一些读操作也算在事务里面了吧?这是很容易犯的错误,你在事务中Enlist了一个select 操作。
5.你的操作,某些步骤可以等全部操作完成之后再执行.这类操作具有明显的通知类特点。通知类操作是说,我给你一个通知,并且我保证通知到了你;你必须吃下这个通知,并且保证处理成功,但是你不必我一通知你你就处理。这样的操作很明显可以用另外一个任务去搞。
原则是,尽量缩短事务时间。

后续参考淘宝核心技术团队博客关于分布式事务的两篇文章

分布式事务:http://rdc.taobao.com/blog/cs/?p=637

CAP理论虽然告诉我们,一致性和可用性二者不可兼得,但这并不代表互联网系统都应该牺牲一致性,哪个特性更重要只有业务需求才能决定。

ACID是单机事务的特性,不过在分布式系统中,由于两台机器理论上无法达到一致的状态(参考Fischer等的论文),需要引入一个单点进行协 调,这就出现了著名的两阶段锁协议。两阶段锁(Two-phase commit)协议是每个分布式工程师必须掌握的协议,大致如下:

1, Prepare:协调者(Coordinator)给每个参与者(Participants)发送Prepare消息,每个参与者要么直接返回失败,比如权限验证失败,要么在本地执行但不提交,到达一种“万事俱备,只欠东风”的状态;

2, Commit/Rollback:如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作;

两阶段锁是一种悲观锁,第一个问题是协议本身的成本:整个协议过程是需要加锁的,比如锁住数据库的某条记录,且需要持久化大量事务状态相关的操作日志。更为重要的是,两阶段锁在出现故障时表现出来的脆弱性,比如两阶段锁的致命缺陷:当协调者出现故障,整个事务需要等到协调者恢复后才能继续执行,如果 协调者出现类似磁盘故障等永久性错误,该事务将成为永久遗弃的孤儿。两阶段锁的更为详细的描述可以参考Andrew S. Tanenbaum的大作<>。

针对两阶段锁协调者故障的问题,有不同的解决方法:第一种是三阶段锁(Three-phase commit),这种方法纯粹是理论上的方法,工程上不具备可操作性;第二种方法是对协调者进行Replication,当主协调者出现故障时,可以由备 机接替其继续服务。如果需要将这个过程自动化,可以引入Paxos协议执行主协调者选举(参考Jim Gray和Lamport的论文)。

当然,这里需要简单说明,并不是所有和多机有关的事务都需要两阶段锁,比如Microsoft Azure系统中存三个副本,主副本的事务操作以操作日志的形式同步到辅副本,虽然三台机器都进行了事务操作,不过本质上还是单机的事务操作。

和两阶段锁这种悲观锁相对应的就是乐观锁:大多数操作成功失败的可能性很小,所以不需要类似两阶段锁的Prepare阶段,直接在多个参与者上执 行,当某个参与者执行出现问题,再执行补偿操作。又如存储系统中经常出现的Read-Test-Write操作,我们不会将整个过程锁住,而是允许 Read-Test-Write过程中其它客户端对互斥资源的访问,比如Read阶段记录数据版本,Write的时候检查版本,如果发现不一致则重试整个 Read-Test-Write过程。

BASE主要是针对多套业务系统而言,在存储系统内部一般不提这个概念。如果是数据库这样的SQL系统,就是ACID的一致性模型;如果是类似 GFS + Bigtable这样的NOSQL系统,就是单行事务的一致性模型;如果是类似Google Megastore这样的NOSQL系统,就是支持跨机多行事务的一致性模型;如果是Dynamo这样的去中心化系统,就是基于冲突合并的一致性模型。多套业务系统之间可以采用BASE的原则进行架构设计,比如采用消息中间件进行可靠的消息传输,保证多台业务系统最终达到一致的状态。

几乎所有的NOSQL存储系统都不支持分布式事务,因为需求不够强烈,且实现极其复杂。然而,如果业务确实需要分布式事务,那就支持吧,虽然影响扩展性,不过不可能因为我们是NOSQL系统就可以找到其它不影响扩展性的方法。

用消息队列消除分布式事务:http://rdc.taobao.com/blog/cs/?p=671

由于数据量的巨大,大部分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

 

你可能感兴趣的:(Java分布式事务)