很久没有更新过了,准备开始写一些质量高一些的文章,也会同步到SegmentFault,个人主页:https://segmentfault.com/u/jinhaozhi/articles,近期主要记录下接近一年的核心工作。
一种通过单机事务来保障分布式数据一致性的策略,作用就是在分布式系统中保证上下游的数据最终一致;
在任何一个电商系统中,订单都是属于核心链路部分,要求高可用和稳定,但是作为核心链路,对下游的其余依赖也是难以避免的,比如活动、履约(履约可能有完全异步的设计方案,但在我们的业务中不好实现)、结算和售后等,一个本地事务的结构可能如下:
S1是核心链路的服务,某个本地事务包括A,B,C三个步骤,A调用下游服务S2,B调用下游服务S3,这样就有可能出现以下这些异常case:
造成的后果呢就是数据不一致,比如A是活动券核销,S1是订单服务,S2是活动服务,就会出现订单下失败了,但是这张券却用掉了,用户肯定不会买账。
解决方案其实很多,列举几种(这里说的劣势并不是说不好,取决于业务场景和实现方式):
1、阿里开源的分布式事务解决方案seata
优:傻瓜式的使用方式,支持AT和TCC模式(我只用过这两种)
劣:
听了内部负责人的讲座,设计和实现都很精妙,考虑的也很周到,但是第三点对核心链路的业务来说是硬伤,第一点影响也较大,rt基本翻了一倍,最终选择弃用;
2、TCC
思路:下游提供回滚方案,本地事务失败时调用下游回滚接口,同时提供补偿方案,保障回滚成功;TCC是比较经典的分布式事务解决方案,形式有很多,个人觉得劣势也是开发成本太高,需要下游协作,但是使用范围很广泛,也容易扩展;
3、本地消息表
基本思路如图,以服务S1为例,本地事务中并不真正的执行A,B,C操作,事务提交后再去真正的执行A,B,C,同时提供补偿任务,保证A,B,C最后执行成功;
这种本地消息表的实现方案特别适用于核心链路业务,比如订单,将一些外部依赖放到本地事务外面,比如活动券,如果用户有这张券,而且券是可用的,订单业务就不应该被活动业务干扰,换言之,就是这笔订单必须下单成功,就算活动的服务挂了,这笔订单也要成功,但是不可降级的业务逻辑还是要放到本地事务中,比如用户的风控校验,店铺校验,活动券的时效校验等,这些逻辑应该与订单同时成功或同时失败,所以应该放到本地事务中;
还有个最大努力通知方案,详情请参考各大搜索引擎;
这里以订单的活动券核销和履约为例,介绍本地消息表的简单实现
msg_type代表消息类型,msg_content记录消息内容,包括参数信息以及对应的订单号等,开发者可以自己定义
@Getter
@AllArgsConstructor
public enum OrderMsgEnum {
USE_PRIZE(0, "核销消息"),
PERFORMACE(1, "履约消息")
;
private Integer code;
private String desc;
public static OrderMsgEnum of(Integer code) {
return Arrays.stream(OrderMsgEnum.values()).filter(orderMsgEnum -> orderMsgEnum.getCode().equals(code)).findFirst().get();
}
这里使用策略模式和注解,可以省去很多难看的if else,扩展起来也会方便很多
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MsgType {
OrderMsgEnum msgType();
}
该类用于分发策略,在启动时收集具体的策略实现类,TransactionSynchronizationAdapter是spring提供的钩子方法,通过在本地事务中注册钩子函数,实现事务提交后调用钩子函数的逻辑,其实现也非常简单,将注册的适配器(钩子函数的实现)保存到threadLocal中,在事务的commit执行后回调这个钩子函数,spring4以后支持注解的方式@TransactionalEventListener(我用了不生效,原因不明)
@Component
@Slf4j
public class OrderMsgHandlerContext implements ApplicationContextAware {
public Map msgHandlerMap = new HashMap();
private static final int EXECUTOR_CORE_POOL_SIZE = 30;
private static final int EXECUTOR_MAX_POLL_SIZE = 50;
private static final long EXECUTOR_KEEP_ALIVE_TIME_MILLIS = 30000L;
private static ExecutorService executor =
new ThreadPoolExecutor(EXECUTOR_CORE_POOL_SIZE,
EXECUTOR_MAX_POLL_SIZE,
EXECUTOR_KEEP_ALIVE_TIME_MILLIS,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(100));
//执行操作后删除消息
public boolean exec(OrderLocalMsg msg) {
if (msgHandlerMap.get(msg.getMsgType()).exec(msg)) {
return delete(msg.getMsgType(), msg.getId());
} else {
CatUtils.addCatMetric("执行消息任务失败");
log.error("exec task error, msgType:{}, msg:{}", msg.getMsgType(), JsonUtils.toJson(msg));
return false;
}
}
//插入消息
public OrderLocalMsg add(Integer msgType, T msg) {
AbstractOrderMsgHandler orderMsgHandler = msgHandlerMap.get(msgType);
return orderMsgHandler.add(msg);
}
private boolean delete(Integer msgType, Long id) {
return msgHandlerMap.get(msgType).delete(id);
}
//启动获取策略类
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Class extends Annotation> annotationClass = MsgType.class;
Map beanWithAnnotation = applicationContext.getBeansWithAnnotation(annotationClass);
Set> entitySet = beanWithAnnotation.entrySet();
for (Map.Entry entry : entitySet) {
Class extends Object> clazz = entry.getValue().getClass();
MsgType declaredAnnotation = clazz.getDeclaredAnnotation(MsgType.class);
if (declaredAnnotation == null) {
log.error("no protocol type annotation!");
}
msgHandlerMap.put(declaredAnnotation.msgType().getCode(), (AbstractOrderMsgHandler) applicationContext.getBean(clazz));
}
}
//钩子函数执行消息任务
public TransactionSynchronizationAdapter getTransactionSynchronizationAdapter(OrderLocalMsg localMsg) {
return new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
if (!Objects.isNull(localMsg.getId())) {
exec(localMsg);
}
}
};
}
//钩子函数批量执行消息
public TransactionSynchronizationAdapter getTransactionSynchronizationAdapter(List msgList) {
return new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
for (OrderLocalMsg orderLocalMsg : msgList) {
executor.submit(() -> exec(orderLocalMsg));
}
}
};
}
}
@Data
@Component
public abstract class AbstractOrderMsgHandler {
OrderMsgEnum msg;
/**
* 插入一条数据到本地消息表
* @return
*/
public abstract OrderLocalMsg add(T msg);
/**
* 删除一条数据
* @param id
* @return
*/
public abstract boolean delete(Long id);
/**
* 重试任务
* @param orderLocalMsg
* @return
*/
public abstract boolean exec(OrderLocalMsg orderLocalMsg);
}
以履约消息为例,只截取部分字段,会意就行
@Data
public class PerformanceOrderMsg {
/**
* 购买商家mallId
*/
private Long mallId;
/**
* 支付时间
*/
private Date payTime;
/**
* 订单号
*/
private String orderSn;
/**
* 订单支付金额(单位:分)
*/
private Long amount;
}
以订单履约为例
@Component(value = "orderPerformanceMsgHandler")
@Slf4j
@MsgType(msgType = OrderMsgEnum.PERFORMACE)
public class OrderPerformanceMsgHandler extends AbstractOrderMsgHandler {
@Autowired
private OrderLocalMsgRepository repository;
@Autowired
private OrderPerformanceService orderPerformanceService;
@Autowired
private OrderRepository orderRepository;
@Override
public OrderLocalMsg add(PerformanceOrderMsg msg) {
OrderLocalMsg orderLocalMsg = new OrderLocalMsg();
orderLocalMsg.setOrderSn(msg.getOrderSn());
orderLocalMsg.setMsgType(OrderMsgEnum.PERFORMACE.getCode());
orderLocalMsg.setMsgContent(JsonUtils.toJson(msg));
repository.create(orderLocalMsg);
return orderLocalMsg;
}
@Override
public boolean delete(Long id) {
return repository.delete(id);
}
//通过返回true或false来判断操作是否成功,报错不应该抛出去
//操作成功则删除消息,操作失败则依赖补偿任务重试
@Override
public boolean exec(OrderLocalMsg orderLocalMsg) {
if (Objects.isNull(orderLocalMsg) || StringUtils.isEmpty(orderLocalMsg.getMsgContent())) {
log.error("msg is null, or msg content is null!");
return false;
}
PerformanceOrderMsg msgContent = JsonUtils.fromJson(orderLocalMsg.getMsgContent(), PerformanceOrderMsg.class);
OrderPerformanceDto dto = BeanUtils.convert(msgContent, OrderPerformanceDto.class);
try {
orderPerformanceService.performance(dto);
} catch (Exception e) {
log.error("exec type: order performance error, orderSn:{}", msgContent.getOrderSn());
CatUtils.addEvent(CatUtils.SYSTEM_EXCEPTION_TYPE, CatConsts.SERVICE_ORDER_PERFORMANCE_ERROR);
return false;
}
return true;
}
}
@Slf4j
@Component
@EnableScheduling
public class OrderMsgRetryTask {
@Autowired
private OrderMsgHandlerContext context;
@Autowired
private OrderLocalMsgRepository msgRepository;
private static final int EXECUTOR_CORE_POOL_SIZE = 30;
private static final int EXECUTOR_MAX_POLL_SIZE = 50;
private static final long EXECUTOR_KEEP_ALIVE_TIME_MILLIS = 30000L;
private static ExecutorService executor =
new ThreadPoolExecutor(EXECUTOR_CORE_POOL_SIZE,
EXECUTOR_MAX_POLL_SIZE,
EXECUTOR_KEEP_ALIVE_TIME_MILLIS,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(100));
//cron=xxx,根据业务需要定义任务重试策略
public void doExecute() {
Page page = PageHelper.startPage(1, 100, false);
List msgs = page.doSelectPage(() -> msgRepository.findByExample(new OrderLocalMsgExample())) ;
if (CollectionUtils.isEmpty(msgs)) {
log.info("no order msg in db");
return;
}
msgs.stream().forEach(msg -> executor.submit(() -> context.exec(msg)));
}
}
这就是一个本地消息表的实现方案,使用示例:
/履约消息落库
PerformanceOrderMsg msg = BeanUtils.convert(orderPerformanceDto, PerformanceOrderMsg.class);
OrderLocalMsg localMsg = orderMsgHandlerContext.add(OrderMsgEnum.PERFORMANCE.getCode(), msg);
//afterCommit删除消息
TransactionSynchronizationManager.registerSynchronization(orderMsgHandlerContext.getTransactionSynchronizationAdapter(localMsg));
三行代码就能使用,如果消息类型需要扩展,比如新增结算消息,步骤:1、枚举类新增结算消息类型;2、新增枚举消息结构定义;3、新增结算策略类实现;-----个人觉得扩展起来还是很方便的
具体的实现还是要跟着具体的业务逻辑走~欢迎一切交流和撕逼