在所有的服务器编程当中,定时任务永远是一个不可或缺的需求。
最直接的需求就是,每天凌晨0点0分的时候总是有一大堆的各种精力重置。
怎么来设计这个接口呢,想了几个方案:
- 每秒触发
- 每分钟触发
- 每整点触发
- 每天触发
- 每个月触发
oh no!不靠谱啊,如果这接口真设计成这样,得有多烂,灵光一现,unix下的crontab表达式非常完美的解决了这个问题。
附上crontab表达式的语法说明如下:
crontab特殊的符号说明:
"*"代表所有的取值范围内的数字。特别要注意哦!
"/"代表每的意思,如"*/5"表示每5个单位
"-"代表从某个数字到某个数字
","分散的数字
crontab文件的使用示例:
30 21 * * * 表示每晚的21:30
45 4 1,10,22 * * 表示每月1、10、22日的4 : 45
10 1 * * 6,0 表示每周六、周日的1 : 10
0,30 18-23 * * * 表示在每天18 : 00至23 : 00之间每隔30分钟
0 23 * * 6 表示每星期六的11 : 00 pm
* */1 * * * 每一小时
* 23-7/1 * * * 晚上11点到早上7点之间,每隔一小时
* 8,13 * * 1-5 从周一到周五的上午8点和下午1点
0 11 4 * mon-wed 每月的4号与每周一到周三的11点
0 4 1 jan * 一月一号的4点
看起来很复杂的样子,但其实够用就好,我们也不需要实现全部特性。
- 实现一个毫秒级别的定时器Update
- 根据这个update函数实现一个秒级别定时器
- 然后每秒取得自然时间与表达式中 分、时、几号、月份、星期几 分别匹配就可以实现了
- 由于定时器除了增加以外,可能还需要一个删除功能,那就再提供一个定时器命名的功能,用于增删改查定时器是本身
- 再加个测试函数。。完美
直接上代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
|
--------------------------------------------
--任何一个记录产生一个实例
local Clock = {}
local Clock_mt = {__index = Clock}
local
function
__checkPositiveInteger(name, value)
if
type(value) ~=
"number"
or value < 0 then
error(name ..
" must be a positive number"
)
end
end
--验证是否可执行
local
function
__isCallable(callback)
local tc = type(callback)
if
tc ==
'function'
then
return
true
end
if
tc ==
'table'
then
local mt = getmetatable(callback)
return
type(mt) ==
'table'
and type(mt.__call) ==
'function'
end
return
false
end
local
function
newClock(cid, name, time, callback, update, args)
assert(time)
assert(callback)
assert(__isCallable(callback),
"callback must be a function"
)
return
setmetatable({
cid = cid,
name = name,
time = time,
callback = callback,
args = args,
running = 0,
update = update
}, Clock_mt)
end
function
Clock:reset(running)
running = running or 0
__checkPositiveInteger(
'running'
, running)
self.running = running
self.deleted = nil --如果已经删除的,也要复活
end
local
function
updateEveryClock(self, dt)
__checkPositiveInteger(
'dt'
, dt)
self.running = self.running + dt
while
self.running >= self.time
do
self.callback(unpack(self.args))
self.running = self.running - self.time
end
return
false
end
local
function
updateAfterClock(self, dt) -- returns
true
if
expired
__checkPositiveInteger(
'dt'
, dt)
if
self.running >= self.time then
return
true
end
self.running = self.running + dt
if
self.running >= self.time then
self.callback(unpack(self.args))
return
true
end
return
false
end
local
function
match( left, right )
if
left ==
'*'
then
return
true
end
--单整数的情况
if
'number'
== type(left) and left == right then
return
true
end
--范围的情况 形如 1-12/5,算了,先不支持这种每隔几分钟的这种特性吧
_,_,a,b = string.find(left,
"(%d+)-(%d+)"
)
if
a and b then
return
(right >= tonumber(a) and right <= tonumber(b))
end
--多选项的情况 形如 1,2,3,4,5
--哎,luajit不支持gfind,
--
for
d
in
string.gfind(left,
"%d+"
)
do
--其实也可以
for
i
in
string.gmatch(left,
'(%d+)'
)
do
local pos = 0
for
st,sp
in
function
()
return
string.find(left,
','
, pos,
true
) end
do
if
tonumber(string.sub(left, pos, st - 1)) == right then
return
true
end
pos = sp + 1
end
return
tonumber(string.sub(left, pos)) == right
end
local
function
updateCrontab( self, dt )
local now = os.date(
'*t'
)
local tm = self.time
--print(
'updateCrontab/now:'
, now.min, now.hour, now.day, now.month, now.wday)
--print(
'updateCrontab/tm'
, tm.mn, tm.hr, tm.day, tm.mon, tm.wkd)
--print(
'match:'
,match(tm.mn, now.min), match(tm.hr, now.hour), match(tm.day, now.day), match(tm.mon, now.month), match(tm.wkd, now.wday))
if
match(tm.mn, now.min) and match(tm.hr, now.hour)
and match(tm.day, now.day) and match(tm.mon, now.month)
and match(tm.wkd, now.wday)
then
--print(
'matching'
,self.name,self.callback,self.running)
self.callback(unpack(self.args))
self.running = self.running + 1
end
return
false
end
--遍历并执行所有的定时器
local
function
updateClockTables( tbl )
for
i =
#tbl, 1, -1 do
local v = tbl[i]
if
v.deleted ==
true
or v:update(1) then
table.remove(tbl,i)
end
end
end
----------------------------------------------------------
local crontab = {}
crontab.__index = crontab
function
crontab.
new
( obj )
local obj = obj or {}
setmetatable(obj, crontab)
--执行一下构造函数
if
obj.ctor then
obj.ctor(obj)
end
return
obj
end
function
crontab:ctor( )
--所有的定时器
self._clocks = self._clocks or {}
self._crons = self._crons or {}
--累积的时间差
self._diff = self._diff or 0
--已命名的定时器,设置为弱引用表
self._nameObj = {}
setmetatable(self._nameObj, {__mode=
"k,v"
})
--取得现在的秒数,延迟到整点分钟的时候启动一个定时
self:after(
"__delayUpdateCrontab"
, 60-os.time()%60,
function
( )
--在整点分钟的时候,每隔一分钟执行一次
self:every(
"__updateCrontab"
, 60,
function
( )
updateClockTables(self._crons)
end)
end)
end
function
crontab:update( diff )
self._diff = self._diff + diff
while
self._diff >= 1000
do
--TODO:这里真让人纠结,要不要支持累积时间误差呢?
self._diff = self._diff - 1000
--开始对所有的定时器心跳,如果返回
true
,则从列表中移除
updateClockTables(self._clocks)
end
end
function
crontab:remove( name )
if
name and self._nameObj[name] then
self._nameObj[name].deleted =
true
end
end
--通过判断callback的真正位置,以及参数类型来支持可变参数
--返回值顺序 number, string, number,
function
, args
--总的有如下5种情况
--1) cid,name,time,callback,args
--2) name,cid,time,callback,args
--3) name,time,callback,args
--4) cid,time,callback,args
--5) time,callback,args
local
function
changeParamsName( p1, p2, p3, p4, p5 )
if
__isCallable(p4) then
if
type(p1) ==
'string'
then
return
p2,p1,p3,p4,p5
else
return
p1,p2,p3,p4,p5
end
elseif __isCallable(p3) then
if
type(p1) ==
'string'
then
return
nil,p1,p2,p3,p4
else
return
p1,nil,p2,p3,p4
end
else
return
nil,nil,p1,p2,p3
end
end
function
crontab:every( cid, name, time, callback, args )
--支持可变参数
cid, name, time, callback, args = changeParamsName(cid, name, time, callback,args)
__checkPositiveInteger(
'time'
, time)
local clock = newClock(cid, name, time, callback, updateEveryClock, args or {})
table.insert(self._clocks,clock)
if
name and name ~=
''
then
self._nameObj[name] = clock
end
return
clock
end
function
crontab:after( cid, name, time, callback, args )
cid, name, time, callback, args = changeParamsName(cid, name, time, callback,args)
__checkPositiveInteger(
'time'
, time)
local clock = newClock(cid, name, time, callback, updateAfterClock, args or {})
table.insert(self._clocks,clock)
if
name and name ~=
''
then
self._nameObj[name] = clock
end
return
clock
end
--增加计划任务,精度到达分钟级别
--表达式:分钟[0-59] 小时[0-23] 每月的几号[1-31] 月份[1-12] 星期几[1-7]
-- 星期天为1,
--
"*"
代表所有的取值范围内的数字
--
"-"
代表从某个数字到某个数字
--
"/"
代表每的意思,如
"*/5"
表示每5个单位,未实现
--
","
分散的数字
-- 如:
"45 4-23/5 1,10,22 * *"
function
crontab:addCron(cid, name, crontab_str, callback, args )
cid, name, crontab_str, callback, args = changeParamsName(cid, name, crontab_str, callback, args)
--print(cid, name, crontab_str, callback)
local t = {}
for
v
in
string.gmatch(crontab_str,
'[%w._/,%-*]+'
)
do
--如果可以转成整型直接转了,等下直接对比
local i = tonumber(v)
table.insert(t, i and i or v)
end
if
table.getn(t) ~= 5 then
return
error(string.format(
'crontab string,[%s] error!'
,crontab_str))
end
local time = {mn = t[1], hr = t[2], day = t[3], mon = t[4], wkd = t[5]}
local clock = newClock(cid, name, time, callback, updateCrontab, args or {})
table.insert(self._crons,clock)
if
name and name ~=
''
then
self._nameObj[name] = clock
end
end
return
crontab
|
再看看测试代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
--传说中的测试代码
local
function
RunTests()
-- the following calls are equivalent:
local
function
printMessage(a )
print(
'Hello'
,a)
end
local cron = crontab.
new
()
local c1 = cron:after( 5, printMessage)
local c2 = cron:after( 5, print, {
'Hello'
})
c1:update(2) -- will print nothing, the action is not done yet
c1:update(5) -- will print
'Hello'
once
c1:reset() -- reset the counter to 0
-- prints
'hey'
5 times and then prints
'hello'
while
not c1:update(1)
do
print(
'hey'
)
end
-- Create a periodical clock:
local c3 = cron:every( 10, printMessage)
c3:update(5) -- nothing (total time: 5)
c3:update(4) -- nothing (total time: 9)
c3:update(12) -- prints
'Hello'
twice (total time is now 21)
-------------------------------------
c1.deleted =
true
c2.deleted =
true
c3.deleted =
true
------------------------------
--测试一下match
print(
'----------------------------------'
)
assert(match(
'*'
,14) ==
true
)
assert(match(
'12-15'
,14) ==
true
)
assert(match(
'18-21'
,14) ==
false
)
assert(match(
'18,21'
,14) ==
false
)
assert(match(
'18,21,14'
,14) ==
true
)
--加一个定时器1分钟后执行
cron:update(1000)
--加入一个定时器每分钟执行
cron:addCron(
'每秒执行'
,
'* * * * *'
, print, {
'.......... cron'
})
cron:update((60-os.time()%60)*1000)
cron:update(30*1000)
cron:update(31*1000)
cron:update(1)
cron:update(60*1000) --打印两次
end
|
也可以直接到 https://github.com/linbc/crontab.lua 下载代码
参考资料:
http://www.cise.ufl.edu/~cop4600/cgi-bin/lxr/http/source.cgi/commands/simple/cron.c
https://github.com/kikito/cron.lua