我们经常见到一些场景,开发者为了增加用户互动的趣味或者是实际业务的强烈需要,在系统中设置票券等道具,并定时发放给用户,用户拿到这些道具去做一些好玩的事情。例如我们系统在每天固定的时间点(0点、4点、8点、12点、16点、20点共6个时间点),以下统称为“生产点”,为所有注册账号(用户)生产一张票。票可以投给参加活动的选手。每张票有效期3天。票源源不断的被生产,源源不断的被使用或者失效,像是一个小的生态系统。
1. 初级方案
初期用户量很少。为了迅速上线。采取了简单的方案。
ScheduledExecutorService executor = Executors.newSingleThreadExecutor;
logger.info("BallotScheduler assign thread come in");
Calendar cal = Calendar.getInstance();
int h = cal.get(Calendar.HOUR_OF_DAY);
int initialDelay = Integer.MAX_VALUE;
Calendar next = Calendar.getInstance();
if (h < 4) {
next.set(Calendar.HOUR_OF_DAY, 4);
} else if (h < 8) {
next.set(Calendar.HOUR_OF_DAY, 8);
} else if (h < 12) {
next.set(Calendar.HOUR_OF_DAY, 12);
} else if (h < 16) {
next.set(Calendar.HOUR_OF_DAY, 16);
} else if (h < 20) {
next.set(Calendar.HOUR_OF_DAY, 20);
} else {
next.add(Calendar.DATE, 1);
next.set(Calendar.HOUR_OF_DAY, 0);
}
next.set(Calendar.MINUTE, 0);
next.set(Calendar.SECOND, 0);
initialDelay = (int) ((next.getTimeInMillis() - cal.getTimeInMillis()) / 1000) - 1;
logger.info("initialDelay: " + initialDelay);
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
long t = System.currentTimeMillis() + Constants.HOURMILLIS * 4 + 1000;
Date d = new Date(t);
redisUtils.setStr(Constants.BALLOT_NEXT_ASSIGN_TIMESTAMP, t + "");
logger.info("BallotScheduler run thread come in-------" + d);
int start = 0;
Calendar now = Calendar.getInstance();
Calendar overdate = Calendar.getInstance();
overdate.add(Calendar.DATE, Constants.BALLOT_OVERDATE_DAY);
while (true) {
logger.info("start: {}; step: {}", start, step);
List<User> users = userDao.getUsers(start, step);
if (users == null || users.size() == 0) {
logger.info("users == null");
Calendar thisTime = Calendar.getInstance();
thisTime.add(Calendar.SECOND, Constants.BALLOT_ASSIGN_DELAY_SECONDS);
break;
}
List<UserBallot> ballots = new ArrayList<>();
for (User user : users) {
logger.info("user:" + user.getPassport());
UserBallot ballot = new UserBallot();
ballot.setCreatetime(now.getTime());
ballot.setPassport(user.getPassport());
ballot.setOverdate(overdate.getTime());
ballots.add(ballot);
}
int rtn = userBallotDao.saveBatch(ballots);
logger.info("save ballot rtn: {}", rtn);
start += step;
}
}
}, initialDelay, Constants.BALLOT_ASSIGN_DELAY_SECONDS, TimeUnit.SECONDS);
关键点:
initialDelay 计算出程序部署启动后当前时间距离下次“生产点”的时间。
Constants.BALLOT_ASSIGN_DELAY_SECONDS 四个小时的秒数
Constants.BALLOT_NEXT_ASSIGN_TIMESTAMP 将下次的“生产点”的时间放入redis。“系统将于X小时Y分Z秒 后产生一张新的加油票”就是通过这个值来换算的
while循环 批量给所有用户插入票。过期时间设为Constants.BALLOT_OVERDATE_DAY天后
这种方案思路和实现简单,但是用户量30000+,生产票延时太明显,直观表现是20点整时,APP上显示出产生了加油票,但实际并没有,过了一会儿才有票。
即便考虑将批量插入阶段采用多线程,在用户活跃度较高的时间点比如20点整,在定时任务与API程序共同部署在某台服务器的情况下,对系统的瞬时压力也是显而易见的。于是思考有什么更好的方案?
2. 改进方案
票在数据库中提前生产好。只是到了这6个“生产点”才让它们出来“见人”。
Calendar cal = Calendar.getInstance();
int h = cal.get(Calendar.HOUR_OF_DAY);
int initialDelay = Integer.MAX_VALUE;
Calendar next = Calendar.getInstance();
if (h < 4) {
next.set(Calendar.HOUR_OF_DAY, 4);
} else if (h < 8) {
next.set(Calendar.HOUR_OF_DAY, 8);
} else if (h < 12) {
next.set(Calendar.HOUR_OF_DAY, 12);
} else if (h < 16) {
next.set(Calendar.HOUR_OF_DAY, 16);
} else if (h < 20) {
next.set(Calendar.HOUR_OF_DAY, 20);
} else {
next.add(Calendar.DATE, 1);
next.set(Calendar.HOUR_OF_DAY, 0);
}
next.set(Calendar.MINUTE, 0);
next.set(Calendar.SECOND, 0);
next.set(Calendar.MILLISECOND, 0);
initialDelay = (int) ((next.getTimeInMillis() - cal.getTimeInMillis()) / 1000) - 1;
logger.info("initialDelay: " + initialDelay);
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
long t = System.currentTimeMillis() + Constants.HOURMILLIS * 4 + 1000;
redisUtils.setStr(Constants.BALLOT_NEXT_ASSIGN_TIMESTAMP, t + "");
}
}, initialDelay, Constants.BALLOT_ASSIGN_DELAY_SECONDS, TimeUnit.SECONDS);
int init = 0;
if (h < 3) {
init = 3 - h;
} else {
init = 24 + 3 - h;
}
logger.info("initDelay: " + init);
executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
Calendar cal = Calendar.getInstance();
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
for (int i = 0; i < 6; i++) {
cal.add(Calendar.HOUR_OF_DAY, 4);
long createtime = cal.getTimeInMillis();
long overdate = createtime + Constants.DAYMILLIS * Constants.BALLOT_OVERDATE_DAY;
batchSave(createtime, overdate);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
logger.error(e.getMessage(), e);
}
}
// 删除过期的星票
userBallotDao.deleteOverdate();
}
}, init, 24, TimeUnit.HOURS);
Constants.BALLOT_NEXT_ASSIGN_TIMESTAMP 的计算沿用了方案1的
每天init时间点(3点多)去做提前生产出接下来一天6个“生产点”的票。createtime设置为它应该“见人”的时间。
数据库passport(255), createtime设为唯一索引。防止重复插入。