乐趣区

一次派发优惠券开发优化

一、需求

平台做优惠券改造,有一个需求:系统派发指定优惠券给全部注册用户

  • 1、如果用户对该优惠券已达到单人限领数量,则不给予派发
  • 2、如果优惠券库存不足以派发给所有用户,则派发已库存的优惠券,直至库存派发完。

二、生产运行环境

一个服务多机部署

二、第一个版本

1、解决问题

为了控制派发给用户的优惠券数量,并且精准扣减库存,采用分布式锁对优惠券进行上锁,串行派发。

3、处理逻辑

  • a、查询所有用户;
  • b、循环每个用户,做如下派发逻辑:
    1、对优惠券进行上锁;
    2、查询优惠券库存是否大于 0,如果大于 0,则进行下一步发放步骤;反之,跳出当优惠券派发循环;
    3、查询用户已领数量、单人限领数量、库存数量,确定出该用户能领取的优惠券数;
    4、派发优惠券给用户;
    5、扣减库存;
    6、释放优惠券锁;

3、存在问题

  • 未考虑到用户量,串行派发,新能不好

三、第二个版本

1、解决问题

  • 问题的根本是串行派发,使用数据库更新的原子性保证库存(update xx_table set amont = amout – num where id=xxxx),这样可以除去分布式锁;
  • 分批派发给用户,多线程派发;
  • 以服务器 CPU 核心数为参考,创建数量一致的线程数进行派发,避免 CPU 线程切换上下文带来的性能消耗;
  • 使用多线程阻塞等待,控制并非线程数,可以参考基于 Future / CountDownLatch 等实现;

2、处理逻辑

  • a、分页查询用户,进行分批派发;
  • b、对分页查询处理的分批用户,创建多线程,再次分批进行优惠券派发;
  • c、每个派发线程都是串行执行一下逻辑:
    1、查询优惠券库存是否大于 0,如果大于 0,则进行下一步发放步骤;反之,跳出当优惠券派发循环;
    2、查询用户已领数量、单人限领数量、库存数量,确定出该用户能领取的优惠券数;
    3、派发优惠券给用户;
    4、扣减库存;

关键代码

int coreSize = Runtime.getRuntime().availableProcessors() * 2; // 服务器 CPU 核心数
int pageNo = 1;
int singleTreadBatchSize = 500; // 单线程派发数量
int pageSize = coreSize * singleTreadBatchSize; 
List<Future<Object>> sendTasks = new LinkedList<>();
long total = 0;
do {PageDto<String> data = userDataBiz.getUserUids(pageNo, pageSize);
    total = data.getTotal();
    List<String> uids = data.getRecords();
    if (CollectionUtils.isEmpty(uids)) {break;}
    int fromIndex = 0;
    int toIndex = 0;
    do {
        fromIndex = toIndex;
        toIndex += singleTreadBatchSize;
        toIndex = (toIndex > uids.size()) ? uids.size() : toIndex;
        if (fromIndex >= toIndex) {break;}
        List<String> uidGroup = uids.subList(fromIndex, toIndex);
        if (CollectionUtils.isEmpty(uidGroup)) {break;}
        Future<Object> sendTask = executor.submit(() -> {
            // 串行派发优惠券给指定用户
            sendTicket2Users(activity, ticket, uidGroup);
            return new Object();});
        sendTasks.add(sendTask);
    } while (true);
    // 等待当前派发完成再进行下一批次
    for (Future task : sendTasks) {task.get();
    }
} while (pageNo++ * pageSize < total);

2、存在问题

  • 基于此版本在测试环境对 85000 人进行派发,耗时大概 23min。按生产大概 850000 人来算,派发时间也要 230min,合计 3 天又 1 小时。(p≧w≦q)

四、第三个版本

1、解决问题

  • 针对单机已经没有什么优化的了。(这里要针对每个用户进行限领判断,就不能做批量扣库存、批量派发记录 insert);
  • 单机不行,就往多机考虑(生产就是多机部署)。把分批派发给用户的任务分发给各个机器,多个机器同时多线程派发,那就是性能翻倍了;

2、处理逻辑

  • 1、进行派发时,根据用户总数,计算出派发任务清单,包含派发人范围、券;
  • 2、利用 MQ 消息队列,发送派发任务;
  • 3、MQ 消费者接收到派发任务后,如果派发人员数量过多,则开启多线程派发,反之,单线程派发(判断数量根据实际)执行一下逻辑:
    3.1、查询优惠券库存是否大于 0,如果大于 0,则进行下一步发放步骤;反之,跳出当优惠券派发循环;
    3.2、查询用户已领数量、单人限领数量、库存数量,确定出该用户能领取的优惠券数;
    3.3、派发优惠券给用户;
    3.4、扣减库存;

2、存在问题

如果还是 850000 人一台机器 230min 来算,如果 10 台机器,要 23min;如果是 50 台机器,要 4.6min;(可实际哪来的 50 台、10 台 ????)

五、第四个版本

1、解决问题

跟同事讨论过,才发现是自己没事找事。
整个系统派发优惠券的触发机制是,派发一到就派发给所有用户,现在平台用户还不是特别大,如果是是几百万上千万用户,这耗时就哈哈了。
分析下这个派券系统:
在整个优惠券体系中,涉及的主体包括 用户、券。用户和券的关系就是接收券、使用券。
系统派发会带来极大的内耗,可以考虑把这部分转嫁到用户身上,由用户来触发,而不是系统触发。
因为用户操作时间存在不确定性,这样可以分散系统短时间内的压力。由指定时间,用户登录访问系统(或是其他某种特定事件)触发申请系统派发给当前用户。

六、总结

优化从单机级别、多机级别、业务层次进行分析处理,如果能一开始对用户量有一定的概念,也免得这么浪费事件再开发。

退出移动版