Oinone如何支持构建分布式项目

分布式调用下的[强制]约束

1、[强制]分布式调用情况下base库和redis需共用;
2、[强制]如果环境有设计器,设计器的base库和redis保持一致也需与项目中的保持一致;
3、[强制]相同base库下,不同应用的相同模块的数据源需保持一致;
4、[强制]项目中需引入分布式缓存包。参考下文的分布式包依赖

分布式支持

1、分布式包依赖

1) 父pom的依赖管理中先加入pamirs-distribution的依赖

<dependency>
    <groupId>pro.shushi.pamirs</groupId>
    <artifactId>pamirs-distribution</artifactId>
    <version>${pamirs.distribution.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

2) 启动的boot工程中增加pamirs-distribution相关包

<!-- 分布式服务发布 -->
<dependency>
    <groupId>pro.shushi.pamirs.distribution</groupId>
    <artifactId>pamirs-distribution-faas</artifactId>
</dependency>
<!-- 分布式元数据缓存 -->
<dependency>
    <groupId>pro.shushi.pamirs.distribution</groupId>
    <artifactId>pamirs-distribution-session</artifactId>
</dependency>
<dependency>
    <groupId>pro.shushi.pamirs.distribution</groupId>
    <artifactId>pamirs-distribution-gateway</artifactId>
</dependency>

3)启动工程的Application中增加类注解@EnableDubbo

@EnableDubbo
public class XXXStdApplication {

    public static void main(String[] args) throws IOException {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        // ………………………………
        log.info("XXXX Application loading...");
    }
}

2、修改bootstrap.yml文件

注意序列化方式:serialization: pamirs

以下只是一个示例(zk为注册中心),注册中心支持zk和Nacos;
Nacos作为注册中心参考:https://doc.oinone.top/kai-fa-shi-jian/5835.html

spring:
  profiles:
    active: dev
  application:
    name: pamirs-demo
  cloud:
    service-registry:
      auto-registration:
        enabled: false
pamirs:
  default:
    environment-check: true
    tenant-check: true

---
spring:
  profiles: dev
  cloud:
    service-registry:
      auto-registration:
        enabled: false
    config:
      enabled: false
      uri: http://127.0.0.1:7001
      label: master
      profile: dev
    nacos:
      server-addr: http://127.0.0.1:8848
      discovery:
        enabled: false
        namespace:
        prefix: application
        file-extension: yml
      config:
        enabled: false
        namespace:
        prefix: application
        file-extension: yml
dubbo:
  application:
    name: pamirs-demo
    version: 1.0.0
  registry:
    address: zookeeper://127.0.0.1:2181
  protocol:
    name: dubbo
    port: -1
    serialization: pamirs
  scan:
    base-packages: pro.shushi
  cloud:
    subscribed-services:
  metadata-report:
    disabled: true

3、模块启动的最⼩集

pamirs:
boot:
 init: true
 sync: true
 modules:
 - base
 - 业务工程的Module

4、业务模型间的依赖关系

  1. 服务调用方(即Client端),在启动yml中modules不安装服务提供方的Module
  2. 服务调用方(即Client端),项目的pom中只依赖服务提供方的API(即模型和API的定义)
  3. 服务调用方(即Client端),项目模块定义(即模型Module定义),dependencies中增加服务提供方的Modeule. 如下面示例代码中的FileModule
    @Module(
        name = DemoModule.MODULE_NAME,
        displayName = "oinoneDemo工程",
        version = "1.0.0",
        dependencies = {ModuleConstants.MODULE_BASE, CommonModule.MODULE_MODULE, 
                FileModule.MODULE_MODULE, SecondModule.MODULE_MODULE/**服务提供方的模块定义*/
        }
    )
  4. 服务调用方(即Client端),启动类的ComponentScan需要配置服务提供方API定义所在的包. 如下面示例中的:pro.shushi.pamirs.second
    @ComponentScan(
        basePackages = {"pro.shushi.pamirs.meta",
                "pro.shushi.pamirs.framework",
                "pro.shushi.pamirs",
                "pro.shushi.pamirs.demo",
                "pro.shushi.pamirs.second" /**服务提供方API定义所在的包*/
        },
        excludeFilters = {
                @ComponentScan.Filter(
                        type = FilterType.ASSIGNABLE_TYPE,
                        value = {RedisAutoConfiguration.class, RedisRepositoriesAutoConfiguration.class,
                                RedisClusterConfig.class}
                )
        })
    @Slf4j
    @EnableTransactionManagement
    @EnableAsync
    @EnableDubbo
    @MapperScan(value = "pro.shushi.pamirs", annotationClass = Mapper.class)
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class, FreeMarkerAutoConfiguration.class})
    public class DemoApplication {

5、模块启动顺序

服务提供方的模块需先启动。原因:模块在启动过程中,会校验依赖模块是否存在。

6、Dubbo日志相关

关闭Dubbo元数据上报

dubbo:  
    metadata-report:
            disabled: true

关闭元数据上报,还有错误日志打印出来的话,可以在log中配置

logging:
      level:
        root: info
        pro.shushi.pamirs.framework.connectors.data.mapper.PamirsMapper: info
        pro.shushi.pamirs.framework.connectors.data.mapper.GenericMapper: info # mybatis sql日志
        RocketmqClient: error
        # Dubbo相关的日志
        org.apache.dubbo.registry.zookeeper.ZookeeperRegistry: error
        org.apache.dubbo.registry.integration.RegistryDirectory: error
        org.apache.dubbo.registry.client.metadata.store.RemoteMetadataServiceImpl: off
        org.apache.dubbo.metadata.store.zookeeper.ZookeeperMetadataReport: off
        org.apache.dubbo.metadata.store.nacos.NacosMetadataReport: off

更详细的教程参考:https://shushi.yuque.com/yoxz76/oio3/deaa1p

分布式支持-事务相关

1、分布式事务解决方案

完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,分布式事务就是为了保证不同资源服务器的数据一致性。典型的分布式事务场景:
1、跨库事务, 补充具体场景
2、微服务拆分带来的跨内部服务;
3、微服务拆分带来的跨外部服务;

2、事务策略

采用微服务架构,需考虑分布式事务问题(即平台各子系统之间的数据一致性)。
1)对于单个系统/模型内部, 比如:库存中心、账户中心等,采用强事务的方式。比如:在扣减库存的时候,库存日志和库存数列的变化在一个事务中,保证两个表的数据同时成功或者失败。
强事务管理采用编码式,Oinone事务管理兼容Spring的事务管理方式;
2)为了提高系统的可用性、可扩展性和性能,对于某些关键业务和数据一致性要求特别高的场景,采用强一致性外,其他的场景建议采用最终一致性的方案;
对于分布式事务采用最终数据一致性,借助可靠消息和 Job 等方式来实现。

2.1 基于MQ的事务消息

Oinone如何支持构建分布式项目

采用最终一致性方案,基于MQ的事务消息的方式。
事务消息的逻辑由发送端 Producer进行保证(消费端无需考虑)。基于MQ事务消息的实现步骤:
1)首先,发送一个事务消息,MQ将消息状态标记为Prepared,注意此时这条消息消费者是无法消费到的。
2)接着,执行业务代码逻辑,可能是一个本地数据库事务操作
3)确认发送消息,这个时候,MQ将消息状态标记为可消费,这个时候消费者,才能真正的保证消费到这条数据。

2.2 基于JOB的补偿

定期校对:业务活动的被动方,根据定时策略,向业务活动主动方查询(主动方提供查询接口),恢复丢失的业务消息。

2.3 数据一致性

  • 对RPC超时和重试机制设计的检查,是否会带来重复数据
  • 对数据幂等、去重机制的设计是否有考虑到
  • 对事务、数据(最终)一致性设计是否有考虑到
  • 数据缓存时,当数据发生变化时,是否有相应的机制保证缓存数据的一致性和有效性

Oinone构建分布式项目一些注意点

参考文档:https://doc.oinone.top/kai-fa-shi-jian/6475.html

Oinone社区 作者:shao原创文章,如若转载,请注明出处:https://doc.oinone.top/kai-fa-shi-jian/5572.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
shao的头像shao数式管理员
上一篇 2024年2月20日 pm6:37
下一篇 2024年2月20日 pm6:46

相关推荐

  • 问题排查调试工具使用手册

    当前端发起对应用的访问时,如果出现错误,那么我们可以通过以下方式进行简易排查,如果排查不出来,则也可以把排查工具给出的信息发送给Oinone官方售后进行进一步分析。本文将通过模拟异常信息,来介绍排查工具,提供了哪些辅助信息帮我们来快速定位问题。 排查工具基础介绍 通过前端页面的 /debug 路由路径访问调试工具的页面,假设我们的前端页面访问地址为http://localhost:6800,那么我们的排查工具请求路径就是 http://localhost:6800/debug排查工具可以帮我们排查前端页面元数据异常和后端接口的异常 排查前端页面元数据 将问题页面浏览器地址栏内 page 后的部分复制到调试工具的 debug 路由后重新发起请求,如图可以看到调试工具展示的信息,可以根据这些信息排查问题。 排查后端接口 后端接口出现问题后,打开(在原页面)浏览器的调试工具,切换到“网络”的标签页,在左侧的历史请求列表中找到需要调试的请求,右键会弹出菜单,点击菜单中的 “复制”,再次展开该菜单,点击二级菜单中的“以 fetch 格式复制”,这样可以复制到调试所需要的信息 2.复制调试信息到“接口调试”标签页内的文本框内,点击“发起请求”按钮获取调试结果 我们可以看到页面展示了该接口的各种调试信息,我们可以据此排查问题。 场景化的排查思路 业务代码中存在代码bug 报错后发起调试请求,我们可以看到,调试工具直接给出了异常抛出的具体代码所在位置,此时再切换到“全部堆栈”下,可以看到是业务类的233行导致的空指针异常,查看代码后分析可得是data.getName().eqauls方法在调用前未做条件判断补全该判断后代码可以正常执行 业务代码中没有直接的错误,异常在平台代码中抛出 报错后发起调试请求可以看到异常不在业务代码内再切换到“全部堆栈”,可以看到具体异常信息,提示core_demo_item表出现了重复的主键,该表是DemoItem模型的我们还可以切换到“sql调试”的标签页,可以看到出错的具体sql语句经过分析可以得知是240行的data.create()�重复创建数据导致的。 三、排查工具无法定位怎么办 当我们通过排查工具还是没有定位到问题的时候,可以通过调试页面的“下载全部调试数据”和“下载调试数据”按钮将调试信息的数据发送给官方售后人员帮助我们定位排查问题。 点击页面最顶部的“下载全部调试数据”按钮,可以下载页面调试数据和接口调试数据点击“调试接口”标签页内的“下载调试数据”按钮,可以下载接口调试数据 四、排查工具细节

    2024年5月21日
    1.4K00
  • Oinone开发实践-业务实现多租户方案

    总体方案 业务项目中,需要隔离的模型自定义增加租户字段进行数据隔离; 参考了Mybatis-Plus插件的TenantSqlParser进行的JPA实现,使用jsqlparser解析并修改SQL; 实现获取当前用户租户ID,SQL增删改查时处理租户字段,实现租户数据的隔离 参考项目: https://github.com/baomidou/mybatis-plus https://github.com/JSQLParser/JSqlParser 具体实现方式 1、业务上定义两个基础抽象模型包含租户字段 定义包含ID的基础抽象模型,且包含租户字段(如:公司编码, 用其他字段作为租户字段也可以,根据实际业务情况灵活修改)。 @Model.model(XXIdModel.MODEL_MODEL) @Model.Advanced(type = ModelTypeEnum.ABSTRACT) @Model(displayName = “带公司CODE的基础ID抽象模型”, summary = “带公司Code的Id模型”) public abstract class XXIdModel extends IdModel { public static final String MODEL_MODEL = “demo.biz.XXIdModel”; @Field.String @Field(displayName = “所属公司编码”, invisible = true, index = true) private String companyCode; } 定义包含Code的基础抽象模型,且包含租户字段(如:公司编码, 用其他字段作为租户字段也可以,根据实际业务情况灵活修改)。 @Model.model(XXCodeModel.MODEL_MODEL) @Model.Advanced(type = ModelTypeEnum.ABSTRACT) @Model(displayName = “带公司CODE的基础Code抽象模型”, summary = “带公司CODE的Code模型”) public abstract class XXCodeModel extends CodeModel { public static final String MODEL_MODEL = “demo.biz.XXCodeModel”; @Field.String @Field(displayName = “所属公司编码”, invisible = true, index = true) private String companyCode; } 2、业务模块的模型需租户隔离的都是继承上面这两个模型; @Model.model(PetPetCompany.MODEL_MODEL) @Model(displayName = “宠物公司”, labelFields = “name”) public class PetPetCompany extends AbstractCompanyCodeModel { public static final String MODEL_MODEL = “demo.PetPetCompany”; @Field.String @Field(displayName = “名称”) private String name; @Field.Text @Field(displayName = “简介”) private String introduction; } 3、自定义扩展Session,Session中设置租户信息 每次请求多把登录用户所属公司编码(companyCode)放到Session中;Session扩展参考:https://doc.oinone.top/oio4/9295.html 4、定义拦截器Interceptor进行数据隔离 数据创建和查询通过拦截器把Session中的中的公司编码(companyCode)设置到隔离字段中;拦截器的java示例代码参考: package pro.shushi.pamirs.demo.core.interceptor; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.ItemsListVisitor; import net.sf.jsqlparser.expression.operators.relational.MultiExpressionList; import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.insert.Insert; import net.sf.jsqlparser.statement.select.*; import net.sf.jsqlparser.statement.update.Update; import net.sf.jsqlparser.statement.values.ValuesStatement; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.mapping.SqlCommandType; import org.apache.ibatis.mapping.SqlSource; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import…

    2024年4月6日
    84200
  • 模型字段之序列化方式

    本文核心是带大家全面了解oinone的序列方式,包括支持的序列化类型、注意点、如果新增客户化序列化方式以及字段默认值的反序列化。 字段序列化方式说明 序列化方式 说明 备注 JSON JSON序列化 主要用于模型相关类型字段的序列化,是@Field.serialize默认选项 DOT 点拼接集合元素 COMMA 逗号拼接集合元素 BIT 按位与,2次幂数求和 非@Field.serialize可选项列表,用于二进制枚举序列化不需要配置,由oinone自动推断 字段序列化方式举例 1、给模型PetItemDetail 增加两个字段:petItemDetails类型为List 和 tags类型为List,并设置为不同的序列化方式,petItemDetails为JSON(缺省就是JSON,可不配),tags为COMMA。2、同时设置 @Field.Advanced(columnDefinition = "varchar(1024)"),防止序列化后存储过长。 @Model.model(PetItem.MODEL_MODEL) @Model(displayName = "宠物商品",summary="宠物商品",labelFields = {"itemName"}) public class PetItem extends AbstractDemoCodeModel{ public static final String MODEL_MODEL="demo.PetItem"; @Field(displayName = "品种") @Field.many2one @Field.Relation(relationFields = {"typeId"},referenceFields = {"id"}) private PetType type; @Field(displayName = "品种类型",invisible = true) private Long typeId; @Field(displayName = "详情", serialize = Field.serialize.JSON, store = NullableBoolEnum.TRUE) @Field.Advanced(columnDefinition = "varchar(1024)") private List<PetItemDetail> petItemDetails; @Field(displayName = "商品标签",serialize = Field.serialize.COMMA,store = NullableBoolEnum.TRUE,multi = true) @Field.Advanced(columnDefinition = "varchar(1024)") private List<String> tags; } 字段序列化注意点 必须使用Field#store属性将字段存储设置为NullableBoolEnum.TRUE。 使用Field#serialize属性指定序列化方式,默认为JSON。 如把PetItemDetail设置为存储模型,须在PetItem的petItemDetails字段上使用Field.Relation#store属性将关联关系存储设置为false。不然会同时存储petItemDetails字段和对应的PetItemDetail表记录 注册自己的序列化器 注册自己的序列化器(实现pro.shushi.pamirs.meta.api.core.orm.serialize.Serializer接口), 如oinone的DOT的序列化方式,用type()方法返回值做匹配,serialize和deserialize分别对应序列化和反序列化方法。 package pro.shushi.pamirs.framework.compute.serialize; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import pro.shushi.pamirs.meta.api.core.orm.serialize.Serializer; import pro.shushi.pamirs.meta.common.constants.CharacterConstants; import pro.shushi.pamirs.meta.enmu.SerializeEnum; import pro.shushi.pamirs.meta.util.TypeUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 点表达式序列生成处理器实现 * @author shushi@shushi.pro * @version 1.0.0 */ @SuppressWarnings("rawtypes") @Slf4j @Component public class DotSerializeProcessor implements Serializer<Object, String> { @Override public String serialize(String ltype, Object value) { if (null == value) { return null; } if (List.class.isAssignableFrom(value.getClass())) { return StringUtils.join((List) value, CharacterConstants.SEPARATOR_DOT); } else { return StringUtils.join(Collections.singletonList(value), CharacterConstants.SEPARATOR_DOT); } } @SuppressWarnings("unchecked") @Override public Object deserialize(String ltype, String ltypeT, String value,…

    2024年5月24日
    1.2K00
  • 项目中常用的 Tools 工具类

    模型拷贝工具类 KryoUtils.get().copy(modelData); ArgUtils.convert(DataReport.MODEL_MODEL, DataDesignerReport.MODEL_MODEL, report); pro.shushi.pamirs.framework.common.utils.ObjectUtils#clone(T) Rsql工具类 RsqlParseHelper.parseRsql2Sql(queryWrapper.getModel(), rsql); RSQLHelper.getRsqlValues(sql.getOriginRsql(), fieldSet); 序列化工具类 后端使用的JSON序列化 JsonUtils.toJSONString(nodes); 前端使用的JSON序列化 PamirsJsonUtils.toJSONString(nodes, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteDateUseDateFormat, SerializerFeature.BrowserCompatible); 生成ID //根据模型生成id Long generate = (Long) Spider.getDefaultExtension(IdGenerator.class).generate(PamirsTableInfo.fetchKeyGenerator(Teacher.MODEL_MODEL)); //生成id Long l = Long.valueOf(UidGeneratorFactory.getCachedUidGenerator().getUID()); 权限相关 // 获取权限路径path AccessResourceInfoSession.getInfo().getOriginPath(); 其他 PamirsSession相关 PamirsSession.isAdmin() # 是否admin用户 PamirsSession.getUserId() # 获取登录用户ID PamirsSession.getRequestVariables() PamirsSession.getContext().getModelCache().get(PetShop.MODEL_MODEL).getTable(); # 获取模型信息 PamirsSession.getContext().getExtendCache(ActionCacheApi.class).get(Teacher.MODEL_MODEL, "importArchivesInfo") # 获取函数信息

    2025年5月8日
    57401

Leave a Reply

登录后才能评论