page contents

Redis实战之限制操作频率

对相似的业务场景进行分析,发现本质问题并设计通用的解决方案 基于redis特性,相对简单的实现通用的频率限制功能

attachments-2020-06-NuAFhodu5ee31199194a2.png

场景


场景1

留言功能限制,30秒内只能评论10次,超出次数不让能再评论,并提示:过于频繁

场景2

点赞功能限制,10秒内只能点赞10次,超出次数后不能再点赞,并封印1个小时,提示:过于频繁,被禁止操作1小时

场景3

上传记录功能,需要限制一天只能上传 100次,超出次数不让能再上传,并提示:超出今日上线


抽离本质


在业务开发的过程中,我们不断的参与各种业务场景的方案设计,往往很容易碰到很类似的场景,只不过当前所属的业务模块不一样,其实这些需求的本质是解决同一个问题,当我们遇到这种场景的时候,我们需要根据自己经验分析抽离出需求的本质问题,实现一个通用的解决方案,这可能就是区别于你是有灵魂的工程师还是cp(copy paste)最强王者吧。


勾画功能逻辑流程图,方便理解:

attachments-2020-06-IEi7xDnT5ee311b7bf738.jpg

通过分析上面的需求场景,其实他们有很多相似的地方,我们可以把需求场景抽离成:

  • 时间范围X秒内
  • 限制操作数Y次
  • 超出封印时间Z(秒/具体时间)
  • 超出不让再操作,并提示

attachments-2020-06-0KJv2BQO5ee311ddb301f.jpg

(最小时间单位用秒:天/小时/分钟都可换算成秒,用秒可以解决更多的场景)


如果把功能抽离成一个通用函数是不是大概是这样:

<?php
/**
 * 频率限制
 * @param string $action 操作动作
 * @param int $userId 发起操作的用户ID
 * @param int $time 时间范围X秒内
 * @param int $number 限制操作数Y次
 * @param array $expire 超出封印时间Z ['type'=>1,'ttl'=>过期时间/秒] ['type'=>2,'ttl'=>具体过期时间戳] 二选一
 * @return bool
 * @throws \Exception
 */
public static function frequencyLimit(string $action, int $userId, int $time, int $number, $expire = [])
{
    // todo 根据用户操作动作时间范围,进行频率的控制和失效释放
}


解决方案落地


功能需要进行用户时间内,操作动作,操作次数存储,失效过期的清理,这里主角:redis 终于登场了,基于redis特性,incr的原子操作和key 支持过期机制,内存存储的效率优势,可以相对简单灵活并且又高效的完成目的。

通用功能的代码实现:

<?php
/**
 * 频率限制
 * @param string $action 操作动作
 * @param int $userId 发起操作的用户ID
 * @param int $time 时间范围X秒内
 * @param int $number 限制操作数Y次
 * @param array $expire  超出封印时间Z ['type'=>1,'ttl'=>过期时间/秒] ['type'=>2,'ttl'=>具体过期时间戳] 二选一
 * @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('非法参数');
    }
    $key = 'act:limit:' . $action . ':' . $userId;
    $r = RedisClient::connect();
    //获取当前累计次数
    $current = intval($r->get($key));
    if ($current >= $number) return false;
    //累计并返回最新值
    $current = $r->incr($key);
    //第一次累加,设置控制操作频率的有效时间
    if ($current === 1) $r->expire($key, $time);
    //未超出限制次数先放过
    if ($current < $number) return true;
    //超出后根据需要重新设置过期失效时间 $current === $number 判断保证只重新设置一次
    $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;
}
//场景1

/**
 * 评论限制
 * @param int $userId
 * @return bool|string
 */
public function doComment(int $userId)
{
    try {
        $pass = FrequencyLimit::doHandle('comment', $userId, 30, 10);
        if (!$pass) return '过于频繁';
        // todo 评论逻辑
        return true;
    } catch (\Exception $e) {
        return $e->getMessage();
    }
}

//场景2
/**
 * 点赞限制
 * @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小时';
        // todo 点赞逻辑
        return true;
    } catch (\Exception $e) {
        return $e->getMessage();
    }
}

//场景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();
    }
}

//场景N

编码上可以根据你设计这个通用方案的复杂度进行进一步抽象,如抽象成频率限制的功能类等


总结


对相似的业务场景进行分析,发现本质问题并设计通用的解决方案

基于redis特性,相对简单的实现通用的频率限制功能


attachments-2020-06-vGiSuOtt5ee3118356bcc.jpg

  • 发表于 2020-06-12 13:26
  • 阅读 ( 540 )

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1478 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章