場(chǎng)景
場(chǎng)景1
留言功能限制染突,30秒內(nèi)只能評(píng)論10次唁情,超出次數(shù)不讓能再評(píng)論啊终,并提示:過于頻繁
場(chǎng)景2
點(diǎn)贊功能限制镜豹,10秒內(nèi)只能點(diǎn)贊10次,超出次數(shù)后不能再點(diǎn)贊蓝牲,并封印1個(gè)小時(shí)趟脂,提示:過于頻繁,被禁止操作1小時(shí)
場(chǎng)景3
上傳記錄功能例衍,需要限制一天只能上傳 100次昔期,超出次數(shù)不讓能再上傳,并提示:超出今日上線
抽離本質(zhì)
在業(yè)務(wù)開發(fā)的過程中佛玄,我們不斷的參與各種業(yè)務(wù)場(chǎng)景的方案設(shè)計(jì)硼一,往往很容易碰到很類似的場(chǎng)景,只不過當(dāng)前所屬的業(yè)務(wù)模塊不一樣梦抢,其實(shí)這些需求的本質(zhì)是解決同一個(gè)問題般贼,當(dāng)我們遇到這種場(chǎng)景的時(shí)候,我們需要根據(jù)自己經(jīng)驗(yàn)分析抽離出需求的本質(zhì)問題奥吩,實(shí)現(xiàn)一個(gè)通用的解決方案哼蛆,這可能就是區(qū)別于你是有靈魂的工程師還是cp(copy paste)最強(qiáng)王者吧。
通過分析上面的需求場(chǎng)景霞赫,其實(shí)他們有很多相似的地方腮介,我們可以把需求場(chǎng)景抽離成:
時(shí)間范圍X秒內(nèi)
限制操作數(shù)Y次
超出封印時(shí)間Z(秒/具體時(shí)間)
超出不讓再操作,并提示
(最小時(shí)間單位用秒:天/小時(shí)/分鐘都可換算成秒端衰,用秒可以解決更多的場(chǎng)景)
如果把功能抽離成一個(gè)通用函數(shù)是不是大概是這樣:
解決方案落地
功能需要進(jìn)行用戶時(shí)間內(nèi)叠洗,操作動(dòng)作,操作次數(shù)存儲(chǔ)旅东,失效過期的清理惕味,這里主角:redis 終于登場(chǎng)了,基于redis特性玉锌,incr的原子操作和key 支持過期機(jī)制名挥,內(nèi)存存儲(chǔ)的效率優(yōu)勢(shì),可以相對(duì)簡(jiǎn)單靈活并且又高效的完成目的主守。
通用功能的代碼實(shí)現(xiàn):
/**
* 頻率限制
* @param string $action 操作動(dòng)作
* @param int $userId 發(fā)起操作的用戶ID
* @param int $time 時(shí)間范圍X秒內(nèi)
* @param int $number 限制操作數(shù)Y次
* @param array $expire? 超出封印時(shí)間Z ['type'=>1,'ttl'=>過期時(shí)間/秒] ['type'=>2,'ttl'=>具體過期時(shí)間戳] 二選一
* @return bool
* @throws \Exception
*/
public function frequencyLimit(string $action, int $userId, int $time, int $number, $expire = [])
{
if (empty($action) || $userId <= 0 || $time <= 0 || $number <= 0) {
throw new \Exception('非法參數(shù)');
}
$key = 'act:limit:' . $action . ':' . $userId;
$r = RedisClient::connect();
//獲取當(dāng)前累計(jì)次數(shù)
$current = intval($r->get($key));
if ($current >= $number) return false;
//累計(jì)并返回最新值
$current = $r->incr($key);
//第一次累加禀倔,設(shè)置控制操作頻率的有效時(shí)間
if ($current === 1) $r->expire($key, $time);
//未超出限制次數(shù)先放過
if ($current < $number) return true;
//超出后根據(jù)需要重新設(shè)置過期失效時(shí)間 $current === $number 判斷保證只重新設(shè)置一次
$type = empty($expire['type']) ? 0 : intval($expire['type']);
$ttl = empty($expire['ttl']) ? 0 : intval($expire['ttl']);
if ($current === $number && $ttl > 0 && in_array($type, [1, 2])) {
if ($type === 1) $r->expire($key, $ttl);
if ($type === 2) $r->expireAt($key, $ttl);
}
return false;
}
//場(chǎng)景1
/**
* 評(píng)論限制
* @param int $userId
* @return bool|string
*/
public function doComment(int $userId)
{
try {
$pass = FrequencyLimit::doHandle('comment', $userId, 30, 10);
if (!$pass) return '過于頻繁';
// todo 評(píng)論邏輯
return true;
} catch (\Exception $e) {
return $e->getMessage();
}
}
//場(chǎng)景2
/**
* 點(diǎn)贊限制
* @param int $userId
* @return bool|string
*/
public function doLike(int $userId)
{
try {
$pass = FrequencyLimit::doHandle('like', $userId, 10, 10, ['type' => 1, 'ttl' => 1 * 60 * 60]);
if (!$pass) return '過于頻繁榄融,被禁止操作1小時(shí)';
// todo 點(diǎn)贊邏輯
return true;
} catch (\Exception $e) {
return $e->getMessage();
}
}
//場(chǎng)景3
/**
* 上傳限制
* @param int $userId
* @return bool|string
*/
public function doUpload(int $userId)
{
try {
$expire = strtotime(date('Y-m-d', strtotime(+1 . 'days')));
$pass = FrequencyLimit::doHandle('upload', $userId, 1 * 24 * 60 * 60, 100, ['type' => 2, 'ttl' => $expire]);
if (!$pass) return '超出今日上線';
// todo 上傳邏輯
return true;
} catch (\Exception $e) {
return $e->getMessage();
}
}
總結(jié)
對(duì)相似的業(yè)務(wù)場(chǎng)景進(jìn)行分析,發(fā)現(xiàn)本質(zhì)問題并設(shè)計(jì)通用的解決方案
基于redis特性救湖,相對(duì)簡(jiǎn)單的實(shí)現(xiàn)通用的頻率限制功能