本地消息表这个方案最初是ebay提出的,核心就是将需要分布式处理的任务通过本地消息日志存储的方式来异步执行。该方案可以存到本地文本,数据库或消息队列,再通过异步线程或者自动job发起重试。
1、背景介绍
供应链仓储域子域繁多,例如库存域,lpn域等,平时开发的过程中涉及很多分布式事务的场景,例如收货加库存,发货扣库存,拣货入箱,发货出箱等一些分布式事务场景,所以迫切需要出一套分布式事务处理方案,在调研了市场上的分布式事务解决方案,结合wms自身业务域不是强一致性的特色,选择了最终一致性,且使用本地消息表去实现它。
本地消息表这个方案最初是ebay提出的,核心就是将需要分布式处理的任务通过本地消息日志存储的方式来异步执行。该方案可以存到本地文本,数据库或消息队列,再通过异步线程或者自动job发起重试。
2、设计前的思考
在操作本地事务的同时,需要额外写入一张需要最终一致性业务记录的表,即本地消息表,且该业务记录是有状态的,在本地事务提交后,再执行需要最终一致性的方法,成功后更新记录状态为成功,如果失败了,还要引入兜底重试机制,下图能简单介绍它的功能:
为了实现以上最终一致性的功能。我们同样还需要做到以下几点:
- 无侵入:这个好理解,对于需要最终一致性的场景,可以很简单的实现
- 策略化:包括重试次数,重试的间隔时间,是否使用异步方式等
- 通用性:最好是无改动(或者很小改动)的支持绝大部分的场景,拿过来直接可用
- 复用性:对于异常场景存在需要重试场景,同时希望把正常逻辑和重试逻辑复用
3、架构设计
调研了大家对一致性框架的诉求,最终定义了如,入参自定义序列化,环境隔离,同步异步执行切换,注解支持,自定义执行拦截器,以及适配得物仓储业务的业务上下文订制以及持久化等一系列的核心能力,底层依赖了Spring的生态,在数据存储依赖了Mysql,Mongodb,缓存分布式锁上依赖了Redis等一系列主流的中间件,最终以jar包形式实现,尽可能做到即拿即用。
4、详细设计
基于在以上的架构设计后,做了以下设计
4.1 注解支持:@EnableConsistency
为了让用户更快,更方便的接入一致性框架,我们在早期的抽象类继承的方案上做了一版本升级,使用注解,使得使用方式跟@Transactional注解一样,只要加上@EnableConsistency就支持最终一致性的支持,非常方便。
4.2 自动重试&重试等待策略可配:
最终一致性有个天然的组成部分就是需要重试,一致性框架也不例外,引用了Spring的
ScheduledTask实现定时重试那些运行失败的记录,另外重试等待策略同样可配置:
4.2.1 重试等待策略
固定时间重试
支持配置固定时间间隔重试
延迟指数重试
底层采用Math.pow函数在重试次数越多次,执行间隔时间呈指数级增长
4.2.2 自定义重试次数
注解式支持重试次数的定义
4.3 自定义拦截器
在执行需要最终一致性方法的时候,我们同样提供了如Spring AOP一样被代理方法的前置,成功,异常后需要做的一些切面功能,非常方便的满足使用者的扩展,解耦了实现与扩展。
/**
* 在记录重试次数失败后 执行
* @param context
* @param lastException
*/
void close(ConsistencyContext context, Throwable lastException);
/**
* 开始执行重试前 拦截器
* 如果返回false 则 执行期不继续进行
* @param context
* @return
*/
boolean open(ConsistencyContext context);
/**
* 发生异常拦截器
* @param context
* @param throwable
*/
void onError(ConsistencyContext context, Throwable throwable);
4.4 业务上下文&持久化
在业务系统的开发中,存在一个ThreadLocal的上下文,记录了用户的组织架构,签到等一系列用户信息。在设计一致性框架的时候我们考虑到用户上下文的存在,暴露了业务上下文扩展,以及存储业务上下文供重试时使用的能力。
public interface ContextListener {
/**
* 获取上下文内容
* @return
*/
Map<String,String> getContext();
/**
* 设置用户上下文
* @param contextMap
*/
void setContext(Map<String, String> contextMap);
/**
* 清除用户上下文
* @param contextMap
*/
void clear(Map<String, String> contextMap);
}
4.5 环境隔离
由于得物的预发环境与生产环境采用的同一批数据库,故也将一致性框架记录采用了spring.profile.active的值做环境隔离,确保重试时不会预发的跑到生产的数据。
4.6 入参自定义序列化
由于需要在本地消息表中记录需要重试的方法的入参,故就涉及到入参序列化的问题,在思考良久之后,只提供默认的Json方式的序列化与反序列化,如果用户需要额外的序列化与反序列化方法,我们也支持,提供了暴露序列化与反序列化的口子供用户实现。
public interface SerializerListener{
/**
* 进行序列化
* @param params
* @return
*/
String serializer(Object[] params);
/**
* 反序列化
* @param str
* @return
*/
Object[] deserializer(String str);
}
4.7 执行模式可配置
一般用本地消息执行最终一致性的部分,开始的设计是异步化执行,后续收到使用者用户的反馈,也有部分同步执行的场景,故增加了同步异步执行开关,开发者自行选择。
4.8 数据模型&状态机
4.9 核心代码展示
4.9.1ExecutorAutoConfiguration框架初始化
/**
* 加载为切面增强提供织入接口advice,和注入advice的pointcut
*/
@PostConstruct
public void init() {
Set<Class<? extends Annotation>> eventualConsistencyAnnotationTypes = new LinkedHashSet<Class<? extends Annotation>>(1);
eventualConsistencyAnnotationTypes.add(EventualConsistency.class);
this.pointcut = buildPointcut(eventualConsistencyAnnotationTypes);
this.advice = buildAdvice();
buildExecutorManager();
if (this.advice instanceof BeanFactoryAware) {
((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
}
}
4.9.2 核心注解@EventualConsistency
使用最终一致性方法的核心注解:
public @interface EventualConsistency {
/**
* 第一次执行是否异步执行
* @return
*/
boolean async() default true;
/**
* 最大重试次数
* @return
*/
int maxRetryTimes() default 6;
/**
* 延迟时间
* @return
*/
Delay delay() default @Delay;
/**
* Bean names 拦截器
* @return retry listeners bean names
*/
String[] listeners() default {};
@Deprecated
String label() default "";
String beanName() default "";
/**
* Bean names 拦截器 用来进行序列化和反序列化
* @return retry listeners bean names
*/
String serializerListener() default "";
String referenceNo() default "";
@AliasFor(annotation = Transactional.class)
Class<? extends Throwable>[] rollbackFor() default Exception.class;
@AliasFor(annotation = Transactional.class)
Class<? extends Throwable>[] noRollbackFor() default {};
boolean manageContext() default true;
}
4.9.3 核心实现Advice,MethodInterceptor的AnnotationAwareRetryOperationsInterceptor
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取最终执行拦截的委托
MethodInterceptor delegate = getDelegate(invocation.getThis(), invocation.getMethod());
if (delegate != null) {
return delegate.invoke(invocation);
} else {
return invocation.proceed();
}
}
4.9.4 延迟执行策略
public interface DelayPolicy {
/**
* 获取下次执行时间
* @return
*/
Long nextTime();
}
/**
* 延迟指数
*/
public class ExponentialDelayPolicy implements DelayPolicy {
public static final long DEFAULT_INITIAL_INTERVAL = 2L;
public static final int DEFAULT_MULTIPLIER = 3;
/**
* 默认重试最大延迟时间 (24小时)
*/
public static final long DEFAULT_MAX_INTERVAL = 86400L;
/**
* 初始间隔
*/
private volatile long initialInterval = DEFAULT_INITIAL_INTERVAL;
private volatile int multiplier = DEFAULT_MULTIPLIER;
/**
* 最大延迟时间
*/
private volatile long maxInterval = DEFAULT_MAX_INTERVAL;
@Override
public Long nextTime() {
ConsistencyContext context = ConsistencyContextHolder.getContext();
Double pow = Math.pow(initialInterval + context.getRetryCounts(), multiplier);
if(pow > maxInterval){
return maxInterval;
}
return pow.longValue();
}
}
/**
* 固定时间
*/
public class FixedDelayPolicy implements DelayPolicy {
private static final long DEFAULT_DELAY_PERIOD = 10L;
private volatile long delayPeriod = DEFAULT_DELAY_PERIOD;
@Override
public Long nextTime() {
return this.delayPeriod;
}
}
4.9.5 AsyncConsistencyExecutor异步最终一致性执行
5、未来展望
(1)后台管理页面设计,支持报表查询,以及错误异常处理
(2)trace监控接入,方便定位问题
(3)适配业务支持类型DB
(4)自定义归档策略
最终一致性框架是由wms全组同学一起设计和开发完成,并且陪伴了得物快速发展的两年多,经历了2个618以及3个双十一,若干个情人节,圣诞节的考验。系统运行健康,无性能瓶颈,提升了很多场景下最终一致性的开发速度。目前仍在安全稳健的保障着仓储域服务的正常运转。