Monday, November 29, 2021

SegmentFault 最新的文章

SegmentFault 最新的文章


分布式事务的这些常见用法都有坑,来看看正确姿势

Posted: 28 Nov 2021 05:27 PM PST

处理NPC

分布式系统最大的敌人可能就是NPC了,在这里它是Network Delay, Process Pause, Clock Drift的首字母缩写。我们先看看具体的NPC问题是什么:

  • Network Delay,网络延迟。虽然网络在多数情况下工作的还可以,虽然TCP保证传输顺序和不会丢失,但它无法消除网络延迟问题。
  • Process Pause,进程暂停。有很多种原因可以导致进程暂停:比如编程语言中的GC(垃圾回收机制)会暂停所有正在运行的线程;再比如,我们有时会暂停云服务器,从而可以在不重启的情况下将云服务器从一台主机迁移到另一台主机。我们无法确定性预测进程暂停的时长,你以为持续几百毫秒已经很长了,但实际上持续数分钟之久进程暂停并不罕见。
  • Clock Drift,时钟漂移。现实生活中我们通常认为时间是平稳流逝,单调递增的,但在计算机中不是。计算机使用时钟硬件计时,通常是石英钟,计时精度有限,同时受机器温度影响。为了在一定程度上同步网络上多个机器之间的时间,通常使用NTP协议将本地设备的时间与专门的时间服务器对齐,这样做的一个直接结果是设备的本地时间可能会突然向前或向后跳跃。

分布式事务既然是分布式的系统,自然也有NPC。分布式事务可分为

  • 分布式数据库内部的分布式事务:NPC中的C给这类应用,带来了极大的挑战,例如Spanner采用了原子钟,并且增加了Commit-Wait来解决问题。TiDB采用单点授时,引入了一个单点。
  • 跨数据库的异构分布式事务:这是我们这篇文章讨论的主题,因为没有涉及时间戳,所以NPC带来的困扰主要是NP。

TCC的空补偿与悬挂

我们以分布式事务中的TCC(Try,Confirm,Cancel)作为例子,关于TCC模式的详细介绍可以参考:分布式事务最经典的七种解决方案

一般情况下,一个TCC回滚时的执行顺序是,先执行完Try,再执行Cancel,但是由于N,则有可能Try的网络延迟大,导致先执行Cancel,再执行Try。

这种情况就引入了分布式事务中的两个难题:

  1. 空补偿:Cancel执行时,Try未执行,事务分支的Cancel操作需要判断出Try未执行,这时需要忽略Cancel中的业务数据更新,直接返回
  2. 悬挂:Try执行时,Cancel已执行完成,事务分支的Try操作需要判断出Cancel一致性,这时需要忽略Try中的业务数据更新,直接返回

分布式事务还有一类需要处理的常见问题,就是重复请求,业务需要做幂等处理。因为空补偿、悬挂、重复请求都跟NP有关,我们把他们统称为子事务乱序问题。在业务处理中,需要小心处理好这三种问题,否则会出现错误数据。

现有方案的问题

我看到开源项目dtm之外,包括各云厂商,各开源项目,他们给出的业务实现建议大多类似如下:

  • 空补偿:"针对该问题,在服务设计时,需要允许空补偿,即在没有找到要补偿的业务主键时,返回补偿成功,并将原业务主键记录下来,标记该业务流水已补偿成功。"
  • 防悬挂:"需要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝执行该笔服务,以免造成数据不一致。"

上述的这种实现,能够在大部分情况下正常运行,但是上述做法中的"先查后改"在并发情况下是容易掉坑里的,我们分析一下如下场景:

  • 正常执行顺序下,Try执行时,在查完没有空补偿记录的业务主键之后,事务提交之前,如果发生了进程暂停P,或者事务内部进行网络请求出现了拥塞,导致本地事务等待较久
  • 全局事务超时后,Cancel执行,因为没有查到要补偿的业务主键,因此判断是空补偿,直接返回
  • Try的进程暂停结束,最后提交本地事务
  • 全局事务回滚完成后,Try分支的业务操作没有被回滚,产生了悬挂

事实上,NPC里的P和C,以及P和C的组合,有很多种的场景,都可以导致上述竞态情况,就不一一赘述了。

虽然这种情况发生的概率不高,但是在金融领域,一旦涉及金钱账目,那么带来的影响可能是巨大的。

PS:幂等控制如果也采用"先查再改",也是一样很容易出现类似的问题。解决这一类问题的关键点是要利用唯一索引,"以改代查"来避免竞态条件。

子事务屏障技术

下面我们来详解dtm是如何解决这个问题的。

dtm首创了子事务屏障技术,用于同时解决空补偿、防悬挂、幂等这三个问题,对于TCC事务,他的详细工作过程如下:

  1. 在本地数据库中创建好子事务屏障表dtm_barrier.barrier,唯一索引为gid-branchid-branchop
  2. 对于Try、Confirm、Cancel操作,insert ignore一条记录gid-branchid-try|confirm|cancel,如果影响行数为0(重复请求、悬挂),直接提交返回
  3. 对于Cancel操作额外再insert ingore一条记录 gid-branchid-try,如果影响行数为1(空补偿),直接提交返回
  4. 执行业务逻辑并提交返回,如果业务发生错误则回滚

假如Try和Cancel的执行时间没有重叠,那么读者容易分析出上述过程能够解决空补偿和悬挂问题。如果出现了Try和Cancel执行时间重叠的情况,我们看看会发生什么。

假设Try和Cancel并发执行,Cancel和Try都会插入同一条记录gid-branchid-try,由于唯一索引冲突,那么两个操作中只有一个能够成功,而另一个则会等持有锁的事务完成后返回。

  • 情况1,Try插入gid-branchid-try失败,Cancel操作插入gid-branchid-try成功,此时就是典型的空补偿和悬挂场景,按照子事务屏障算法,Try和Cancel都会直接返回
  • 情况2,Try插入gid-branchid-try成功,Cancel操作插入gid-branchid-try失败,按照上述子事务屏障算法,会正常执行业务,而且业务执行的顺序是Try在Cancel前
  • 情况3,Try和Cancel的操作在重叠期间又遇见宕机等情况,那么至少Cancel会被dtm重试,那么最终会走到情况1或2。

综上各种情况的详细论述,子事务屏障能够在各种NP情况下,保证最终结果的正确性。

事实上,子事务屏障有大量优点,包括:

  • 两个insert判断解决空补偿、防悬挂、幂等这三个问题,比其他方案的三种情况分别判断,逻辑复杂度大幅降低
  • dtm的子事务屏障是SDK层解决这三个问题,业务完全不需要关心
  • 性能高,对于正常完成的事务(一般失败的事务不超过1%),子事务屏障的额外开销是每个分支操作一个SQL,比其他方案代价更小。

上述的理论与分析过程也同样适用于SAGA。dtm里面的子事务屏障同时支持了TCC和SAGA两种事务模式。

Java接入

dtm是首个支持自动处理子事务乱序问题的开源框架,极大的减轻了业务负担,降低了分布式事务的使用门槛。

dtm已经提供了Java的SDK,包含了子事务屏障功能,帮助用户自动处理子事务乱序问题。DTM提供的是一套SDK形式(非注解形式)的接口,易于理解和使用。

我们编写了一个非常完整的示例,该示例的主要功能模拟了一个跨行转账(跨服务转账)的业务场景,包含的内容有:如何组织子事务、如何做冻结资金、如何处理失败、如何使用子事务屏障。您在这个例子的基础上改一改,就能够完成您实际的TCC分布式事务。

这篇文章用Java轻松完成一个分布式事务TCC,自动处理空补偿、悬挂、幂等给出了详细的Java TCC事务原理与接入过程,方便用户快速上手。

小结

阅读完这篇干货文,希望这里介绍的子事务屏障技术能够帮助你更好更正确的完成你的分布式事务。

我们不仅提供了前面Java的子事务屏障SDK,还提供了go、python版本的子事务屏障,有需要可以自取:dtm

如果您有分布式事务相关的业务需求,dtm能够支持多种语言,并且简单易上手。

如果您想要学习分布式事务相关的知识,dtm的文档备受好评,能够让读者快速入门分布式事务,理论结合实践,让读者逐步深入。

欢迎大家访问dtm,欢迎Issue、PR、Star

基于IDEA Plugin插件开发,撸一个DDD脚手架

Posted: 24 Nov 2021 05:25 PM PST

作者:小傅哥
博客:https://bugstack.cn

沉淀、分享、成长,让自己和他人都能有所收获!😄

  • 最近很感兴趣结合 IDEA Plugin 开发能力,扩展各项功能。也基于此使用不同的案例,探索 IDEA Plugin 插件开发技术。希望这样的成体系学习和验证总结,能给更多需要此技术的伙伴,带来帮助。
  • 源码地址:https://github.com/fuzhengwei/CodeGuide#1-%E6%BA%90%E7%A0%81

一、前言

研发,要避免自嗨!

你做这个东西的价值是什么?有竞品调研吗?能赋能业务吗?那不已经有同类的了,你为什么还自己造轮子?

你是不是也会被问到这样的问题,甚至可能还有些头疼。但做的时候挺嗨,研究技术嘛,还落地了,多刺激。不过要说价值,好像一时半会还体现不出来,能不能赋能业务就不更不一定了。

可谁又能保证以后不能呢,技术的点是一个个攻克尝试的才有机会再深度学习后把这些内容连成一片,就像单说水、单说沙子、单说泥巴,好像并没有啥用,但把它们凑到一块再给把火,就烧成了砖,砖就码成了墙,墙就盖成房。

二、需求目的

我们这一章节把 freemarker 能力与 IDEA Plugin 插件能力结合,开发一个DDD 脚手架 IDEA 插件,可能你会想为什么要把脚手架开发到插件里呢?还有不是已经有了成型的脚手架可以用吗?

首先我们目前看到的脚手架基本都是网页版的,也就是一次性创建工程使用,不过在我们实际使用的时候,还希望在工程创建过程中把数据库、ES、Redis等生成对应的 ORM 代码,减少开发工作量。并且在使用的工程骨架的过程中,还希望可以随着开发需要再次补充新的功能进去,这个时候网页版的脚手架都不能很好的支持了。此外一些大厂都会自己的技术体系,完全是使用市面的脚手架基本很难满足自身的需求,所以就需要有一个符合自己场景的脚手架了。

那么,我们本章节就把脚手架的开发放到 IDEA 插件开发中,一方面学习脚手架的建设,另外一方面学习如何改变工程向导,创建出自己需要的DDD结构脚手架。

三、案例开发

1. 工程结构

guide-idea-plugin-scaffolding ├── .gradle └── src     ├── main     │   └── java     │       └── cn.bugstack.guide.idea.plugin      │           ├── domain     │           │     ├── model        │           │     │    └── ProjectConfigVO.java            │           │     └── service        │           │          ├── impl          │           │          │    └── ProjectGeneratorImpl.java       │           │          ├── AbstractProjectGenerator.java          │           │          ├── FreemarkerConfiguration.java           │           │          └── IProjectGenerator.java           │           ├── factory     │           │      └── TemplateFactory.java       │           ├── infrastructure     │           │     ├── DataSetting.java            │           │     ├── DataState.java       │           │     ├── ICONS.java           │           │     └── MsgBundle.java          │           ├── module       │           │     ├── DDDModuleBuilder.java         │           │     └── DDDModuleConfigStep.java              │           └── ui     │                 ├── ProjectConfigUI.java       │                 └── ProjectConfigUI.form     ├── resources     │   ├── META-INF     │   │   └── plugin.xml      │   └── template     │       ├── pom.ftl     │       └── yml.ftl      ├── build.gradle       └── gradle.properties

源码获取:#公众号:bugstack虫洞栈 回复:idea 即可下载全部 IDEA 插件开发源码

在此 IDEA 插件工程中,主要分为5块区域:

  • domain:领域层,提供创建 DDD 模板工程的服务,其实这部分主要使用的就是 freemarker
  • factory:工厂层,提供工程创建模板,这一层的作用就是我们在 IDEA 中创建新工程的时候,可以添加上我们自己的内容,也就是创建出我们定义好的 DDD 工程结构。
  • infrastructure:基础层,提供数据存放、图片加载、信息映射这些功能。
  • module:模块层,提供 DDD 模板工程的创建具体操作和步骤,也就是说我们创建工程的时候是一步步选择的,你可以按需添加自己的步骤页面,允许用户选择和添加自己需要的内容。比如你需要连库、选择表、添加工程所需要的技术栈等
  • ui:界面层,提供Swing 开发的 UI 界面,用于用户图形化选择和创建。

2. UI 工程配置窗体

public class ProjectConfigUI {      private JPanel mainPanel;     private JTextField groupIdField;     private JTextField artifactIdField;     private JTextField versionField;     private JTextField packageField;  }
  • 使用 Swing UI Designer 创建一个配置工厂信息的 UI 窗体,通过这样的方式创建可以直接拖拽。
  • 在这个 UI 窗体中我们主要需要;roupIdartifactIdversionpackage

3. 配置工程步骤创建

3.1 数据存放

cn.bugstack.guide.idea.plugin.infrastructure.DataSetting

@State(name = "DataSetting",storages = @Storage("plugin.xml")) public class DataSetting implements PersistentStateComponent<DataState> {      private DataState state = new DataState();      public static DataSetting getInstance() {         return ServiceManager.getService(DataSetting.class);     }      @Nullable     @Override     public DataState getState() {         return state;     }      @Override     public void loadState(@NotNull DataState state) {         this.state = state;     }       public ProjectConfigVO getProjectConfig(){         return state.getProjectConfigVO();      }  }
  • 在基础层提供数据存放的服务,把创建工程的配置信息存放到服务中,这样比较方便设置和获取。

3.2 扩展步骤

cn.bugstack.guide.idea.plugin.module.DDDModuleConfigStep

public class DDDModuleConfigStep extends ModuleWizardStep {      private ProjectConfigUI projectConfigUI;      public DDDModuleConfigStep(ProjectConfigUI projectConfigUI) {         this.projectConfigUI = projectConfigUI;     }      @Override     public JComponent getComponent() {         return projectConfigUI.getComponent();     }      @Override     public boolean validate() throws ConfigurationException {         // 获取配置信息,写入到 DataSetting         ProjectConfigVO projectConfig = DataSetting.getInstance().getProjectConfig();         projectConfig.set_groupId(projectConfigUI.getGroupIdField().getText());         projectConfig.set_artifactId(projectConfigUI.getArtifactIdField().getText());         projectConfig.set_version(projectConfigUI.getVersionField().getText());         projectConfig.set_package(projectConfigUI.getPackageField().getText());          return super.validate();     }  }
  • 继承 ModuleWizardStep 开发一个自己需要的步骤,这个步骤就会出现到我们创建新的工程中。
  • 同时在重写的 validate 方法中,把从工程配置 UI 窗体中获取到信息,写入到数据配置文件中。

3.3 配置步骤

cn.bugstack.guide.idea.plugin.module.DDDModuleBuilder

public class DDDModuleBuilder extends ModuleBuilder {      private IProjectGenerator projectGenerator = new ProjectGeneratorImpl();      @Override     public Icon getNodeIcon() {         return ICONS.SPRING_BOOT;     }          /**      * 重写 builderId 挂载自定义模板      */     @Nullable     @Override     public String getBuilderId() {         return getClass().getName();     }          @Override     public ModuleWizardStep[] createWizardSteps(@NotNull WizardContext wizardContext, @NotNull ModulesProvider modulesProvider) {          // 添加工程配置步骤,可以自己定义需要的步骤,如果有多个可以依次添加         DDDModuleConfigStep moduleConfigStep = new DDDModuleConfigStep(new ProjectConfigUI());          return new ModuleWizardStep[]{moduleConfigStep};     } }
  • 在 createWizardSteps 方法中,把我们已经创建好的 DDDModuleConfigStep 添加工程配置步骤,可以自己定义需要的步骤,如果有多个可以依次添加。
  • 同时需要注意,只有重写了 getBuilderId() 方法后,你新增加的向导步骤才能生效。

4. 开发脚手架服务

cn.bugstack.guide.idea.plugin.domain.service.AbstractProjectGenerator

public abstract class AbstractProjectGenerator extends FreemarkerConfiguration implements IProjectGenerator {      @Override     public void doGenerator(Project project, String entryPath, ProjectConfigVO projectConfig) {          // 1.创建工程主POM文件         generateProjectPOM(project, entryPath, projectConfig);          // 2.创建四层架构         generateProjectDDD(project, entryPath, projectConfig);          // 3.创建 Application         generateApplication(project, entryPath, projectConfig);          // 4. 创建 Yml         generateYml(project, entryPath, projectConfig);          // 5. 创建 Common         generateCommon(project, entryPath, projectConfig);     }  }
  • 在 domain 领域层添加用于创建脚手架框架的 FreeMarker 服务,它是一款 模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。FreeMarker 在线手册:http://freemarker.foofun.cn
  • 按照 DDD 工程结构,分层包括:application、domain、infrastructure、interfaces,那么我们把这些创建过程抽象到模板方法中,具体交给子类来创建。

5. 调用脚手架服务

cn.bugstack.guide.idea.plugin.module.DDDModuleBuilder

public class DDDModuleBuilder extends ModuleBuilder {      private IProjectGenerator projectGenerator = new ProjectGeneratorImpl();      @Override     public Icon getNodeIcon() {         return ICONS.SPRING_BOOT;     }      @Override     public void setupRootModel(@NotNull ModifiableRootModel rootModel) throws ConfigurationException {          // 设置 JDK         if (null != this.myJdk) {             rootModel.setSdk(this.myJdk);         } else {             rootModel.inheritSdk();         }          // 生成工程路径         String path = FileUtil.toSystemIndependentName(Objects.requireNonNull(getContentEntryPath()));         new File(path).mkdirs();         VirtualFile virtualFile = LocalFileSystem.getInstance().refreshAndFindFileByPath(path);         rootModel.addContentEntry(virtualFile);          Project project = rootModel.getProject();          // 创建工程结构         Runnable r = () -> new WriteCommandAction<VirtualFile>(project) {             @Override             protected void run(@NotNull Result<VirtualFile> result) throws Throwable {                 projectGenerator.doGenerator(project, getContentEntryPath(), DataSetting.getInstance().getProjectConfig());             }         }.execute();      }  }
  • DDDModuleBuilder#setupRootModel 中,添加创建 DDD工程框架的服务,projectGenerator.doGenerator(project, getContentEntryPath(), DataSetting.getInstance().getProjectConfig());
  • 另外这里需要用到 IDEA 提供的线程调用方法,new WriteCommandAction 才能正常创建。

6. 配置模板工程

6.1 模板工厂

cn.bugstack.guide.idea.plugin.factory.TemplateFactory

public class TemplateFactory extends ProjectTemplatesFactory {      @NotNull     @Override     public String[] getGroups() {         return new String[]{"DDD脚手架"};     }      @Override     public Icon getGroupIcon(String group) {         return ICONS.DDD;     }      @NotNull     @Override     public ProjectTemplate[] createTemplates(@Nullable String group, WizardContext context) {         return new ProjectTemplate[]{new BuilderBasedTemplate(new DDDModuleBuilder())};     }  }
  • 模板工厂的核心在于把我们用于创建 DDD 的步骤添加 createTemplates 方法中,这样算把整个创建自定义脚手架工程的链路就串联完成了。

6.2 文件配置

plugin.xml

<idea-plugin>     <id>cn.bugstack.guide.idea.plugin.guide-idea-plugin-scaffolding</id>     <name>Scaffolding</name>     <vendor email="184172133@qq.com" url="https://bugstack.cn">小傅哥</vendor>      <!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html          on how to target different products -->     <depends>com.intellij.modules.platform</depends>      <extensions defaultExtensionNs="com.intellij">         <projectTemplatesFactory implementation="cn.bugstack.guide.idea.plugin.factory.TemplateFactory"/>         <applicationService serviceImplementation="cn.bugstack.guide.idea.plugin.infrastructure.DataSetting"/>     </extensions>  </idea-plugin>
  • 接下来还需要把我们创建的工程模板以及数据服务配置到 plugin.xml 中,这样在插件启动的时候就可以把我们自己插件启动起来了。

四、测试验证

  • 点击 Plugin 启动 IDEA 插件,之后创建工程如下:

  • 快拿去试试吧,启动插件,点击创建工程,傻瓜式点击,就可以创建出一个 DDD 工程结构了。

五、总结

  • 学习使用 IDEA Plugin 开发技术,改变创建工程向导,添加自己需要的工程创建模板,这样就可以创建出一个 DDD 脚手架工程骨架了,接下来你还可以结合自己实际的业务场景添加自己需要的一些技术栈到脚手架中。
  • 如果你愿意尝试可以在工程创建中链接到数据库,把数据库中对应的表生成Java代码,这样一些简单的配置、查询、映射,就不用自己动手写了。
  • 在开发 DDD 脚手架的源码中还有一些细节过程,包括图标的展示、文案的信息、Freemarker的使用细节,这些你都可以在源码中学习并调试验证。

六、系列推荐

No comments:

Post a Comment