java2pc3pc实现_分布式事务-2PC实战Updated on ⼀⽉ 2, 2017
分布式事务-2PC实战
在了解分布式事务之前,先了解⼀下什么是事务的基本要素及事务属性:
⼀、事务的基本要素
事务的四个基本要素:ACID
原⼦性:整个事务中的操作,要么全部完成, 要么全部不完成(全部撤销)。
⼀致性:事务开始之前和结束之后,数据库的完整性没有遭到破坏。
隔离性:在同⼀时间,只允许⼀个事务请求同⼀数据。
持久性:事务完成以后,该事务对数据库所做的操作持久化在数据库中,并不会被回滚。
⼆、事务的属性:
1.传播⾏为(事务的传递):
2.隔离级别(控制并发的弊端):
3.只读(优化);
4.超时(释放资源);
5.回滚规则(指定要不要再出错后回滚事务);
使⽤Spring注解管理传播⾏为:
// 如果有事务,那么加⼊事务,没有的话新建⼀个(默认)
1)@Transactional(propagation=Propagation.REQUIRED)
// 容器不为这个⽅法开启事务(如果有事务将其挂起,执⾏⽅法)
2)@Transactional(propagation=Propagation.NOT_SUPPORTED)
// 不管是否存在事务,都创建⼀个新的事务,原来的挂起,新的执⾏完毕,继续执⾏⽼的事务
3)@Transactional(propagation=Propagation.REQUIRES_NEW)
/
/ 必须在⼀个已有的事务中执⾏,否则抛出异常
4)@Transactional(propagation=Propagation.MANDATORY)
// 必须在⼀个没有的事务中执⾏,否则抛出异常(与Propagation.MANDATORY相反)
5)@Transactional(propagation=Propagation.NEVER)
// 如果其他bean调⽤这个⽅法,在其他bean中声明事务,那就⽤事务.如果其他bean没有声明事务,那就不⽤事务.
6)@Transactional(propagation=Propagation.SUPPORTS)
//内嵌到⼀个事务⾥⾯,当运⾏到内部Transaction时会停⽌,执⾏内部的Transaction 等其执⾏完了才执⾏外部的;
7)@Transactional(propagation=Propagation.NESTED)
/*
public void methodName(){log4j2不打印日志
// 本类的修改⽅法 1
update();
// 调⽤其他类的修改⽅法
otherBean.update();
// 本类的修改⽅法 2
update();
}
other失败了不会影响 本类的修改提交成功
本类update的失败,other也失败
*/
事务中经常出现的并发问题
分析⼏个场景:
脏读:⼀个事务读取了另⼀个事务操作但未提交的数据。
⽐如A、B两个事务,都操作同⼀张表,A刚刚对数据进⾏了操作(插⼊、修改等)但还没有提交,这时B读取到了A刚刚操作的数据,因为A有可能回滚,所以这部分数据有可能只是临时的、⽆效的,即脏数据。
不可重复读:⼀个事务中的多个相同的查询返回了不同数据。
⽐如A、B两个事务,A中先后有两次查询相同数据的操作,第⼀次查询完之后,B对相关数据进⾏了修改,造成A事务第⼆次查询出的数据与第⼀次不⼀致。
幻读:事务并发执⾏时,其中⼀个事务对另⼀个事务中操作的结果集的影响。
⽐如A、B两个事务,事务A操作表中符合条件的若⼲⾏。事务B插⼊符合A操作条件的数据⾏,然后再提交。后来发现事务A并没有如愿
对“所有”符合条件的数据⾏做了修改~~
SQL规范定义的四个事务隔离级别
以上都是事务中经常发⽣的问题,所以为了兼顾并发效率和异常控制,SQL规范定义了四个事务隔离级别:
Read uncommitted (读未提交):如果设置了该隔离级别,则当前事务可以读取到其他事务已经修改但还没有提交的数据。这种隔离级别是最低的,会导致上⾯所说的脏读
Read committed (读已提交):如果设置了该隔离级别,当前事务只可以读取到其他事务已经提交后的数据,这种隔离级别可以防⽌脏读,但是会导致不可重复读和幻读。这种隔离级别最效率较⾼,并且不可重复读和幻读在⼀般情况下是可以接受的,所以这种隔离级别最为常⽤。
Repeatable read (可重复读):如果设置了该隔离级别,可以保证当前事务中多次读取特定记录的结果相同。可以防⽌脏读、不可重复读,但是会导致幻读。
Serializable (串⾏化):如果设置了该隔离级别,所有的事务会放在⼀个队列中执⾏,当前事务开启后,其他事务将不能执⾏,即同⼀个时间点只能有⼀个事务操作数据库对象。这种隔离级别对于保证数据完整性的能⼒是最⾼的,但因为同⼀时刻只允许⼀个事务操作数据库,所以⼤⼤降低了系统的并发能⼒。
引⽤⼀张很经典的表格来按隔离级别由弱到强来标⽰为:
查看事务隔离级别
命令⾏登录mysql,查看当前事务隔离级别:
select @@tx_isolation; 或者  select @@_isolation;
3.只读;
// readOnly=true只读,不能更新,删除
@Transactional (propagation = Propagation.REQUIRED,readOnly=true)
4.超时;释放资源
// 设置超时时间
@Transactional (propagation = Propagation.REQUIRED,timeout=30)
5.回滚规则;
默认遇到throw new RuntimeException("...");会回滚
需要捕获的throw new Exception("...");不会回滚
// 指定回滚
@Transactional(rollbackFor=Exception.class)
public void methodName() {
// 不会回滚
throw new Exception("...");
}
//指定不回滚
@Transactional(noRollbackFor=Exception.class)
public ItimDaoImpl getItemDaoImpl() {
// 会回滚
throw new RuntimeException("注释");
}
本地事务
以⽀付宝转账余额宝为例,假设有
⽀付宝账户表:A(id,userId,amount)
余额宝账户表:B(id,userId,amount)
⽤户的userId=1;
从⽀付宝转账1万块钱到余额宝的动作分为两步:
1)⽀付宝表扣除1万:update A set amount=amount-10000 where userId=1;
2)余额宝表增加1万:update B set amount=amount+10000 where userId=1;如何确保⽀付宝余额宝收⽀平衡呢?有⼈说这个很简单嘛,可以⽤事务解决。
⾮常正确!如果你使⽤spring的话⼀个注解就能搞定上述事务功能。
如果系统规模较⼩,数据表都在⼀个数据库实例上,上述本地事务⽅式可以很好地运⾏,但是如果系统规模较⼤,⽐如⽀付宝账户表和余额宝账户表显然不会在同⼀个数据库实例上,他们往往分布在不同的物理节点上,这时本地事务已经失去⽤武之地。
既然本地事务失效,分布式事务⾃然就登上舞台。
什么是分布式事务?
分布式事务是指事务的参与者、⽀持事务的服务器、资源管理器以及事务管理器分别位于分布系统的不同节点之上,在两个或多个⽹络计算机资源上访问并且更新数据,将两个或多个⽹络计算机的数据进⾏的多次操作作为⼀个整体进⾏处理。如不同银⾏账户之间的转账。
分布式事务—两阶段提交协议
两阶段提交协议(Two-phase Commit,2PC)经常被⽤来实现分布式事务。⼀般分为协调器C和若⼲事务执⾏者Si两种⾓⾊,这⾥的事务执⾏者就是具体的数据库,协调器可以和事务执⾏器在⼀台机器上。
1) 我们的应⽤程序(client)发起⼀个开始请求到TC;
2) TC先将消息写到本地⽇志,之后向所有的Si发起消息。以⽀付宝转账到余额宝为例,TC给A的prepare消息是通知⽀付宝数据库相应账⽬扣款1万,TC给B的prepare消息是通知余额宝数据库相应账⽬增加1w。为什么在执⾏任务前需要先写本地⽇志,主要是为了故障后恢复⽤,本地⽇志起到现实⽣活中凭证 的效果,如果没有本地⽇志(凭证),出问题容易死⽆对证;
3) Si收到消息后,执⾏具体本机事务,但不会进⾏commit,如果成功返回,不成功返回。同理,返回前都应把要返回的消息写到⽇志⾥,当作凭证。
4) TC收集所有执⾏器返回的消息,如果所有执⾏器都返回yes,那么给所有执⾏器发⽣送commit消息,执⾏器收到commit后执⾏本地事务的commit操作;如果有任⼀个执⾏器返回no,那么给所有执⾏器发送abort消息,执⾏器收到abort消息后执⾏事务abort操作。
注:TC或Si把发送或接收到的消息先写到⽇志⾥,主要是为了故障后恢复⽤。如某⼀Si从故障中恢复后,先检查本机的⽇志,如果已收到,则提交,如果则回滚。如果是,则再向TC询问⼀下,确定下⼀步。如果什么都没有,则很可能在阶段Si就崩溃了,因此需要回滚。
现如今实现基于两阶段提交的分布式事务也没那么困难了,如果使⽤java,那么可以使⽤开源软件
atomikos(www.atomikos/)来快速实现。
不过但凡使⽤过的上述两阶段提交的同学都可以发现性能实在是太差,根本不适合⾼并发的系统。为什么?
1)两阶段提交涉及多次节点间的⽹络通信,通信时间太长!
2)事务时间相对于变长了,锁定的资源的时间也变长了,造成资源等待时间也增加好多!
正是由于分布式事务存在很严重的性能问题,⼤部分⾼并发服务都在避免使⽤,往往通过其他途径来解决数据⼀致性问题。在⾼并发的时候不建议使⽤。
对于在项⽬中接触到JTA,⼤部分的原因是因为在项⽬中需要操作多个数据库,同时,可以保证操作的原⼦性,保证对多个数据库的操作⼀致性。
下⾯我将基于Spring4.1.7+atomikos+mybaits 实现两阶段的分布式事务处理,通过AOP⾯向切⾯实现动态实现数据源的切换所需JAR:
com.atomikos
transactions-jta
4.0.4
com.atomikos
atomikos-util
4.0.4
com.atomikos
transactions-jms
4.0.4
com.atomikos
transactions-osgi
4.0.4
com.atomikos
transactions-api
4.0.4
jta
1.1
配置l:
init-method="init" destroy-method="close" abstract="true">
${jdbc.driverClassName}
${order.jdbc.url}
${order.jdbc.password}
${order.jdbc.username}
${jdbc.initialSize}
${jdbc.maxActive}
${jdbc.minIdle}
${jdbc.maxWait}
SELECT 'x' FROM DUAL
${stOnBorrow}
${stOnReturn}
${stWhileIdle}