单个请求
- 状态机/幂等/分布式锁设计成本低,实现简单,原生就是原子的不需要考虑原子性
批量请求
需要各个依赖节点均支持批量请求,需要考虑下面这些场景
- 状态机,批量请求混合状态数据场景
- 幂等,多个字段组合幂等场景实现成本高,如何查询DB,批量查询会存在很多脏数据
- 原子性,批量请求是否需要支持原子性
- 分布式锁,list类型如何锁?锁第一条则存在ABA问题,锁多条成本高,接口可用性会随批量操作数量增大而大大降低。如果存在batchInsert,以及单条select幂等场景,则只能锁多条。例如:邮件批量发送时批量写入日志,实际底层是单个用户遍历发送邮件,幂等是基于单个用户幂等
复杂场景的批量更新问题
5. 循环单个更新请求,要控制事务的大小
6. 自定义sql,要控制sql语句大小
<update id="batchUpdateStatusErrorCode" parameterType="java.util.List">
update message_send_log
<trim prefix="set" suffixOverrides=",">
<trim prefix="status = case" suffix="else status end, send_times = send_times+1,">
when 1=2 then status
<foreach collection="list" item="item" index="index">
<if test="item.status !=null">
when id=#{item.id} and db_create_time=#{item.dbCreateTime} then #{item.status}
</if>
</foreach>
</trim>
<trim prefix="error_code = case" suffix="else error_code end,">
when 1=2 then error_code
<foreach collection="list" item="item" index="index">
<if test="item.errorCode !=null">
when id=#{item.id} and db_create_time=#{item.dbCreateTime} then #{item.errorCode}
</if>
</foreach>
</trim>
</trim>
where id in
<foreach collection="list" index="index" item="item" separator="," open="(" close=")">
#{item.id}
</foreach>
and msg_id = #{msgId} and db_create_time between #{start} and #{end}
</update>
同步请求
- 不需要考虑乱序问题,上下游不需要额外的交互,可以同步拿到下游状态
- 实时性强
异步请求
特点
- 消息乱序,乱序情况下覆盖写问题,维护状态机,或无状态请求可以增加版本号
- 上下游交互请求与响应匹配,维护请求方唯一属性,便于上游请求超时可以反查状态判断请求实际状态是否已到达且处理成功
- 延迟不可控
实现方式
线程池
将请求放入线程池异步请求子模块。下游处理超时,上游线程池堆积满以后如何处理?是否允许线程池有队列,如果有重启丢弃问题处理
消息
上游发消息,子模块监听。无法感知下游状态,需要上有提供请求唯一标识,下游维护反查接口
DB+定时任务
伪异步设计
- 接口同步或异步将请求写入DB
- 定时任务轮询DB异步处理请求
状态机(FSM “finite state machine”)
两类状态机
- 若输出只和状态有关而与输入无关,则称为Moore状态机。
- 输出不仅和状态有关而且和输入有关系,则称为Mealy状态机。
状态机思考
- 状态机不能过多,状态机之间的状态流转维护成本高
- 更新成本高,批量请求时,乐观更新需要将按照状态进行分片批处理,或者自定义sql各种case when,或业务或实操上设计批量操作时,限制只能单状态批处理
- 状态机回滚问题。有些业务场景需要回滚状态,例如:配送员配送货物代收后,消费者想要送货上门,需要回滚至待配送,重新送货上门。或者消费者拒收后,因为商家介入与消费者协商或者消费者反悔,需要重新配送
幂等
上游幂等
接口入参增加幂等key与bizType字段,由上游提供幂等
中台设计常用的实现方式,维护成本低,扩展性好,例如实现批量请求,因为幂等key通常设计为bizType+唯一key,等同于是一个唯一的字段,而不是多字段,从而实现批量处理更简单
下游幂等
下游基于上游业务字段实现幂等
公司的财务设计实现方式,维护到后期导致出现,业务繁多时,不同业务间出现多种幂等字段组合,因此接口也存在多个幂等接口实现
总结
- Java语言开发,API中不能使用枚举类型,否则服务端新增枚举会影响客户端。还有一个踩坑场景,服务端采用go语言开发,框架(grpc-gateway),客户端使用java语言开发,服务端通过swagger提供接口,使用https://github.com/swagger-api/swagger-codegen逆向生成代码时,接口的枚举api逆向生成为:NUMBER_1(1),显然不是服务端的定义,导致请求服务端失败。虽然可能是api接口声明导致,或者grpc-gateway协议转换层导致。但是使用枚举会有可能踩坑(我们踩了-_-!!!报错见下方引用1),如果使用Integer则不会有该类问题,相比之下可以在api设计时可以考虑使用Integer或其他类型替换枚举
- 批量请求的接口特点是高吞吐
- 单个请求的接口特点是高并发
引用1. {“code”:3,“detail”:[],“message”:"proto: (line 1:152): invalid value for enum type: “NUMBER_1"”}
swagger逆向生成脚本
java -jar modules/swagger-codegen-cli/target/swagger-codegen-cli.jar generate \
-i http://www.myhost.com/myapi.swagger.json \
-l java \
-o /Users/gallant/Documents/swagger \
--model-package org.gallant.dto \
--api-package org.gallant.api
就单个接口而言,吞吐与并发是互斥的,设计过程中可以基于实际的业务进行设计。就像jvm的Parallel GC与CMS GC的选择
接口与消息
- 消息实时性差,可能出现消息丢失,可能出现消息挤压,上下游交互需要生产者生产消息,消息中间件持久化消息,消费者消费消息,延迟不可控。上下游解耦
- 接口实时性强,上下游强耦合