玩家数据重置问题的思考

问题描述:有一个参与度很高的活动,玩家的数据是每天重置的。时常有玩家抱怨,他们的活动数据会无缘无故地重置。经常就是,半夜玩了一段时间,白天再上线,打开界面看到,奋斗的成果全没了!What?

先简要介绍该活动的重置机制实现。(以下为Erlang)

-module(activity).

-export([get_data/1]).

-record(act, {
time = 0 :: integer(), % 玩家当天首次进入该活动的时间(Unix时间戳)
data = [] :: list() % 玩家的活动数据
}).


stamp() ->
{MS, S, _} = os:timestamp(),
MS * 1000000 + S.

%% 当天0点的Unix时间戳
midnight() ->
stamp() - calendar:time_to_seconds(time()).

need_reset(Time) ->
Time < midnight().

do_reset(Act) ->
Act#act{time = stamp(), data = []}.

%% 获取玩家数据
get_data(Act) ->
Time = Act#act.time,
case need_reset(Time) of
true ->
do_reset(Act);
false ->
Act
end.
 

 

判断是否需要重置的标准:每次获取玩家数据时,若Time为昨天或者更早之前的时间,则认为这是过期的数据,需要重置。

 

咋一看,此逻辑并没有问题。然而,有一次查到了某玩家的数据,在2点的时候是这样的:

#act{
time = 1570464000, % 2019年10月8日00:00:00
data = [1, 2, 3, 4]
}.

而在13点的时候是这样的:

#act{
time = 1570504000, % 2019年10月8日11:06:40
data = [1, 2]
}.

说明玩家半夜时在线,11点多时也在线。

为什么会这样呢?玩家数据不应该被重置啊!于是,推测need_reset(Time)

会以极小的概率为true,从而导致重置。为了验证猜想,设计了以下的代码。

-module(test_reset).

-export([loop/0]).

stamp() ->
{MS, S, _} = os:timestamp(),
MS * 1000000 + S.

%% 当天0点的Unix时间戳
midnight() ->
stamp() - calendar:time_to_seconds(time()).

%% 判断某个时间戳是否某天0点。
%% 由于服务器时间是北京时间,东八区,所以加8小时。
is_some_midnight(Time) ->
(Time + 8 * 60 * 60) rem 24 * 60 * 60 =:= 0.

loop() ->
Midnight = midnight(),
case is_some_midnight(Midnight) of
true ->
ok;
false ->
io:format("~p,", [Midnight])
end,
loop().

 

1> c(test_reset).

{ok,test_reset}

2> test_reset:loop().

1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,1570550401,

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded

(v)ersion (k)ill (D)b-tables (d)istribution

q

果真如此。过了几秒之后,test_reset:loop().就有输出了。1570464001 是2019年10月8日00:00:01。这种情况下,need_reset(Time)为false。

再验证下,具体如何算多了1秒。

loop(N) ->
Os = {MS, S, _} = os:timestamp(),
Time = time(),
Stamp = MS * 1000000 + S,
Midnight = Stamp - calendar:time_to_seconds(Time),
case is_some_midnight(Midnight) of
true ->
ok;
false ->
io:format("~p,~p,~p,~p~n", [N, Os, Time, Midnight])
end,
loop(N + 1).

1> c(test_reset).

{ok,test_reset}

2> test_reset:loop(1).

1825750,{1570,585715,0},{9,48,34},1570550401

1825751,{1570,585715,251},{9,48,34},1570550401

1825752,{1570,585715,363},{9,48,34},1570550401

1825753,{1570,585715,536},{9,48,34},1570550401

1825754,{1570,585715,626},{9,48,34},1570550401

1825755,{1570,585715,714},{9,48,34},1570550401

1825756,{1570,585715,795},{9,48,34},1570550401

1825757,{1570,585715,877},{9,48,34},1570550401

1825758,{1570,585715,980},{9,48,34},1570550401

1825759,{1570,585715,1063},{9,48,34},1570550401

1825760,{1570,585715,1143},{9,48,34},1570550401

1825761,{1570,585715,1205},{9,48,34},1570550401

1825762,{1570,585715,1256},{9,48,34},1570550401

1825763,{1570,585715,1310},{9,48,34},1570550401

1825764,{1570,585715,1360},{9,48,34},1570550401

1825765,{1570,585715,1408},{9,48,34},1570550401

1825766,{1570,585715,1458},{9,48,34},1570550401

1825767,{1570,585715,1507},{9,48,34},1570550401

1825768,{1570,585715,1555},{9,48,34},1570550401

1825769,{1570,585715,1611},{9,48,34},1570550401

1825770,{1570,585715,1660},{9,48,34},1570550401

1825771,{1570,585715,1709},{9,48,34},1570550401

1825772,{1570,585715,1757},{9,48,34},1570550401

1825773,{1570,585715,1811},{9,48,34},1570550401

1825774,{1570,585715,1864},{9,48,34},1570550401

1825775,{1570,585715,1913},{9,48,34},1570550401

1825776,{1570,585715,1960},{9,48,34},1570550401

1825777,{1570,585715,2041},{9,48,34},1570550401

1825778,{1570,585715,2088},{9,48,34},1570550401

1825779,{1570,585715,2138},{9,48,34},1570550401

1825780,{1570,585715,2183},{9,48,34},1570550401

1825781,{1570,585715,2227},{9,48,34},1570550401

1825782,{1570,585715,2270},{9,48,34},1570550401

1825783,{1570,585715,2312},{9,48,34},1570550401

1825784,{1570,585715,2355},{9,48,34},1570550401

1825785,{1570,585715,2399},{9,48,34},1570550401

1825786,{1570,585715,2441},{9,48,34},1570550401

1825787,{1570,585715,2483},{9,48,34},1570550401

1825788,{1570,585715,2553},{9,48,34},1570550401

1825789,{1570,585715,2600},{9,48,34},1570550401

1825790,{1570,585715,2644},{9,48,34},1570550401

3655710,{1570,585716,0},{9,48,35},1570550401

3655711,{1570,585716,296},{9,48,35},1570550401

3655712,{1570,585716,471},{9,48,35},1570550401

3655713,{1570,585716,560},{9,48,35},1570550401

3655714,{1570,585716,671},{9,48,35},1570550401

3655715,{1570,585716,756},{9,48,35},1570550401

3655716,{1570,585716,845},{9,48,35},1570550401

3655717,{1570,585716,926},{9,48,35},1570550401

3655718,{1570,585716,1007},{9,48,35},1570550401

3655719,{1570,585716,1087},{9,48,35},1570550401

3655720,{1570,585716,1168},{9,48,35},1570550401

3655721,{1570,585716,1231},{9,48,35},1570550401

3655722,{1570,585716,1282},{9,48,35},1570550401

3655723,{1570,585716,1332},{9,48,35},1570550401

3655724,{1570,585716,1387},{9,48,35},1570550401

3655725,{1570,585716,1465},{9,48,35},1570550401

3655726,{1570,585716,1516},{9,48,35},1570550401

3655727,{1570,585716,1562},{9,48,35},1570550401

3655728,{1570,585716,1612},{9,48,35},1570550401

3655729,{1570,585716,1659},{9,48,35},1570550401

3655730,{1570,585716,1705},{9,48,35},1570550401

3655731,{1570,585716,1750},{9,48,35},1570550401

3655732,{1570,585716,1793},{9,48,35},1570550401

3655733,{1570,585716,1842},{9,48,35},1570550401

3655734,{1570,585716,1886},{9,48,35},1570550401

3655735,{1570,585716,1929},{9,48,35},1570550401

3655736,{1570,585716,1972},{9,48,35},1570550401

3655737,{1570,585716,2016},{9,48,35},1570550401

3655738,{1570,585716,2059},{9,48,35},1570550401

3655739,{1570,585716,2103},{9,48,35},1570550401

3655740,{1570,585716,2146},{9,48,35},1570550401

3655741,{1570,585716,2189},{9,48,35},1570550401

3655742,{1570,585716,2235},{9,48,35},1570550401

3655743,{1570,585716,2278},{9,48,35},1570550401

3655744,{1570,585716,2321},{9,48,35},1570550401

3655745,{1570,585716,2365},{9,48,35},1570550401

3655746,{1570,585716,2424},{9,48,35},1570550401

3655747,{1570,585716,2538},{9,48,35},1570550401

3655748,{1570,585716,2594},{9,48,35},1570550401

3655749,{1570,585716,2644},{9,48,35},1570550401

5481341,{1570,585717,0},{9,48,36},1570550401

5481342,{1570,585717,240},{9,48,36},1570550401

5481343,{1570,585717,339},{9,48,36},1570550401

5481344,{1570,585717,423},{9,48,36},1570550401

5481345,{1570,585717,511},{9,48,36},1570550401

5481346,{1570,585717,594},{9,48,36},1570550401

5481347,{1570,585717,676},{9,48,36},1570550401

 

BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded

(v)ersion (k)ill (D)b-tables (d)istribution

q

在CPU非常繁忙的情况下,连续几百万次的调用,os:timestamp()time()的输出偶尔就不一致了。看了下文档和底层代码,它们的实现方式是不同的。连续两次调用时间函数,不一定能得到想要的结果。

 

修改此函数,改成只调用一次时间函数。

midnight1() ->
Timestamp = os:timestamp(),
{Date, _} = calendar:now_to_local_time(Timestamp),
Base = calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}),
LocalDate = calendar:local_time_to_universal_time({Date, {0, 0, 0}}),
calendar:datetime_to_gregorian_seconds(LocalDate) - Base.

出错概率会降低,但每次获取玩家数据时都要计算当天0点的时间戳,仍然不靠谱。也该考虑下其他实现方式了。

 

 

思考:

在OTP 17及以前的文档中(http://erlang.org/documentation/doc-6.4/erts-6.4/doc/html/time_correction.html),有一段是

2.4  Should I use erlang:now/0 or os:timestamp/0

The simple answer is to use erlang:now/0 for everything where you want to keep real time characteristics, but use os:timestamp for things like logs, user communication and debugging (typically timer:ts uses os:timestamp, as it is a test tool, not a real world application API). The benefit of using os:timestamp/0 is that it's faster and does not involve any global state (unless the operating system has one). The downside is that it will be vulnerable to wall clock time changes.

大概说,需要真正的时间特性时用erlang:now/0(单调递增,间隔稳定,接近于wall clock,会校准),在日志,与用户通信,调试时用 os:timestamp/0。后者当然比较快,但是wall clock时间变化时就有点不妙了。

 

在OTP 18及之后(http://erlang.org/documentation/doc-7.0/erts-7.0/doc/html/time_correction.html)说,However, you are strongly encouraged to use the new API instead of the old API based on erlang:now/0. erlang:now/0 has been deprecated since it is and forever will be a scalability bottleneck.

 

Don't

use erlang:now/0 in order to retrieve current Erlang system time.

Do

use erlang:system_time/1 in order to retrieve current Erlang system time on the time unit of your choice.

If you want the same format as returned by erlang:now/0, use erlang:timestamp/0.

新的时间API出来了。不要用 erlang:now/0,会带来系统的瓶颈。用 erlang:system_time/1或者 erlang:timestamp/0。使用erlang:system_time(seconds).就可以获取Unix时间戳了,不需要再又加法又乘法的了。

 

 

我的感想:

普通机器的时间永远都是不准的。我的跑步专属电子表,每两个月会快1分钟。尊贵的劳力士,偶尔也要看电视对对时。手机,Windows系统的电脑,默认都会开启网络时间同步。连Linux机子,运维也要开启ntp时间同步的定时任务。不信可以把时间同步关掉试试,看看一个月之后与实际时间相差多少,此举还能检测机器的质量呢。要有正确的时间观念哟!

 

重要!在一个函数内或者连续的代码中多次调用时间函数,并假设它们的返回值都一样的(尽管以秒为单位)。这是危险的。有一种可能就是,第一个时间函数在上一秒的末尾执行,第二个时间函数在下一秒的开始执行。这种情况下,返回值会相差1秒!

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值