问题:
--------------------------------------------------------------------------------
用户反馈一些定时活动提前开启或者延后开启
1) 登录服务器,查看时间确实慢了或者快了。总之是有几台服务器时间不准确了。
2) 查看代码是使用的ScheduledExecutorService.scheduleAtFixedRate,Java的API,不至于这里存在Bug
3) 查看log4j日志输出发现:
12点的定时活动,之前的[活动运行时间]就是12点整;后面有几天的[活动运行时间]是12点零几分,而且分秒都一致
确认了一下,变化之间同步了一下服务器时间,但是没有重启jvm
4) 初步怀疑是ScheduledExecutorService内部执行使用的是相对时间,不是每次采样服务器系统时间
问题确认-测试:
--------------------------------------------------------------------------------
5) 测试
ScheduledExecutorService service = Executors.newScheduledThreadPool(2); service.scheduleAtFixedRate(new Runnable() { // Runnable-1 @Override public void run() { System.out.println( String.format(" #### %s ####", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S").format(new Date()))); } }, 0, 10, TimeUnit.SECONDS); // 10秒执行一次 service.scheduleAtFixedRate(new Runnable() { // Runnable-2 int i = 0; @Override public void run() { System.out.print( (i++) + "," ); } }, 0, 1, TimeUnit.SECONDS); // 1秒执行一次
输出-1:
0,
#### 2014-11-28 16:51:48.118 ####
1,2,3,4,5,6,7,8,9,
#### 2014-11-28 16:51:58.93 ####
10,11,12,13,14,15,16,17,18,19,20,
#### 2014-11-28 16:52:08.94 ####
21,22,23,24,25,26,27,28,29,
#### 2014-11-28 16:52:18.93 ####
30,31,32,33,34,35,36,37,38,39,
#### 2014-11-28 16:52:28.93 ####
40,41,42,43,44,45,46,47,48,49,
#### 2014-11-28 16:58:36.480 #### // 调整时间
50,51,52,53,54,55,56,57,58,59,
#### 2014-11-28 16:58:46.480 ####
60,61,62,63,64,65,66,67,68,69,
#### 2014-11-28 16:58:56.480 ####
在 16:52:28.93 时调整时间为16:58:36.480(向后跳), Runnable-2依然进行了10次输出,然后Runnable-1输出1次
输出-2:
0,
#### 2014-11-28 17:12:40.971 ####
1,2,3,4,5,6,7,8,9,
#### 2014-11-28 17:12:50.943 ####
10,11,12,13,14,15,16,17,18,19,
#### 2014-11-28 17:13:00.943 #### // 调整时间
20,21,22,23,24,25,26,27,28,29,
#### 2014-11-28 17:05:09.69 ####
30,31,32,33,34,35,36,37,38,39,
#### 2014-11-28 17:05:19.68 ####
在 17:13:00.943 时调整时间为17:05:09.69(向前跳), Runnable-2依然进行了10次输出,然后Runnable-1输出1次
测试结论: 时间的跳跃不影响 Runnable-1 10个秒单位输出一次, ScheduledExecutorService 没有使用系统时间
问题确认-JDK源码:
--------------------------------------------------------------------------------
初始化
ScheduledExecutorService service = Executors.newScheduledThreadPool(2); new ScheduledThreadPoolExecutor(corePoolSize) super(corePoolSize, Integer.MAX_VALUE, 0, TimeUnit.NANOSECONDS, new DelayedWorkQueue())
注册定时任务
service.scheduleAtFixedRate(new Runnable() {...}) RunnableScheduledFuture<?> t = decorateTask(command, new ScheduledFutureTask<Object>(command, null, triggerTime(initialDelay, unit), unit.toNanos(period))); // ScheduledThreadPoolExecutor.ScheduledFutureTask delayedExecute(t); prestartCoreThread addIfUnderCorePoolSize addThread Worker w = new Worker(firstTask); // ThreadPoolExecutor.Worker Thread t = threadFactory.newThread(w); workers.add(w); super.getQueue().add(command); // DelayedWorkQueue
执行
ThreadPoolExecutor.Worker.run task = getTask() r = workQueue.take(); // DelayedWorkQueue ScheduledThreadPoolExecutor.DelayedWorkQueue.take dq.take(); // DelayQueue<RunnableScheduledFuture>
DelayQueue<E extends Delayed>
Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。 该队列的头部 是延迟期满后保存时间最长的 Delayed 元素。 如果延迟都还没有期满,则队列没有头部,并且 poll 将返回 null。 当一个元素的 getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于等于 0 的值时,将发生到期。 即使无法使用 take 或 poll 移除未到期的元素,也不会将这些元素作为正常元素对待。 例如,size 方法同时返回到期和未到期元素的计数。此队列不允许使用 null 元素。 take() long delay = first.getDelay(TimeUnit.NANOSECONDS); if (delay > 0) { long tl = available.awaitNanos(delay); }
ScheduledThreadPoolExecutor.ScheduledFutureTask.getDelay
public long getDelay(TimeUnit unit) { return unit.convert(time - now(), TimeUnit.NANOSECONDS); } final long now() { /** * public static long nanoTime() * 返回最准确的可用系统计时器的当前值,以毫微秒为单位。 * 此方法只能用于测量已过的时间,与系统或钟表时间的其他任何时间概念无关。 * 返回值表示从某一固定但任意的时间算起的毫微秒数(或许从以后算起,所以该值可能为负)。 */ return System.nanoTime() - NANO_ORIGIN; }
问题确认-nanoTime测试:
--------------------------------------------------------------------------------
new Thread(){ public void run () { long lastNanos = System.nanoTime(); long lastMilis = System.currentTimeMillis(); for (int i = 0; i < 100; i++) { try { Thread.sleep(10000L); } catch (InterruptedException e) { } // 10秒钟输出一次 long nanos = System.nanoTime(); long millis = System.currentTimeMillis(); System.out.println( String.format("%-25s %-10s %s-%s = %s [%s]", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S").format(new Date(millis)), millis - lastMilis, nanos, lastNanos, nanos - lastNanos, (nanos - lastNanos) / 1000 / 1000 )); lastNanos = nanos; lastMilis = millis; } } }.start();
输出:
2014-12-05 10:10:45.950 10001 107273041687944-107263041775958 = 9999911986 [9999]
2014-12-05 10:10:55.997 10047 107283088127723-107273041687944 = 10046439779 [10046]
2014-12-05 10:20:04.198 548201 107293088578057-107283088127723 = 10000450334 [10000]
2014-12-05 10:00:12.399 -1191799 107303089035855-107293088578057 = 10000457798 [10000]
2014-12-05 10:00:22.399 10000 107313089495208-107303089035855 = 10000459353 [10000]
在 10:10:55.997 时调整时间为10:20:04.198(向后跳), 在 10:20:04.198 时调整时间为10:00:12.399(向前跳),
nanos - lastNanos总是维持在 10s左右, millis - lastMilis 确实期望中的时间
想法与目标:
--------------------------------------------------------------------------------
7) 我想做一个定时器,依赖于当前系统时间的定时器;顺便解决当前问题
a) 它不是定时任务,不是一个任务系统。
b) 它只做一件事情,到时间了提醒我做某个任务。
比如,12点了,提醒我该吃叫花鸡了;20点了,该打帮会战了。
初步设计:
--------------------------------------------------------------------------------
8) 初步设计
a) 首先创建一个任务线程池,用于执行定时器叫醒的任务。
Executors.newScheduledThreadPool(workServicePoolSize)
b) 创建一个定时器线程, 每隔1秒执行一次handleFunc(心跳步长1秒)
Executors.newSingleThreadScheduledExecutor
c) handleFunc根据当前系统时间查找到时的任务,把任务放置到任务线程池,由任务线程池执行
execute(new Runnable(){});
可以初步解决任务依赖系统时间来执行问题
详细设计:
--------------------------------------------------------------------------------
9) Linux系统时间会变快或者变慢,比如23点战力排行榜截止并在30分钟后开始领取奖励
a) 如果快的太多;大家当前都是22:55,但是服务器已经23:00,想等着最后冲榜的兄弟立马就哭了
b) 如果慢的太多;大家当前都是23:00,但是服务器才是22:55,我都休息了,准备开始领取奖励,你还能冲击战力榜
a) handleFunc每次执行后,记录一下当前执行时间为 lastExecuteTime
b) handleFunc下次执行的时候,拿 executeTime(当前时间) 和 lastExecuteTime比较一下
如果 executeTime == lastExecuteTime + 1: (心跳步长1秒)
正常时间,正常执行
如果 executeTime > lastExecuteTime + 1:
时间快了(通常是服务器时间慢了;校正服务器时间,服务器时间会快进), 需要处理一下:
[lastExecuteTime + 1, executeTime - 1]的任务 根据业务决定是否需要立马补执行
[executeTime]的任务 是当前时间正常任务,需要正常执行
如果 executeTime <= lastExecuteTime:
时间慢了(通常是服务器时间快了;校正服务器时间,服务器时间会回退):
[executeTime, lastExecuteTime] 都执行过了,一般不需要再执行了
注1:当前时间和lastExecuteTime都是抹去毫秒的,日常定时服务基于秒来计算足够了
注2: 服务器可以每隔1个小时同步一次时间,比方 NN:38,通常要避开整点、半点、整十分
注3: 每小时同步一次最多误差几秒而已,对于普通业务而言:
时间快了的情况下,立马补执行一下就可以了,比较重要的奖励提前或者延迟5秒发没有多少差别
时间慢了的情况下,可以忽视掉[executeTime, lastExecuteTime]间的任务,不需要再执行一次了
注4: 执行时间粒度比较小的,比方说1秒执行一次的,可以无视时间跳跃的问题
详细设计-定时任务:
--------------------------------------------------------------------------------
10) 这样用定时器的方案,可以解决时间跳跃的问题;但是日常开发通常是定时周期任务
比如, 12点吃叫花鸡,12点定时器通知吃叫花鸡;但是吃叫花鸡是每天12点都吃,这就是个定时周期任务,需要每天12点都
通知一下吃。处理方案可以如下:
a) 修改“handleFunc根据当前系统时间查找到时的任务”
b) 查找的任务仓库分为两类仓库:
一次性任务仓库:
到时间就执行任务,并移除; 任务仓库的存储的key是任务执行时间戳
每日的任务仓库:
任务仓库的存储的key是,任务相对于凌晨00:00:00的秒数
handleFunc执行时候,查看一下,今天过去了多少秒(这个是使用系统时间),找到对应任务,然后执行。这个不需要移除任务
这样依然是定时器的概念,“12点到时间了,我叫你去吃叫花鸡;明天12点到时间了,我再叫你去吃叫花鸡”,
而不是“12点了,我叫你去吃叫花鸡;24小时后我再叫你去吃鸡”。
注1: 同理可以增加每周、每小时、每分钟的任务仓库
注2: 通常不需要每月、每年、每十年等周期任务,如果需要加也很简单
注3: 每秒的,不需要这么多少事情, handleFunc 过了就直接执行(每次时间间隔是利用nano计算出来的1秒,所以应该执行)
接口设计:
--------------------------------------------------------------------------------
11) 对外接口:
a) 启动
b) 关闭
c) 注册一次性任务
d) 注册每周任务
e) 注册每日任务
f) 注册每时任务:每个小时的第几分、第几秒执行的什么任务
g) 注册每分任务
h) 注册每秒任务
其它说明:
--------------------------------------------------------------------------------
12) 其它:
a) handleFunc 只是找到任务把它扔进工作线程池执行,不怎么占用CPU,不会造成任务选取的堵塞
b) handleFunc 每次都会去 分钟的任务仓库查找合适的任务并执行;同一任务上一分钟没执行玩,当前任务也会继续执行,不会延迟,会同时执行
既然是每分钟任务,任务不应该超过1分钟;如果偶尔会超过1分钟,可以在注册的任务里面自行加锁
c) 服务器时间慢了;校正服务器时间,服务器时间会快进,补执行任务只补处理一定时间(比如30分钟)
系统时间是1小时同步一次,误差最大不过几秒;如果再大,就应该升级内核或者换服务器了
补执行的时间段过程,可能会影响正常服务(正常服务进程、系统资源占用等等)
d) 任务扔到线程池里面时候,会额外catch住,防止挂掉当前线程