由于下游的流量限制,經(jīng)常有這樣的需求毯焕,每一段時(shí)間只能有固定量的請(qǐng)求磺樱。多于的流量會(huì)造成服務(wù)不可用,或高延時(shí)芜辕。所以需要在上游做一些擁塞的控制活孩,于是就有了如題的需求。
我們將這個(gè)問題簡(jiǎn)化一下询兴,假設(shè)數(shù)據(jù)庫(kù)表里只有兩個(gè)字段:
------------
1 id(自增)
2 time
------------
我們需要實(shí)現(xiàn)的是诗舰,在大量插入請(qǐng)求過來時(shí)训裆,每3秒最多只有一條記錄寫成功蜀铲。
對(duì)分析過程不敢興趣的同學(xué)记劝,可以直接跳到最后的結(jié)論部分厌丑。
注:本例在mysql中實(shí)現(xiàn)渔呵。
建表語(yǔ)句如下:
create table test (
id int not null auto_increment,
time int not null,
primary key (`id`));
默認(rèn)為innodb。
第一版
最開始的實(shí)現(xiàn)是這樣的耕驰。分為兩步录豺,第一步獲得當(dāng)前的max time,之后判斷要插入的行的time是否大于max time厚骗,如果大于领舰,則將本行的時(shí)間修改為time+3迟螺,再插入,偽代碼大概是這樣的:
begin; //開啟事務(wù)
1 select max(time) max_time from test;
2 if cur_time > max_time:
insert into test(id, time) values(my_id, cur_time + 3);
end;
但是這樣會(huì)有問題锉桑,我們知道m(xù)ysql的select是不加鎖的窍株,是基于mvcc的讀球订。所以如果同時(shí)來兩個(gè)請(qǐng)求,它們?cè)诘谝徊侥玫较嗤膍ax_time微驶,都認(rèn)為可以繼續(xù)執(zhí)行步驟2,最后導(dǎo)致都執(zhí)行成功苟耻。這和我們題目要求的每3秒最多插入一條不符合扶檐,因此該方案不可取。
第二版
由于同一個(gè)3秒內(nèi)只能成功一條官卡,我們自然的想到通過unique_key的方式來實(shí)現(xiàn)。首先在time字段上增加unique索引哮翘,如下:
create unique index unique_time on test(time);
之后我們將用戶需要插入的時(shí)間按3秒取整,比如用戶插入的時(shí)間是4阻课, 按3秒取整后是3艰匙, 如果是6,按3秒取整后是6
3->3
4->3
5->3
6->6
7->6
....
這樣之后署驻,兩個(gè)用戶同時(shí)來旺上,第一步獲得max(time)糖埋,都可以更新,但在第二步征候,由于有unique_key的存在祟敛,只有一個(gè)會(huì)成功。偽代碼大概是這樣的:
begin;
1 select max(time) max_time from test;
2 if round(cur_time, 3) > max_time:
insert into test(id, time) values(my_id, round(cur_time, 3);
end;
初看上去卒煞,是挺好的一個(gè)解決方案畔裕,我們將一個(gè)連續(xù)的長(zhǎng)時(shí)間段,按段映射為了一個(gè)個(gè)固定的時(shí)間值扮饶,從數(shù)軸上看,每一段只能有一條記錄成功扛点。好像已經(jīng)完美的解決了需求陵究。但是奥帘,考慮這樣一種情況:
系統(tǒng)內(nèi)最大時(shí)間初始為0
第5秒的時(shí)候來了一個(gè)請(qǐng)求,取整為3松蒜,更新成功已旧。
第6秒的時(shí)候又來了一個(gè)請(qǐng)求,取整為6惊楼,更新成功胁后。
但是嗦枢,這之間的更新間隔只有1秒。換句話說侣诺,這種方案只滿足了平均意義上的沒每3秒插入一條氧秘,但是沒有解決嚴(yán)格的每3秒插入一條這個(gè)條件。所以還是不行的搔确。
第三版
有了前面兩種失敗的方案做鋪墊膳算,我們自然的想到把這兩種方案結(jié)合一下。繼續(xù)在unique_key上做文章华匾,但是這次机隙,我們將time的更新策略變一下,如果滿足插入條件旭旭,直接插入max_time + 3印颤,偽代碼如下:
begin;
1 select max(time) max_time from test;
2 if cur_time > max_time:
insert into test(id, time) values(my_id, max_time + 3);
end;
這樣已經(jīng)可以滿足題目中的要求了年局。
其實(shí)在細(xì)細(xì)想一下矢否,好像脑溢,還是有問題。
假設(shè)當(dāng)前max(time)為6验庙,5秒之后社牲,來了一個(gè)請(qǐng)求搏恤,cur_time為11,之后max_time被更新為6+3 = 9藤巢,1秒之后又來了個(gè)請(qǐng)求息罗,12 > 9,再次更新成功绍刮。
所以這個(gè)方案還是不可行的。
結(jié)論:
第四版
有了前面的失敗經(jīng)驗(yàn)捌木,這次刨裆,我們將unique_key從time上取下來彬檀,在數(shù)據(jù)庫(kù)里單獨(dú)增加一列dup_id,如下:
alter table test add column dup_id int not null;
create unique index unique_dup_id on test(dup_id);
然后偽代碼如下:
begin;
1 select max(time) max_time, max(dup_id) max_dup_id from test;
2 if (cur_time > max_time):
insert into test (id, time, dup_id) values(my_id, cur_time + 3, max_dup_id + 1)
end;
其實(shí)這兩步操作完全沒必要放在一個(gè)事務(wù)中努潘,可以寫成獨(dú)立的兩條語(yǔ)句坤学。
如下:
1 select max(time) max_time, max(dup_id) max_dup_id from test;
2 if (cur_time > max_time):
insert into test (id, time, dup_id) values(my_id, cur_time + 3, max_dup_id + 1)
第五版(簡(jiǎn)化版)
如果還是覺得添加一列蠻復(fù)雜,有一種簡(jiǎn)單一些的方案压怠,使用事務(wù)來完成菌瘫,主體思路是將max(time)鎖起來,偽代碼如下:
begin;
1 select max(time) max_time from test for update;
2 if (cur_time > max_time):
insert into test(id, time) values(my_id, cur_time+3);
end;
這種for update的方式需要注意:
1 time需要有索引雨让,如果沒有索引忿等,則鎖全表
2 當(dāng)time上有索引時(shí),鎖[max_time, +∞](間隙鎖)娃闲,鎖住一個(gè)范圍