玩家數據重置問題的思考

問題描述:有一個參與度很高的活動,玩家的數據是每天重置的。時常有玩家抱怨,他們的活動數據會無緣無故地重置。經常就是,半夜玩了一段時間,白天再上線,打開界面看到,奮鬥的成果全沒了!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秒!

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章