中台的核心是提供了各种可复用的能力,提供给上游的多个业务,减少他们系统开发的时间周期和建设成本,进行快速试错。
解决上述问题的方式:基于业务身份的配置化实现。
一、业务身份
首先来看看业务身份的定义:系统唯一标识一个业务或者一个场景的标志,可以用枚举或者字符串表示。
举几个例子方便理解:
用户中台支撑电商 app 和买车 app 登录场景,对于用户中台来说,电商 app 和买车 app 各自对应一个业务身份,"shopping"和"buyCar"。
电子合同中台支撑理财 app 和买车 app 里的合同签署场景,用户在理财 app 购买理财产品需要签署合同,贷款需要签署合同,在买车 app 买了一辆车需要签署车辆买卖合同。对于电子合同中台来说,这里有 3 个业务身份
理财合同,业务身份为:"finance_products"
贷款合同,业务身份为:"captial_loan"
车辆买卖合同,业务身份为:"car_sale"
业务身份一般有 2 种实现方式
枚举
简单在代码中定义一个枚举,例如对于中户中台来说
public enum BizTypeEnum {
/**
* 电商
*/
SHOPPING("shopping", "电商业务"),
/**
* 买车业务
*/
BUYER_CAR("buyerCar", "买车业务");
/**
* 业务身份code
*/
private String code;
/**
* 业务身份中文名称
*/
private String name;
}
复制代码
优点:实现简单
缺点:
关系型数据库
CREATE TABLE `biz_type` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`code` varchar(255) NOT NULL DEFAULT '' COMMENT '业务身份code',
`name` varchar(255) NOT NULL DEFAULT '' COMMENT '业务身份中文名称',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户中台业务身份';
复制代码
推荐使用基于数据库的业务身份管理
优点:
新增业务身份不需要开发,可以实时生效。
新增配置项的时候,新增字段即可。
缺点:需要一定的开发工作,最好有对应的后台管理页面。
二、扩展点
下面介绍几种扩展点的实现方式,各个方式没有最好,根据业务场景选择最合适的。
业务开关
一般用于是否需要某个功能或者策略,例如 创建成功后是否生成 MQ、是否测试数据等。
CREATE TABLE `biz_type` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`code` varchar(255) NOT NULL DEFAULT '' COMMENT '业务身份code',
`name` varchar(255) NOT NULL DEFAULT '' COMMENT '业务身份中文名称',
`is_test` tinyint(4) DEFAULT '0' COMMENT '是否测试',
`send_mq` tinyint(11) DEFAULT '1' COMMENT '是否发送MQ',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户中台业务身份';
复制代码
代码中实现逻辑:根据 code 查询 biz_type 表,如果 send_mq 的值为 true,则不发送执行 MQ 发送的逻辑。
策略
这里的策略实现有两种方式,以电子合同生成合同编号的逻辑来举例,不同业务身份的合同编号期望的格式不一样:
1. 不同业务定制各自的实现策略
public interface ContractNoStrategy {
String contractNo(Integer uniqId);
}
public class FinanceProductContractNo implements ContractNoStrategy {
public String contractNo(Integer uniqId) {
return System.currentTimeMillis() + uniqId.toString();
}
}
public class CarSaleContractNo implements ContractNoStrategy {
public String contractNo(Integer uniqId) {
return MD5Utils.md5(uniqId.toString());
}
}
// 初始化逻辑
Map<String, ContractNoStrategy> strategyMap = Maps.newConcurrentMap();
strategyMap.put("car_sale", new CarSaleContractNo());
strategyMap.put("finance_products", new FinanceProductContractNo());
// 签署合同时,生成合同编号
String bizCode = "car_sale"; // 参数传入的业务身份
ContractNoStrategy strategy = strategyMap.get(bizCode);
// 参数中的12345只是举例,这里可以是合同的自增ID
String contractNo = strategy.contractNo(12345);
复制代码
优点:不同业务身份有各自的合同编号生成规则,互不影响
缺点:
2. 不同业务选择已抽象的策略
中台预先提供好不同的策略,业务从已有的策略中选择一个,如果没有则从业务需求中抽象出一个策略。还是以上文提到的合同编号场景,合同号生成的 2 种实现可以分别抽象为:
在数据库中业务身份表中新增一个字段表示对应的合同编号生成规则
CREATE TABLE `biz_type` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`code` varchar(255) NOT NULL DEFAULT '' COMMENT '业务身份code',
`name` varchar(255) NOT NULL DEFAULT '' COMMENT '业务身份中文名称',
`contract_no_strategy` varchar(255) DEFAULT NULL COMMENT '合同号生成策略',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户中台业务身份';
复制代码
实现是 MD5 编号生成策略和时间戳生成策略
public interface ContractNoStrategy {
String contractNo(Integer uniqId);
}
public class MD5ContractNo implements ContractNoStrategy {
public String contractNo(Integer uniqId) {
return MD5Utils.md5(uniqId.toString());
}
}
public class CurrentTimeContractNo implements ContractNoStrategy {
public String contractNo(Integer uniqId) {
return System.currentTimeMillis() + uniqId.toString();
}
}
// 初始化逻辑
Map<String, ContractNoStrategy> strategyMap = Maps.newConcurrentMap();
strategyMap.put("md5", new MD5ContractNo());
strategyMap.put("currentTime", new CurrentTimeContractNo());
// 合同生成逻辑
String contractNoStrategy = bizTypeDO.getSontractNoStrategy();
ContractNoStrategy strategy = strategyMap.get(contractNoStrategy);
// 执行获取
String contractNo = strategy.contractNo(12345);
复制代码
优点:
功能更加内聚,没有为业务定制开发的功能代码
新增业务身份时选择已有策略,不需要开发可生效
缺点:
业务定制特别强的需求,很难抽象为通用的策略
回调
所谓回调,就是程序 C 调用程序 S 中的某个函数 A,然后 S 又在某个时候反过来调用 C 中的某个函数 B,对于 C 来说,这个 B 便叫做回调函数。
我们日常中经常会用到回调函数,“回调”这种设计模式同时也可以应用到中台的设计中。对应到中台来说,某业务系统调用了中台的接口,中台在接口的实现里或者在后续的某个时候,调用业务系统中的接口,就是说这里的回调指的是 RPC 场景下的调用。
RPC 方法回调
有些 RPC 框架提供了类似本地回调的功能,以 dubbo 为例
CallbackService 是服务提供者的接口,Dubbo 将基于长连接生成反向代理,这样就可以从服务器端调用客户端的逻辑,会执行下面代码中第 8 行:System.out.println("callback1:" + msg);
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("classpath:consumer.xml");
context.start();
CallbackService callbackService = (CallbackService) context.getBean("callbackService");
callbackService.addListener("foo.bar", new CallbackListener(){
public void changed(String msg) {
System.out.println("callback1:" + msg);
}
});
复制代码
详细参考 dubbo 官方文档http://dubbo.apache.org/zh/docs/v2.7/user/examples/callback-parameter/
协议回调
在对接第三方支付时,支付结果通常是以 HTTP 接口的形式告知接入方,HTTP 接口的参数和返回值都是由第三方支付公司确定。
对于上文提到的合同编号场景,中台来定义接口参数和返回值,参数:uniqId,返回值:contractNo
有 HTTP 和 RPC 两种实现方式
HTTP 方式
中台通过 HTTP 的方式回调到业务方,参数和返回值有了,中台需要知道 HTTP 的 url,可以把对应的 url 保存到业务身份表中
CREATE TABLE `biz_type` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`code` varchar(255) NOT NULL DEFAULT '' COMMENT '业务身份code',
`name` varchar(255) NOT NULL DEFAULT '' COMMENT '业务身份中文名称',
`is_test` tinyint(4) DEFAULT '0' COMMENT '是否测试',
`send_mq` tinyint(11) DEFAULT '1' COMMENT '是否发送MQ',
`contract_no_url` varchar(255) DEFAULT NULL COMMENT '合同号获取的回调URL',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户中台业务身份';
复制代码
例如 对于买车 app 生成合同时会调用这个 HTTP 接口 http://carSale.xxx.com/generateContractNo?uniqId=12345
优点:实现简单
缺点:RPC 的场景下,引入了 HTTP 调用
RPC 方式
以 dubbo 为例,中台提供 jar 包,定义接口:
public interface ContractNoService {
String contractNo(Integer uniqId);
}
复制代码
买车 app 实现这个接口,并暴露服务出去,version 标签中设置约定好的业务身份 code
public class CarSaleContractNo implements ContractNoStrategy {
public String contractNo(Integer uniqId) {
return MD5Utils.md5(uniqId.toString());
}
}
复制代码
<dubbo:service interface="com.xxx.ContractNoService" ref="contractNoService" version="car_sale"/>
复制代码
对于同一个 ContractNoService,会有多个服务提供方,中台代码里编程式动态调用服务,通过 dubbo 的 version 标签来找到对应的服务提供者。
ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>();
// 弱类型接口名
reference.setInterface("com.xxx.ContractNoService");
reference.setVersion("car_sale");
// 声明为泛化接口
reference.setGeneric(true);
// 用org.apache.dubbo.rpc.service.GenericService可以替代所有接口引用
GenericService genericService = reference.get();
// 基本类型以及Date,List,Map等不需要转换,直接调用
Object result = genericService.$invoke("contractNo", new Integer[] {"java.lang.Integer"});
复制代码
优点:
缺点:
插件包注册
回调的方式是在业务方执行代码逻辑,
中台台提供插件包注册机制,实现业务方插件包在运行期的注册。业务代码只允许存在于插件包中,与平台代码严格分离,通过二方包的方式,提供给容器加载。
URL url1 = new URL("file:D:/jarload/test.jar");
URLClassLoader myClassLoader1 = new URLClassLoader(new URL[] { url1 }, Thread.currentThread().getContextClassLoader());
Class<?> myClass1 = myClassLoader1.loadClass("com.java.jarloader.TestAction");
AbstractAction action1 = (AbstractAction) myClass1.newInstance();
action1.action();
复制代码
阿里巴巴 TMF2.0 中使用了插件包的机制,详细参见 http://www.ryu.xin/2018/04/13/Yunqi-Alibaba-TMF2-0-Introduction/
优点:性能高
缺点:
流程编排
中台服务中的业务步骤进行抽象沉淀形成固定的模块,不同业务基于自身的业务场景,对现有的模块进行重组,从而满足自己的需求。
例如 电商履约流程中包含:打包、发票生成、物流运输、签收、货到付款等等很多环境,自营仓库需要发票,商家业务则没有这个限制。不同的业务线需要的步骤也是不同的,把这些步骤都抽象成标准的步骤,各业务进行拼装组合。例如
体验业务线:物流运输、签收、货到付款、发票生成。
自营商城线:打包、发票生成、物流运输。
具体实现方式可以参考
分布式、伸缩、高可靠的异步执行业务逻辑,工具比较丰富,同时提供了可视化 UI
来自 netflix 的为微服务编排引擎,支持的功能很丰富,同时文档也比较全
实际上是在工作流引擎的基础上衍生出来的,设计很灵活,不需要依赖后端的存储,支持复制、分片(借鉴了 kafka)
scala 开发的微服务调度框架
aws 的云服务
DSL 脚本
DSL 全称为 domain-specific language,指的是专注于某个应用程序领域的计算机语言。对于需要非常灵活扩展的地方,可以通过 DSL 脚本满足一定的需求,例如 复杂的结算规则,根据商家、业务等区别,适用不同的规则
def detail_price alias '详情价格'
def transport_fee alias '运输费'
def check_fee alias '检测费'
def service_fee alias '服务费'
if(详情价格>10000){
return (详情价格+运输费+检测费+服务费);
}else{
Return (详情价格+(运输费+检测费+服务费)*0.8);
}
复制代码
总结:
框架在架构设计上遵循一个重要的设计原则叫“依赖倒转原则”,依赖倒转原则是高层模块不能依赖低层模块,它们应该共同依赖一个抽象,这个抽象由高层模块定义,由低层模块实现。
中台的设计过程中通过策略、回调、插件包、流程编排和 DSL 脚本等方式,实现业务的定制化需求。
评论