由于下游的流量限制,經(jīng)常有這樣的需求,每一段時(shí)間只能有固定量的請(qǐng)求。多于的流量會(huì)造成服務(wù)不可用,或高延時(shí)。所以需要在上游做一些擁塞的控制,于是就有了如題的需求。
我們將這個(gè)問(wèn)題簡(jiǎn)化一下,假設(shè)數(shù)據(jù)庫(kù)表里只有兩個(gè)字段:
------------
1 id(自增)
2 time
------------
我們需要實(shí)現(xiàn)的是,在大量插入請(qǐng)求過(guò)來(lái)時(shí),每3秒最多只有一條記錄寫(xiě)成功。
對(duì)分析過(guò)程不敢興趣的同學(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。
第一版
最開(kāi)始的實(shí)現(xiàn)是這樣的。分為兩步,第一步獲得當(dāng)前的max time,之后判斷要插入的行的time是否大于max time,如果大于,則將本行的時(shí)間修改為time+3,再插入,偽代碼大概是這樣的:
begin; //開(kāi)啟事務(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ì)有問(wèn)題,我們知道m(xù)ysql的select是不加鎖的,是基于mvcc的讀。所以如果同時(shí)來(lái)兩個(gè)請(qǐng)求,它們?cè)诘谝徊侥玫较嗤膍ax_time,都認(rèn)為可以繼續(xù)執(zhí)行步驟2,最后導(dǎo)致都執(zhí)行成功。這和我們題目要求的每3秒最多插入一條不符合,因此該方案不可取。
第二版
由于同一個(gè)3秒內(nèi)只能成功一條,我們自然的想到通過(guò)unique_key的方式來(lái)實(shí)現(xiàn)。首先在time字段上增加unique索引,如下:
create unique index unique_time on test(time);
之后我們將用戶(hù)需要插入的時(shí)間按3秒取整,比如用戶(hù)插入的時(shí)間是4, 按3秒取整后是3, 如果是6,按3秒取整后是6
3->3
4->3
5->3
6->6
7->6
....
這樣之后,兩個(gè)用戶(hù)同時(shí)來(lái),第一步獲得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í)候來(lái)了一個(gè)請(qǐng)求,取整為3,更新成功。
第6秒的時(shí)候又來(lái)了一個(gè)請(qǐng)求,取整為6,更新成功。
但是,這之間的更新間隔只有1秒。換句話(huà)說(shuō),這種方案只滿(mǎn)足了平均意義上的沒(méi)每3秒插入一條,但是沒(méi)有解決嚴(yán)格的每3秒插入一條這個(gè)條件。所以還是不行的。
第三版
有了前面兩種失敗的方案做鋪墊,我們自然的想到把這兩種方案結(jié)合一下。繼續(xù)在unique_key上做文章,但是這次,我們將time的更新策略變一下,如果滿(mǎn)足插入條件,直接插入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)可以滿(mǎn)足題目中的要求了。
其實(shí)在細(xì)細(xì)想一下,好像,還是有問(wèn)題。
假設(shè)當(dāng)前max(time)為6,5秒之后,來(lái)了一個(gè)請(qǐng)求,cur_time為11,之后max_time被更新為6+3 = 9,1秒之后又來(lái)了個(gè)請(qǐng)求,12 > 9,再次更新成功。
所以這個(gè)方案還是不可行的。
結(jié)論:
第四版
有了前面的失敗經(jīng)驗(yàn),這次,我們將unique_key從time上取下來(lái),在數(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í)這兩步操作完全沒(méi)必要放在一個(gè)事務(wù)中,可以寫(xiě)成獨(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)化版)
如果還是覺(jué)得添加一列蠻復(fù)雜,有一種簡(jiǎn)單一些的方案,使用事務(wù)來(lái)完成,主體思路是將max(time)鎖起來(lái),偽代碼如下:
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需要有索引,如果沒(méi)有索引,則鎖全表
2 當(dāng)time上有索引時(shí),鎖[max_time, +∞](間隙鎖),鎖住一個(gè)范圍