不论什么一个业务系统都有它的核心业务逻辑。在非常多情况下核心业务逻辑通常都有一些特点:牵扯面广,依赖关系多,流程复杂等。
接下来。我就近期公司内一个真实的案例来简单谈谈对于一个长耗时的任务或者业务逻辑有哪些经常使用的优化手段。
案例简述
要说的这个系统案例是一个统一通信平台,它给客户提供收发短信、email、RTX消息等功能。
不难想象最核心的业务当然是借助这个平台间接通过这些通讯 (sms/email/rtx)网关来收发消息。当然现实的场景远没这么简单,还有一些外围业务也环绕此展开:
- 权限管理
- 流量控制
- 黑名单策略
- 成员管理
- 网关对接
- ...
- 请求处理时间超长,client长时间堵塞处于假死状态
- 短信网关回执的异步写入产生混乱,不是时序问题就是出现处理异常
- 长短信计算错误
梳理业务&模块拆分
由于这是一个老项目,经手过的维护人员非常多,加之没有规范并且维护人员技术水准以及对项目的了解程度各不同样,导致项目中的业务流程非常混乱,到处都是if/else、类似逻辑的方法反复定义、特例配置遍布整个项目。仅仅有拆分清楚了,剥离与独立部署才干成为可能。
梳理的方式有非常大,应该是多重手段并用的结合。经常使用的手段有:
- 慷慨法化小
- 提取反复逻辑
- 又一次推断方法的归属对象
- 又一次推断逻辑的归属层次
- 避免逻辑层次的反向依赖
- ....
分布式组件
这个案例中,还有一个能够单独实现的组件是网关对接器:gateway-adapter。
它的作用是为了适配网关接口。以及处理网关回执。这里所谓的分布式组件,能够是物理上的分布式(比方独立的物理节点)。也能够是逻辑上的分布式(比方仅仅是一个独立的JVM进程)。独立节点与否,能够參照节点资源的利用率,但仅仅要跑在独立的JVM进程上,就能够保证单个服务的稳定性。分布式的组件通常都是基于事件驱动的。它们之间的通信能够基于消息中间件。
- 文件解析、验证组件
- 发送消息业务逻辑处理组件
- 消息发送网关适配组件
缓存查询数据
而使用基于Key-Value的内存缓存。则会对于数据库以及应用程序作尽量少的修改,仅仅须要对关键业务增加一些对缓存訪问/处理的代码。但缓存的使用也会带来数据的可见性、一致性问题。这须要非常好的刷新、同步机制。
并发&多线程
这些组件共享数据库,彼此之间并不产生依赖,这些服务也能够看作是任务处理器。
至于分组的參考值,这是个权衡值,它既须要保证吞吐量。还要保证单个处理线程在时间同意的范围内重回线程池。从而避免线程饥饿。
context&pipeline&filter
它能够用于拆分&重组&串联业务流程。拆分流程的优点显而易见。你甚至能够基于一定的策略,动态载入或卸载一个filter(利用classloader),你也能够对后来修改某个filter的逻辑带来的影响最小化(比方对于依据号码反查不到工号,有些用户选择的处理方案是中止发送。有些用户选择的方案是继续发送。这种修改将会被限制到某个特定的filter内,对外部全然透明)。
同步请求异步化
比方。就这个业务而言,我们就能够定义例如以下用于反映处理进度的数据结构:
组件交互
优化后处理流程
(1)web服务端接到“excel自己定义批量发送”请求后。简单得校验一下格式,将存放发送目标的excel直接保存到服务器的磁盘上,同一时候给同样宿主在该server上的file-parser 组件发送一条表示“有任务到达”的消息。并附带有要发送的消息内容以及发起本次请求的员工信息,在数据库中创建一条请求记录并生成一个唯一的requestID作为处理的批次号(它用于备案以及作分布式处理的日志追踪,字段大致例如以下所看到的),然后该http请求便迅速响应给client。但仅仅给出一个状态码:表示请求已被受理,正在处理中。
(2)这时client首先须要有一点小改进:在页面的模板或者iframe(假设使用的话)区域。展示一个通知提示区(为的是不断跟踪处理状态),须要这个展示区的目的是它们是全局的,不会影响内容页切换。
(3)上面的请求一旦被返回。就能够在模板中採用ajax请求来以poll或push的方式获取处理状态。採用哪种方式依据须要获知发送状态的精确程度而定。假设须要精确的话,可採用poll模式。
(4)回到服务端。excel解析服务会对文件进行解析,提取出里面发送目标的email或手机号。
(5)与此同一时候,它会在消息内容表中插入要发送的消息内容,并提取出该记录的ID作为对消息内容的引用
(6)接下来它会将要发送的号码拆分为若干组,并封装入若干个传输消息内(此处的消息指的是组件之间传输的消息。而非发送消息)。
同一时候会附带发送消息内容的ID以及批次号。
将这些传输消息发送到专门用于处理发送服务的business filter所listen的队列上
(7)business filter是一个分布式组件,用于接收文件解析服务发来的分组传输消息。收到传输消息后解析。然后对每一个传输消息(内部包括已分好组的若干个发送目标)。启用一个独立的线程进行并行处理。
(8)对每一个线程而言(也能够说对每一个传输消息而言),将这个传输消息按发送目标拆分成一个个的子发送消息(以下提及的发送消息都指代这种子发送消息)。将发送消息对象包装进一个上下文对象中(Context此处的意义跟Transfer Object类似。在J2EE中也称之为Value Object)。然后这个发送消息将要流过一个pipeline的filter-chain,每一个filter都是一个子业务的封装,当中包括比方:
- 成员验证:比方要发送的号码是不是本系统的用户
- 流量控制:当前是否发送部门的流量已经超标
- 策略校验:是否满足用户的时间策略以及接受策略
- 内容筛选器
- 内容扩充器
(9)由于这些用filter包装起来的业务,大都在运行——查询&校验的动作,所以对这些业务须要查询的数据集构建合适的缓存,将能够有效得提升处理速度,当然这当中也不可避免会有部分的写操作。
由于这里採用多线程并发处理。因此对竞争资源的写须要保护机制,通常通过同步来保证数据的一致性。
(10)filter进行到chain的最后一步,会依据中间filter处理的状态来推断终于这条消息是否满足发送条件:
- 假设不满足:
- 记录错误/失败原因。
- 更新批次号为当前RequestID的那条记录的相关字段;处理结束
- 假设满足:
- 记录相关信息到消息相应关系表,这里须要的非常多信息都是通过上面的内容扩充器filter进行填充的
- 在数据库的下行队列表中创建一条该消息的发送记录。便于收到回执后改写消息的发送状态,并将这条记录的ID追加到消息中
- 将消息发送到gateway-adapter相应的队列
- 更新批次号为当前RequestID的那条记录的已投递数字段;处理结束
(11)上面将消息发送到gateway-adapter服务相应的队列而不是将其直接发送到网关相应的队列是由于在消息发送给网关之前还有一些问题须要处理:
- 通常网关会是一个独立并且“标准”的service provider,它会对接非常多系统。这时为了不让消息的格式过于混乱,它定义了满足它需求的标准格式。让所有服务使用者来适配它。而这个网关对接服务存在的目的就是为了适配网关的消息格式
- 而对接服务存在的还有一个理由是:依据网关发回的回执来更新数据库中消息的状态:
- 相应于下行消息表中当前messageId的消息状态更新
- 相应于当前requestID的发送成功数/发送失败数字段的更新
(12)对每一条消息进行处理,从而使得不同状态的累加数等于总记录数
以上通过将长耗时的同步任务异步化处理的优化,不仅使得消息从串行同步处理变成了并行异步处理。并且改善了用户体验,在发送的过程中,点击错误日志或者发送过的消息状态就能够实时看到部分处理的结果。