分布式系统中,分布式事务是避不开的问题。
理论知识不再过多赘述,CAP,BASE理论,这些都是面试避不开的话题,下面说一下自己的一些经验。
其实无论是单机事务,还是分布式事务,核心都是一个词,事务。在java语言的落地过程中,事务封装在Connection对象中,正常提交是commit(),异常回滚是rollback()。
最好的解决方案就是去掉分布式事务,接口粗化,避免分布式事务,数据一致性要求不高,甚至可以不用考虑,等应用场景多了之后,可以针对这些数据库应用做一个数据清洗程序,定时清洗即可。
但是问题还是存在,解决问题的思路,就是统一操作connection对象,统一执行或者回滚,这样很容易想到二阶段提交的解决方案。A项目执行execute,同时向B项目发起请求,B项目执行execute后返回执行结果,A再commit,B也跟着commit。
这种数据强一致性解决方案,适用于toB或者金融领域,但是有一个前提,应用要有足够强的容错能力,应用之间完全隔离。否则,一旦出现网络波动,事务无法正常提交,将导致应用的线程大量堵塞,宕机丢失重要数据。
下面讲讲我的解决方案,我们的场景是,数据在web页面上提供查询功能,和钱不打交道,查询频率较高,但是数据实时性准确性要求都不高,容错较强,所以我们的选择是其它事务方保证接口幂等性,本地事务表的方式解决这个问题。
这么做的优点是,第一,不会像两阶段事务,出现网络波动阻塞应用,影响核心业务。第二,三阶段提交方案看起来完善,但是落地后一个需求对应多个接口开发,我们流程比较规范,每个接口都要完善的接口文档,测试用例脚本编写,压测,微服务注册,申请新服务等流程,开发,测试运维人远代价过大。所以采用本地事务表方式解决,开发难度低,便于管理,容错性较高。
我的思路是定义DistributedTranscation注解,然后切面拦截,失败后立即重试一次,继续失败的话记录入库,然后定时任务从库中随机获取失败的任务执行,达到最大重试次数后,不执行。同时需要一个定时任务扫描失败达到最大次数的对象,达到标准后通知运维,开发人员及时排查。
下面截取几个关键类图
切面类
@Around(value = "@annotation(com.summary.net.transcation.config.DistributedTranscation)")
public Object invoke(ProceedingJoinPoint point) throws Throwable {
try {
//如果是恢复线程,退出切面
return point.proceed(point.getArgs());
} catch (Throwable throwable) {
//如果是事务恢复线程,退出切面
if (ThreadLocalUtils.isResume()) {
throw throwable;
}
try {
//立即重试一次
return point.proceed(point.getArgs());
} catch (Throwable e) {
TranscationDTO transcationDTO = TranscationDTO.builder()
.id(generateIDSequenceService.generateSequenceByRedis("transcation"))
.className(getClassName(point))
.methodName(getMethodName(point))
.paramClass(getParamTypeList(point))
.paramContents(getParamContentList(point))
.failReason(START.append(getFailReason(e)).toString())
.retryCount(1)
.insertTimeForHis(new Date())
.validStatus("0")
.threadId(Thread.currentThread().getId())
.beanName(getBeanName(point)).build();
log.error("方法异常, {}", e);
for (TranscationIntercetor transcationIntercetor : pluginChain.getAll()) {
transcationDTO = transcationIntercetor.preSave(transcationDTO);
}
resposity.save(transcationDTO);
//重试失败后,保存失败信息
//保存异常信息
throw new RuntimeException(throwable);
}
}
}
定时任务类
//每10秒随机取10条执行
@Scheduled(cron = "0/10 * * * * ? ")
public void configureTask() {
System.out.println("开始执行.........");
if (ObjectUtils.isEmpty(tasks) && !tasks.addAll(resposity.queryTranscationDTOList())) {
return;
}
System.out.println("任务执行.........");
for (int i = 0; i < tasks.size(); i++) {
service.execute(new DistrubutedTaskService(tasks.get(i), pluginChain, resposity));
tasks.remove(i);
}
}
该项目demo版已上传到github。拉下来配置一下数据库即可执行。主服务测试接口为http://localhost:8091/tt,访问othercontroller的t1接口,可以加jvm参数-Dserver.port=8808启动同一个服务,模拟场景.
github地址 https://github.com/836219171/distributed_transcations.git