虚拟权益(视屏会员月卡、电商平台购物卡等)对接系统的上游限流设计

469 阅读5分钟

前言

虚拟权益系统是一个用于管理和对接各个上游权益供应商,同时提供管理和对接下游分销商的系统。由于上游供应商服务能力不统一,能够支持的请求并发不统一,在分销商做活动等下游分销商请求量激增的情况下,如果把请求全部打到上游供应商,对于一些能力有限的供应商,会导致失败率大大增加,甚至导致上游供应商服务瘫痪不可用。为了避免虚拟权益系统受上游供应商能力限制,对上游供应商做限流是有必要的。

一、先介绍下虚拟权益系统的发券流程

虚拟权益系统的主要功能是接收下游分销商采购和请求上游供应商获取卡券。通常系统设计为分销商提供三个主要的对接接口下单、查询、回调通知。分销商向虚拟权益系统采购卡券的完整流程如下:

  1. 分销商需要先调用下单接口,权益系统接收到请求后校验账户余额等必要校验信息,校验通过后,先把订单信息落入数据库。
  2. 虚拟权益系统调用上游供应商接口获取真实的卡券,得到卡券信息后,通过回调接口通知下游分销商。
  3. 下游分销商也可以在下单成功后,定时通过订单号查询订单结果。

需求分析

  1. 根据上游供应商能力不同可分别配置限流值,限流值更新快速生效
  2. 下游分销商请求量激增,上游供应商能力达到限流值时,不会导致下游分销商订单失败

代码设计

1. 为每一个上游供应商添加一个限流值time_rate(单位毫秒),表示针对此供应商的请求时间间隔为time_rate毫秒,间隔大于time_rate毫秒才可以请求上游

2. 将下游分销商请求权益系统下单与权益系统请求上游供应商发券解耦

解耦可以采用异步调用的方式、消息中间件的方式。考虑到异步线程池在并发较高的情况下队列任务过载导致溢出,本设计采用rabbitmq消息中间件的方式实现解耦。订单入库成功后发送消息到指定队列,如下:

旧代码:providerBaseService.orderSubmit(orderNo);

解耦后代码:rabbitTemplate.convertAndSend("exchange", providerCode, order);

其中"exchange" 为交换机名,providerCode为供应商编号,同一供应商商品的采购订单发送到同一队列。

3. 初始化交换机、队列、队列交换机的绑定,动态添加监听

1)初始化消息监听容器,用于动态添加监听
@Bean
@Primary
public SimpleMessageListenerContainer submitListener() {
    SimpleMessageListenerContainer listener = new SimpleMessageListenerContainer(connectionFactory());
    listener.setAcknowledgeMode(AcknowledgeMode.MANUAL);
    listener.setExposeListenerChannel(true);
    listener.setPrefetchCount(1);
    listener.setConcurrentConsumers(1);
    listener.setMaxConcurrentConsumers(1);
    return listener;
}
2)编写下单消息处理类,限流主要逻辑
@Component("submitMessageListener")
@Slf4j
public class SubmitMessageListener  implements ChannelAwareMessageListener {

    @Autowired
    private IUpProviderService upProviderService;

    @Autowired
    protected Map<String, IProviderBaseService> providerBaseServiceMap;
    
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        boolean acked = false;
        try {
            PlatformOrder order = JSON.parseObject(message.getBody(), PlatformOrder.class);
            if (upProviderService.isSubmitPass(order.getUpProviderCode())) {
                String orderNo = order.getOrderNo();
                // 2.4、异步调用供应商
                String upProviderCode = order.getUpProviderCode();
                IProviderBaseService providerBaseService = providerBaseServiceMap.get(upProviderCode);
                if (providerBaseService == null) {
                    log.error("获取供应商实现失败:upProviderCode={},orderNo={}", upProviderCode, orderNo);
                    return;
                }
                
                providerBaseService.orderSubmit(orderNo);
            }else{
                acked = true;
                channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
            }
        } finally {
            if(!acked){
                channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
            }
        }

    }
}
代码upProviderService.isSubmitPass(order.getUpProviderCode())用来判断指定供应商的下单请求是否满足限流值被允许执行,允许则执行,不允许则执行拒绝策略消息重新回到队列等待下一次消费,具体代码如下:
@Slf4j
@Service
public class UpProviderServiceImpl extends BaseService implements IUpProviderService {


    @Autowired
    private UpProviderMapper upProviderMapper;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final String TIME_RATE_PREFIX = "time:rate:";
    private static final String SUBMIT_LAST_DEAL_TIME_PREFIX = "submit:last:deal:time:";

    private static final ConcurrentHashMap<String, String> timeRateMap = new ConcurrentHashMap<>();

    //获取上游供应商请求上游时间间隔
    private long getTimeRate(long curTime, String providerCode) {
        String key = TIME_RATE_PREFIX + providerCode;
        if (timeRateMap.containsKey(key)) {//内存有
            String[] split = timeRateMap.get(key).split("-");
            if (curTime - Long.parseLong(split[1]) > 30000L) {//30秒更新
                String s = stringRedisTemplate.opsForValue().get(key);
                if (StringUtils.isBlank(s)) {
                    Example example = new Example(UpProvider.class);
                    List<UpProvider> upProviders = upProviderMapper.selectByExample(example);
                    for (UpProvider upProvider : upProviders) {
                        if (upProvider.getDelFlag() == 0) {
                            stringRedisTemplate.opsForValue().set(TIME_RATE_PREFIX + upProvider.getProviderCode(), "" + upProvider.getTimeRate());
                            timeRateMap.put(key, upProvider.getTimeRate() + "-" + curTime);
                        }
                    }
                } else {
                    timeRateMap.put(key, s + "-" + curTime);
                }
            }
        } else {
            String s = stringRedisTemplate.opsForValue().get(key);
            if (StringUtils.isBlank(s)) {
                Example example = new Example(UpProvider.class);
                List<UpProvider> upProviders = upProviderMapper.selectByExample(example);
                for (UpProvider upProvider : upProviders) {
                    if (upProvider.getDelFlag() == 0) {
                        stringRedisTemplate.opsForValue().set(TIME_RATE_PREFIX + upProvider.getProviderCode(), "" + upProvider.getTimeRate());
                        timeRateMap.put(key, upProvider.getTimeRate() + "-" + curTime);
                    }
                }
            } else {
                timeRateMap.put(key, s + "-" + curTime);
            }
        }

        String s = timeRateMap.get(key);
        return StringUtils.isBlank(s)?500L:Long.parseLong(s.split("-")[0]);
    }

    /**
     * 判断 下单是否满足限流
     */
    @Override
    public boolean isSubmitPass(String upProviderCode) {
        String lockKey = "submit:pass:key:"+upProviderCode;
        try {
            if (stringRedisTemplate.opsForValue().increment(lockKey, 1L) == 1L) {
                long curTime = System.currentTimeMillis();
                long longTimeRate = getTimeRate(curTime, upProviderCode);
                String lastDealTime = stringRedisTemplate.opsForValue().get(SUBMIT_LAST_DEAL_TIME_PREFIX + upProviderCode);
                long longLastDealTime = StringUtils.isBlank(lastDealTime) ? 0L : Long.parseLong(lastDealTime);
                if (curTime - longLastDealTime >= longTimeRate) {
                    stringRedisTemplate.opsForValue().set(SUBMIT_LAST_DEAL_TIME_PREFIX + upProviderCode, "" + System.currentTimeMillis());
                    return true;
                }
            }
        } finally {
            stringRedisTemplate.opsForValue().increment(lockKey, -1L);
            stringRedisTemplate.expire(lockKey,5L, TimeUnit.SECONDS);
        }
        return false;
    }

    /**
     * 定时更新请求上游时间间隔,15分钟一次
     */
    @Scheduled(initialDelay = 0L, fixedRate = 1000L * 60L * 15L)
    public void updateTimeRate() {
        log.info("更新请求上游时间间隔开始");
        List<UpProvider> upProviders = upProviderMapper.selectAll();
        for (UpProvider upProvider : upProviders) {
            if (upProvider.getDelFlag() == 0) {
                stringRedisTemplate.opsForValue().set(TIME_RATE_PREFIX + upProvider.getProviderCode(), "" + upProvider.getTimeRate());
            }
        }
    }


}
说明

该段代码主要逻辑是将当前时间和对该供应商上次请求时间做减法与time_rate值作比较,大于time_rate值则返回通过并更新上次请求时间为当前值,小于则返回不通过。其中time_rate值做了三级缓存,一级在程序内存中(30秒更新一次),二级在redis中(15分钟跟新一次),三级在数据库中)。

updateTimeRate() 方法为spring定时任务主要将数据库中time_rate同步到redis中,时间间隔为15分钟。

getTimeRate(long curTime, String providerCode) 方法为从三级缓存中获取指定供应商time_rate值,

3)初始化交换机、队列、队列交换机的绑定,动态添加监听并启动
Connection connection = null;
Channel channel = null;
try {
   //获取下单监听注册下单监听处理类
   SimpleMessageListenerContainer submitListener = (SimpleMessageListenerContainer)application.getBean("submitListener");
   SubmitMessageListener submitMessageListener = (SubmitMessageListener)application.getBean("submitMessageListener");
   submitListener.setMessageListener(submitMessageListener);

   //初始化交换机
   ConnectionFactory connectionFactory = application.getBean(ConnectionFactory.class);
   connection = connectionFactory.createConnection();
   channel = connection.createChannel(false);
   channel.exchangeDeclare(MqConstant.SUBMIT_EXCHANGE, BuiltinExchangeType.DIRECT,true);

    //查询所有供应商信息
   UpProviderMapper upMapper = application.getBean(UpProviderMapper.class);
   Example example = new Example(UpProvider.class);
   List<UpProvider> upProviders = upMapper.selectByExample(example);

   //初始化队列及添加监听
   for (UpProvider upProvider : upProviders) {
      String submitName = MqConstant.SUBMIT_QUEUE_PREFIX + upProvider.getProviderCode();
      channel.queueDeclare(submitName,true,false,false,null);
      channel.queueBind(submitName,MqConstant.SUBMIT_EXCHANGE,upProvider.getProviderCode());
      submitListener.addQueueNames(submitName);
   }
   //启动监听
   submitListener.start();
} catch (Exception e) {
   log.error("初始化权益中心MQ异常:",e);
} finally {
   if(channel != null && channel.isOpen()){
      try {
         channel.close();
      } catch (Exception e) {
         log.error("初始化权益中心MQ关闭CHANNEL异常:",e);
      }
   }

   if(connection != null && connection.isOpen()){
      try {
         connection.close();
      } catch (Exception e) {
         log.error("初始化权益中心MQ关闭CONNECTION异常:",e);
      }
   }
   channel = null;
   connection = null;
}