分布式事物解决方法:零配置 零耦合 零依赖
应用场景
- 非分布式架构系统下
这是本地事务处理,默认情况下,serviceA中开启事务之后,serviceA方法中调用serviceB、serviceC方法,均会保证serviceA、serviceB、serviceC三者业务处理的事务一致性与原子性;
- 分布式架构系统下
serviceA、serviceB、serviceC不同节点下,默认情况下,serviceA、serviceB、serviceC三节点服务均会拥有自己的事务且相互不受影响,同时serviceA方法中远程调用其他两个节点方法具有依次性,假如serviceB、serviceC业务方法均处理完且节点本地事务均提交,但在serviceA方法最后异步异常了,事务需要回滚,这就出现严重的问题,因为不同节点上,serviceB、serviceC事务已经处理完成无法回滚,所以如何保证serviceA、serviceB、serviceC三者业务处理的事务一致性与原子性就是我们需要解决的问题!也就是如何实现分布式事务?
目前主流解决方案
-
两阶段提交(2PC)
优点: 尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域(其实也不能100%保证强一致)
缺点: 实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景,如果分布式系统跨接口调用,目前 .NET 界还没有实现方案 -
补偿事务(TCC)
优点: 跟2PC比起来,实现以及流程相对简单了一些,但数据的一致性比2PC也要差一些
缺点: 缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理 -
本地消息表(异步确保)
优点: 一种非常经典的实现,避免了分布式事务,实现了最终一致性
缺点: 消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理 -
MQ 事务消息
优点: 实现了最终一致性,不需要依赖本地数据库事务。
缺点: 实现难度大,主流MQ不支持,RocketMQ事务消息部分代码也未开源
本项目解决思路(mything.jar)
其思路本质:事务预处理+事务异步提交+事务同步回滚
我们分布式系统中,每一个节点均有自己的本地事务,也就是可以再各自本地节点上建立一个各自的事务管理器;通过用户请求traceId识别一次处理业务的一致性;让每一个节点serviceB、serviceC事务开启之后,会先处理自己本地业务方法,但处理完不会立即提交事务,只会对事务进行一个预处理,判断是否是否可以正确提交;当调用者serviceA对serviceB、serviceC调用完成且本地业务方法正常处理完成时,就会同步通过HTPP通知serviceB、serviceC节点事务同时提交,同时serviceA本地事务也会提交;反正,假如任何一段有异常或者事务预处理失败最后都会通知到serviceA中,serviceA就会通过HTTP通知所有节点对事物进行回滚;这是理想情况下,99.99%的均能正常处理,但这有一个缺点!假如任何一台节点停止服务了时,假如是调用者(消费节点)服务器停掉了,那么被调用者(服务节点)事务就会迟迟未提交,处于等待通知提交状态,导致出现锁表现象;针对该特殊情况,本项目解决方法就是被调用者(服务节点)会每5S对调用者(根消费节点)请求主动咨询,主要是判断本地等待事务是否还需要继续等待通知,如果不需要或者主动咨询失败(2次),本地事务管理器会让其进行回滚;
调用者(根消费节点):serviceA(首先建立事务的)
调用者(消费节点):serviceA、serviceB(相对serviceC来说,两者均是消费)
被调用者(服务节点):serviceB、serviceC(相对serviceA来说,两者均是服务)
应用方法
调用者方引用:让Spring扫描"com.tc.mything.consume"目录,如下
@EnableTransactionManagement //启用事务
@DubboComponentScan("com.xw.serve.task")
@ComponentScan(basePackages={"com.tc.mything.consume"})
@ImportResource({"classpath*:spring-*.xml"})
public class App {
public static void main(String[] args) {
// TODO Auto-generated method stub
SpringApplication application = new SpringApplication(App.class);
application.setRegisterShutdownHook(false);
application.setBannerMode(Banner.Mode.OFF);
BeanHeader.setApplicationContext(application.run(args));
}
}
被调用者引用:让Spring扫描"com.tc.mything.serve"目录,如下
@EnableTransactionManagement //启用事务
@ImportResource({"classpath*:spring-*.xml"})
@DubboComponentScan({"com.xw.serve.common"})
@ComponentScan(basePackages={"com.tc.mything.serve"})
public class App {
public static void main(String[] args) {
// TODO Auto-generated method stub
SpringApplication application = new SpringApplication(App.class);
application.setRegisterShutdownHook(false);
application.setBannerMode(Banner.Mode.OFF);
BeanHeader.setApplicationContext(application.run(args));
}
}
只需要这样引入即可,如果不想引用去掉mything.jar或者去掉扫描路径就行了,对系统原有业务代码无半点侵入!真正达到零配置、零耦合、零依赖、且高效率、应用简单;
特别说明下:本项目实现方式默认过滤了get、select 开头的service方法事务处理,且目前只针对TransactionDefinition.PROPAGATION_REQUIRED处理**
应用结果说明
调用者serviceA代码,如下
@Component("tastTest")
public class TastTest {
@Reference(check=false)
private SysParameterService sysParameterService;
@Scheduled(cron = "0/20 * * * * *")
@Transactional
public void scheduled(){
//第一次调用
sysParameterService.saveSysParameter(SysParameterType.ThirdParty, "ceshi1", SysParamKey.EMAIL_NICK_NAME, "1234560");
//第二次调用
sysParameterService.selectByKey(1);
// TODO Auto-generated method stub
//第三次调用
sysParameterService.saveSysParameter(SysParameterType.ThirdParty, "ceshi", SysParamKey.EMAIL_SENDER, "123456");
System.out.println("=====>>>>>使用cron {}");
}
}
被调用者serviceB代码,如下
@Service(timeout=5000)
@Transactional
public class SysParameterServiceImpl extends BaseServiceImpl<SysParameterMapper,SysParameter,Integer> implements SysParameterService {
private static Map<String, Object> sysParam;
public boolean saveSysParameter(SysParameterType type, String name, SysParamKey key, String val) {
// TODO Auto-generated method stub
boolean bool = false;
SysParameter sp = new SysParameter();
sp.setCreateTime(new Date());
sp.setIsDel(IsDel.NO);
sp.setKey(key.name());
sp.setName(name);
sp.setType(type);
sp.setValType(key.getValType());
sp.setVal(val);
int k = super.dao.insert(sp);
if(k>0) {
if(sysParam != null) sysParam.put(key.name(), SysParameterValType.getByObject(key.getValType(), val));
bool = true;
}
return bool;
}
}
结果日志如下:
调用者(消费者)打印日志记录
被调用者(服务者)打印日志记录
通过日志看的出,调用者(消费者)调用被调用者(服务者)插入数据两次,两次均是同一个事务提交处理;
结束
这是本人花了半个月时间研究的一种实现方式,已经运用过半年多了,中途也修复过许多bug,如果程序猿运用中有发现bug可立即联系我,我会第一时间来和你分享解决方法;无论该轻量级框架是否好坏,只是想分享出来让大家可以一起学习,本项目的源代码会在2019年10月1日在码云上分享出来,到时希望该框架版本能够非常完善。如果有需要引用的程序猿可以点击如下来下载,共同使用发现其缺点;本项目只是展现其解决方法思路,如果针对springcloud或者其他的分布式框架系统均可以采用该思路来解决;
零配置:无任何配置,使用简单明了
零耦合:对系统代码性侵入为零
零依赖:无需依赖类似MQ、数据表等
高效率:利用HTTP无阻塞通知机制
引用mything.jar 下载
mything-1.1.2.jar @联系my:550110979@qq.com