本文已参与「新人创作礼」活动,一起开启掘金创作之路
需求背景
- 起初版本,2B产品与2C产品交互行为,是通过两者各微服务之间直接的互相调用。比如
A(2B产品)->B(2C产品),C(2B产品)->D(2C产品),E(2B产品)->F(2C产品),这些B,D,F都是2C产品内部微服务,A,C,E也都是2B产品内部的微服务。 - 目前,2C产品封装单独的微服务FeedApiGateWay提供给2C产品,用于交互。原先的内部微服务,不再提供服务。
分析需求
- 从2C产品方面来看,这么做是合理的。提供单独FeedApiGateWay微服务,可以收口在一处,方便维护。同时也防止了内部微服务过多的暴露给外部调用,容易造成循环调用。
- 从2B产品方来看,是很方便的。不用再考虑对方的微服务是哪个,内部逻辑是怎样的,也不用再重复写那段
RestTemplate的古老代码。只需要聚焦关注FeedApiGateWay微服务和其接口文档,拿到需要的响应结果即可。
规格文档分析
- 2C产品的FeedApiGateWay微服务,目前提供3个接口(内容查询、事件落盘、数据上报),每个接口都有独立的请求和响应。未来,该微服务还会增加接口,用于2B产品与2C产品之间的交互。
- 而对于2B产品来讲,凡是调用2C产品的FeedApiGateWay微服务接口的地方,都要写一套https的调用逻辑+请求包装、响应解析逻辑。当前3个接口意味着至少有3处要写类似的一套逻辑,太冗余了;另外,从以往经验来看,随着版本迭代,千人千面的写法也会造成代码风格各异,后期不易维护。 那有没有办法从技术角度规范风格,且收编一处呢?
我想到,内部封装一个sdk提供给业务微服务对接,该sdk命名为2B-feedapi,职责为对接2C产品的FeedApiGateWay微服务。
3. 该2B-feedapi的sdk应该有以下几个特征:
- 低耦合,业务微服务容易理解和集成,代码书写量小;跨产品之间业务交互如何设计和实现
- 高聚合,功能相对独立;
- 开放原则,让业务微服务可以定制个性化逻辑,且易扩展以应对未来2C产品的
FeedApiGateWay增加接口 ; - 封闭原则,核心算法步骤,业务微服务不能染指。
如何设计SDK
决定做2B-feedapi了,怎么做才能满足功能要求,且符合“开放、封闭、高聚合、低耦合”特征呢?这个才是设计的难点。
同样的,磨刀不误砍柴工,做之前先来波分析。
-
2C产品提供的3个接口,各接口的请求和响应不一样。意味着,业务方的原始请求和body响应也不一样。那么
2B-feedapi就不应该感知具体的请求和响应是什么样,应该交由业务方决定。 所以,对于2B-feedapi来说,请求和响应必须是泛型的。 -
3个接口除了请求body外,还含有headers头信息,各接口的头信息字段也不一样。所以决定用
Map<String,String>的方式存储headers,业务方负责传入,交由2B-feedapi整合编排。 -
有了上面2点分析,2B-feedapi要有个抽象类,该抽象类,需要提供至少3个方法,
- 根据业务方的原始请求,构造https请求;
- 发起post调用2C产品的FeedApiGateWay的请求;
- 根据业务方的响应类型,从原生https响应里解析出body响应。
这些步骤是所有接口都必须要做的,由此就想到可以使用模板模式来书写此sdk的骨架。
类图设计
从图中可以看出,业务方只需要感知
FeedHttpSender1个类,即门面模式,其他复杂的动作全部交由2B-feedapi这个sdk里面的3个接口类来完成。
包及类设计
遵循springMVC设计思路,“控制层-业务层-模型层”
-
bussiness包,是业务逻辑处理包,分interface和impl两部分。比如,多线程处理,补偿文件记录等都可以放在
xxxServiceImpl.java文件中去实现,xxx的命名也建议合乎业务场景。如代码所示,
EventPersistenceService就是代表着事件落盘场景的interface定义,
/**
* 话单落盘的服务层接口类
*
* @author 桐言桐语
* @since 2021-09-11
*/
public interface EventPersistenceService {
void saveEvent(EventHeaderPersisReq eventHeaderPersisReq, CompensateConfig compensateConfig);
}
其EventPersistenceServiceImpl就是此接口的具体实现。
/**
* 话单落盘的服务层实现类
*
* @author 桐言桐语
* @since 2021-09-11
*/
@Service
public class EventPersistenceServiceImpl implements EventPersistenceService {
......
@Override
public void saveEvent(EventHeaderPersisReq eventHeaderPersisReq, CompensateConfig compensateConfig) {
try {
excutor.execute(new SaveEventCallable(eventHeaderPersisReq, compensateConfig));
} catch (RejectedExecutionException e) {
LogManager.getDebugLog()
.error("submit SaveEventCallable Thread failed! eventPersistenceReq=[{}]", eventHeaderPersisReq, e);
}
}
......
}
-
invoker包,对应2C产品的3个接口的实际实现,需要完成
AbstractFeedApiInvoker抽象类中抽象方法的具体实现。包括构造HttpRequest,和获取具体body响应。 -
model包,通俗易懂,就是放一些模型。
-
sender包,这个很重要,类似于控制层Controller。业务方调用2B-feedapi这个sdk的能力时,都是从
FeedHttpSender类作为入口,算是个门面。由它真正的向业务方暴露接口能力。
5.
AbstractFeedApiInvoker,此抽象类就是该sdk的灵魂所在。它泛型了请求和响应,业务侧可以自定义Req和Rsp,给了业务侧个性化的场地。同时,其也是真正地向2C产品的FeedApiGateWay发起https post调用。
public abstract class AbstractFeedApiInvoker<ReqModel extends InvokeRequest<Req>, Req, RespImpl extends Resp> {
...
}
业务方如何调用此SDK
有了上述sdk做得诸多的铺垫,业务侧想要调用2C产品的FeedApiGateWay的接口,就很简单了,一行代码搞定。
- 先注入
FeedHttpSender类; - 然后业务方直接使用
FeedHttpSender提供的对应接口。 如业务侧想要调用2C产品的FeedApiGateWay的事件落盘接口,直接一行完成:
@Autowired
private FeedHttpSender FeedHttpSender;
FeedHttpSender.saveEvent(eventHeaderPersisReq, EventUtils.buildCompensateConfig());
业务场景——落盘接口关键实现
- 客户端每一次事件落盘
saveEvent(...)请求,需要立即响应。而落盘动作调用链较长,流经过2B产品,2C产品的FeedApiGateWay微服务,EventProcessor微服务直到最终的HDFS落盘,花费时间较长;且事件落盘成功与否,客户端并不是需要立即感知。 所以这里设计成异步的,即客户端请求落盘后,立即响应落盘成功;由异步线程去真正完成落盘调用。
/**
* 初始化线程池
*/
@PostConstruct
public void initExecutor() {
RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(queueSize);
excutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, workQueue,
new SaveEventThreadFactory(), handler);
excutor.prestartAllCoreThreads();
}
@Override
public void saveEvent(EventHeaderPersisReq eventHeaderPersisReq, CompensateConfig compensateConfig) {
try {
excutor.execute(new SaveEventCallable(eventHeaderPersisReq, compensateConfig));
} catch (RejectedExecutionException e) {
LogManager.getDebugLog()
.error("submit SaveEventCallable Thread failed! eventPersistenceReq=[{}]", eventHeaderPersisReq, e);
}
}
- 怎么保证幂等?即若调用FeedApiGateWay失败,有可能是网络抖动引发的失败,需要间隔0/4/8秒重试3次;若重试后还是失败,则记录错单文件,期待定时任务补偿重试,达到最终落盘成功,幂等的效果。
@Override
public void run() {
for (int i = 0; i < MAX_TRY_TIMES; i++) {
// 获取失败后需要等待一段时间, 然后再重试
SleepUtil.sleepSeconds(i * PER_TRY_MILLSECOND);
try {
InvokeRequest<EventPersistenceReq> invokeRequest = new InvokeRequest<>();
invokeRequest.setHeaders(eventHeaderPersisReq.getHeaders());
invokeRequest.setRequest(eventHeaderPersisReq.getEventPersistenceReq());
EventPersistenceRsp response =
eventPersistenceInvoker.invokePost(invokeRequest, EventPersistenceRsp.class);
LogManager.getDebugLog()
.info("end ivokePost saveEvent,requestId=[{}],response={}",
eventHeaderPersisReq.getEventPersistenceReq().getRequestId(), response);
return;
} catch (Exception e) {
LogManager.getDebugLog()
.error("ivokePost saveEvent Thread failed! eventPersistenceReq=[{}]", eventHeaderPersisReq, e);
// 达到最大重试次数,则记录错单文件
if (i == (MAX_TRY_TIMES - 1)) {
// 记录补偿日志
compensateService.recordLog(compensateConfig.getMicroserviceName(), compensateConfig.getSchemaId(),
compensateConfig.getMethodName(), JSON.toJSONString(eventHeaderPersisReq),
EventHeaderPersisReq.class);
LogManager.getDebugLog().debug("SaveEventCallable compensateService recordLog finished");
}
}
}
}
总结
- 写实际业务代码时,与平时大家刷leetcode不同,一定要在动手之前充分分析需求,理解清楚需求后,多考虑几种实现需求的方案,当然想出的方案与最后真正落地方案之间会有差距。但是只要做到平时多总结,积少成多,逐渐就是内化成自己的一套逻辑。
- 对于本文,巧用模板模式,定义了2B产品与2C产品交互算法骨架,而将算法中的一些与业务场景挂钩的步骤(封装
HttpRequest、编排业务、rsp响应等)延迟到子类中,使得子类可以不改变该算法结构的情况下重定义这些特定步骤,符合”开闭“原则。同时,在父类中提取了公共的部分代码,便于代码复用。另外,运用门面模式,为2B_feedapi这sdk提供一个简单的对外接口,业务侧轻松集成使用。 - 业务场景——事件落盘接口代码,适用2B_feedapi的sdk, 充分考虑业务场景异步化的要求,使用线程池技术。同时,实现事件落盘失败的重试+补偿逻辑,异常分支考虑全面。
文中产品具体名称、代码关键词等涉密部分均已化名,勿对号入座。