一、需求
平台做优惠券改造,有一个需求:系统派发指定优惠券给全部注册用户
- 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、解决问题
跟同事讨论过,才发现是自己没事找事。
整个系统派发优惠券的触发机制是,派发一到就派发给所有用户,现在平台用户还不是特别大,如果是是几百万上千万用户,这耗时就哈哈了。
分析下这个派券系统:
在整个优惠券体系中,涉及的主体包括 用户、券。用户和券的关系就是接收券、使用券。
系统派发会带来极大的内耗,可以考虑把这部分转嫁到用户身上,由用户来触发,而不是系统触发。
因为用户操作时间存在不确定性,这样可以分散系统短时间内的压力。由指定时间,用户登录访问系统(或是其他某种特定事件)触发申请系统派发给当前用户。
六、总结
优化从单机级别、多机级别、业务层次进行分析处理,如果能一开始对用户量有一定的概念,也免得这么浪费事件再开发。