跨产品之间业务交互如何设计和实现

857 阅读6分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

需求背景

  1. 起初版本,2B产品2C产品交互行为,是通过两者各微服务之间直接的互相调用。比如A(2B产品)->B(2C产品)C(2B产品)->D(2C产品)E(2B产品)->F(2C产品),这些B,D,F都是2C产品内部微服务,A,C,E也都是2B产品内部的微服务。
  2. 目前,2C产品封装单独的微服务FeedApiGateWay提供给2C产品,用于交互。原先的内部微服务,不再提供服务。

分析需求

  1. 2C产品方面来看,这么做是合理的。提供单独FeedApiGateWay微服务,可以收口在一处,方便维护。同时也防止了内部微服务过多的暴露给外部调用,容易造成循环调用。
  2. 2B产品方来看,是很方便的。不用再考虑对方的微服务是哪个,内部逻辑是怎样的,也不用再重复写那段RestTemplate的古老代码。只需要聚焦关注FeedApiGateWay微服务和其接口文档,拿到需要的响应结果即可。

规格文档分析

  1. 2C产品FeedApiGateWay微服务,目前提供3个接口(内容查询、事件落盘、数据上报),每个接口都有独立的请求和响应。未来,该微服务还会增加接口,用于2B产品2C产品之间的交互。
  2. 而对于2B产品来讲,凡是调用2C产品FeedApiGateWay微服务接口的地方,都要写一套https的调用逻辑+请求包装、响应解析逻辑。当前3个接口意味着至少有3处要写类似的一套逻辑,太冗余了;另外,从以往经验来看,随着版本迭代,千人千面的写法也会造成代码风格各异,后期不易维护。 那有没有办法从技术角度规范风格,且收编一处呢?

我想到,内部封装一个sdk提供给业务微服务对接,该sdk命名为2B-feedapi,职责为对接2C产品FeedApiGateWay微服务。 2B-feedapi的作用 3. 该2B-feedapi的sdk应该有以下几个特征:

  • 低耦合,业务微服务容易理解和集成,代码书写量小;跨产品之间业务交互如何设计和实现
  • 高聚合,功能相对独立;
  • 开放原则,让业务微服务可以定制个性化逻辑,且易扩展以应对未来2C产品FeedApiGateWay增加接口 ;
  • 封闭原则,核心算法步骤,业务微服务不能染指。

如何设计SDK

决定做2B-feedapi了,怎么做才能满足功能要求,且符合“开放、封闭、高聚合、低耦合”特征呢?这个才是设计的难点。

同样的,磨刀不误砍柴工,做之前先来波分析。

  1. 2C产品提供的3个接口,各接口的请求和响应不一样。意味着,业务方的原始请求和body响应也不一样。那么2B-feedapi就不应该感知具体的请求和响应是什么样,应该交由业务方决定。 所以,对于2B-feedapi来说,请求和响应必须是泛型的。

  2. 3个接口除了请求body外,还含有headers头信息,各接口的头信息字段也不一样。所以决定用Map<String,String>的方式存储headers,业务方负责传入,交由2B-feedapi整合编排。

  3. 有了上面2点分析,2B-feedapi要有个抽象类,该抽象类,需要提供至少3个方法,

    • 根据业务方的原始请求,构造https请求;
    • 发起post调用2C产品FeedApiGateWay的请求;
    • 根据业务方的响应类型,从原生https响应里解析出body响应。

    这些步骤是所有接口都必须要做的,由此就想到可以使用模板模式来书写此sdk的骨架。

类图设计

2B-feedapi类图设计 从图中可以看出,业务方只需要感知FeedHttpSender1个类,即门面模式,其他复杂的动作全部交由2B-feedapi这个sdk里面的3个接口类来完成。

包及类设计

遵循springMVC设计思路,“控制层-业务层-模型层”

包设计

  1. bussiness包,是业务逻辑处理包,分interface和impl两部分。比如,多线程处理,补偿文件记录等都可以放在xxxServiceImpl.java文件中去实现,xxx的命名也建议合乎业务场景。 bussiness包细节

    如代码所示,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);
        }
    }
......
}
  1. invoker包,对应2C产品的3个接口的实际实现,需要完成AbstractFeedApiInvoker抽象类中抽象方法的具体实现。包括构造HttpRequest,和获取具体body响应。

  2. model包,通俗易懂,就是放一些模型。

  3. sender包,这个很重要,类似于控制层Controller。业务方调用2B-feedapi这个sdk的能力时,都是从FeedHttpSender类作为入口,算是个门面。由它真正的向业务方暴露接口能力。

sender 5. AbstractFeedApiInvoker,此抽象类就是该sdk的灵魂所在。它泛型了请求和响应,业务侧可以自定义ReqRsp,给了业务侧个性化的场地。同时,其也是真正地向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());

业务场景——落盘接口关键实现

  1. 客户端每一次事件落盘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);
    }
}
  1. 怎么保证幂等?即若调用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");
            }
        }
    }
}

总结

  1. 写实际业务代码时,与平时大家刷leetcode不同,一定要在动手之前充分分析需求,理解清楚需求后,多考虑几种实现需求的方案,当然想出的方案与最后真正落地方案之间会有差距。但是只要做到平时多总结,积少成多,逐渐就是内化成自己的一套逻辑。
  2. 对于本文,巧用模板模式,定义了2B产品2C产品交互算法骨架,而将算法中的一些与业务场景挂钩的步骤(封装HttpRequest、编排业务、rsp响应等)延迟到子类中,使得子类可以不改变该算法结构的情况下重定义这些特定步骤,符合”开闭“原则。同时,在父类中提取了公共的部分代码,便于代码复用。另外,运用门面模式,为2B_feedapi这sdk提供一个简单的对外接口,业务侧轻松集成使用。
  3. 业务场景——事件落盘接口代码,适用2B_feedapi的sdk, 充分考虑业务场景异步化的要求,使用线程池技术。同时,实现事件落盘失败的重试+补偿逻辑,异常分支考虑全面。

文中产品具体名称、代码关键词等涉密部分均已化名,勿对号入座。