本地消息表的设计与实现

前言

很久没有更新过了,准备开始写一些质量高一些的文章,也会同步到SegmentFault,个人主页:https://segmentfault.com/u/jinhaozhi/articles,近期主要记录下接近一年的核心工作。

背景

  • 本地消息表是什么?

       一种通过单机事务来保障分布式数据一致性的策略,作用就是在分布式系统中保证上下游的数据最终一致;

  • Case(以订单为例)

   在任何一个电商系统中,订单都是属于核心链路部分,要求高可用和稳定,但是作为核心链路,对下游的其余依赖也是难以避免的,比如活动、履约(履约可能有完全异步的设计方案,但在我们的业务中不好实现)、结算和售后等,一个本地事务的结构可能如下:

                  本地消息表的设计与实现_第1张图片

S1是核心链路的服务,某个本地事务包括A,B,C三个步骤,A调用下游服务S2,B调用下游服务S3,这样就有可能出现以下这些异常case:

  • A成功了,B失败了,S1的本地事务回滚,但S2并不感知
  • A,B成功了,C失败了,S1的本地事务回滚,S2和S3都不感知
  • etc...

造成的后果呢就是数据不一致,比如A是活动券核销,S1是订单服务,S2是活动服务,就会出现订单下失败了,但是这张券却用掉了,用户肯定不会买账。

解决方案

解决方案其实很多,列举几种(这里说的劣势并不是说不好,取决于业务场景和实现方式):

1、阿里开源的分布式事务解决方案seata

优:傻瓜式的使用方式,支持AT和TCC模式(我只用过这两种)

劣:

  • 维护全局的状态需要成本,包括集群的通信与等待;
  • 会破坏原生的事务,每一次写db操作就会提交一次,侵入严重;
  • 稳定性依赖于网络和seata集群;

听了内部负责人的讲座,设计和实现都很精妙,考虑的也很周到,但是第三点对核心链路的业务来说是硬伤,第一点影响也较大,rt基本翻了一倍,最终选择弃用;

2、TCC

思路:下游提供回滚方案,本地事务失败时调用下游回滚接口,同时提供补偿方案,保障回滚成功;TCC是比较经典的分布式事务解决方案,形式有很多,个人觉得劣势也是开发成本太高,需要下游协作,但是使用范围很广泛,也容易扩展;

3、本地消息表

基本思路如图,以服务S1为例,本地事务中并不真正的执行A,B,C操作,事务提交后再去真正的执行A,B,C,同时提供补偿任务,保证A,B,C最后执行成功;

                                                        本地消息表的设计与实现_第2张图片

这种本地消息表的实现方案特别适用于核心链路业务,比如订单,将一些外部依赖放到本地事务外面,比如活动券,如果用户有这张券,而且券是可用的,订单业务就不应该被活动业务干扰,换言之,就是这笔订单必须下单成功,就算活动的服务挂了,这笔订单也要成功,但是不可降级的业务逻辑还是要放到本地事务中,比如用户的风控校验,店铺校验,活动券的时效校验等,这些逻辑应该与订单同时成功或同时失败,所以应该放到本地事务中;

还有个最大努力通知方案,详情请参考各大搜索引擎;

具体实现

这里以订单的活动券核销和履约为例,介绍本地消息表的简单实现

表结构(实体类OrderLocalMsg)

msg_type代表消息类型,msg_content记录消息内容,包括参数信息以及对应的订单号等,开发者可以自己定义

本地消息表的设计与实现_第3张图片

消息类型定义

@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,扩展起来也会方便很多

注解@MsgType定义

@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 annotationClass = MsgType.class;
        Map beanWithAnnotation = applicationContext.getBeansWithAnnotation(annotationClass);
        Set> entitySet = beanWithAnnotation.entrySet();
        for (Map.Entry entry : entitySet) {
            Class 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、新增结算策略类实现;-----个人觉得扩展起来还是很方便的

总结

具体的实现还是要跟着具体的业务逻辑走~欢迎一切交流和撕逼

你可能感兴趣的:(分布式,Java)