倒计时生产票、卷需求的实现



       我们经常见到一些场景,开发者为了增加用户互动的趣味或者是实际业务的强烈需要,在系统中设置票券等道具,并定时发放给用户,用户拿到这些道具去做一些好玩的事情。例如我们系统在每天固定的时间点(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设为唯一索引。防止重复插入。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值