账号层级限流-抢白名单功能

功能说明

应用场景

数据库高负荷时,在员工账号层级进行限流,目的是让数据库尽早缓冲过来

特色功能

  1. 支持临时 追加/减少 白名单名额
  2. 支持重置白名单
  3. 直接配置化,不需要操作数据库

源代码

// config/white_list.php
<?php

// 抢白名单配置,白名单内的员工允许访问 xxx 系统,其他员工则跳转到登陆界面
// 变更配置后,记得执行 php artisan optimize
return [
    // 是否启用
    'is_enable'  =>  env('WHITE_LIST_IS_ENABLE', 0),

    // 白名单名额,支持临时追加白名单名额,但不支持减少操作
    'number'  =>  env('WHITE_LIST_NUMBER', 100),

    // 缓存 key 前缀,变更后会重置白名单
    'cache_key_prefix'  =>  env('WHITE_LIST_CACHE_KEY_PREFIX', 'first'),

    // 缓存 key
    'cache_key_list_init'  =>  '_xxx_white_list_init',
    'cache_key_list'  =>  '_xxx_white_list',
    'cache_key_hash'  =>  '_xxx_white_hash'
    'cache_ttl'  =>  43200,

    // 员工没在白名单内的提示文案,并跳转到登陆界面
    'error_msg'  =>  'xxx 系统暂不可用,请稍晚一些再登陆使用,谢谢配合!',
];

// Services/RedisHelper.php
<?php

namespace  App\Services;

use Illuminate\Support\Facades\Redis;

class  RedisHelper
{
    /**
    * redis排他锁:拒绝并发相同的请求
    * @param  string $key
    * @param $value
    * @param  int $ttl
    * @return  bool
    */
    public static function setNxWithTTL(string $key, $value, int $ttl =  300):  bool
    {
        if ((Redis::connection('default'))->set($key, $value, 'ex', $ttl, 'nx')) {
            return true;
        }
        return false;
    }
}
/**
* 核心代码~判断该员工是否在白名单内:只允许部分员工可以正常使用 xxx 系统,其他人则跳转到登陆界面
* @param  int $staffId
* @return  bool 返回 true 表示该员工在白名单内,允许正常使用 xxx 系统
*/
public function isInWhiteList(int $staffId): bool
{
    // 是否启用白名单功能
    if (!config('white_list.is_enable')) {
        return true;
    }

    // 白名单名额
    $whiteListNumber = (int)config('white_list.number');
    if ($whiteListNumber <=  0) {
        return false;
    }

    $redis =  Redis::connection('default');
    $cacheKeyPrefix =  config('white_list.cache_key_prefix');
    $cacheKeyListInit = $cacheKeyPrefix .  config('white_list.cache_key_list_init');
    $cacheKeyList = $cacheKeyPrefix .  config('white_list.cache_key_list');
    $cacheKeyHash = $cacheKeyPrefix .  config('white_list.cache_key_hash');
    $cacheTTL =  config('white_list.cache_ttl');

    // 判断该员工是否在白名单中
    if ($redis->exists($cacheKeyHash) && $redis->hexists($cacheKeyHash, $staffId)) {
        return true;
    }

    // 初始化令牌队列
    if (!$redis->exists($cacheKeyListInit)) {
        if (!RedisHelper::setNxWithTTL("{$cacheKeyListInit}_nx", 1, config('cache.one_hour'))) {
            return true;
        }

        $redis->rpush($cacheKeyListInit, array_fill(0, $whiteListNumber, 1));
        $redis->expire($cacheKeyListInit, $cacheTTL);
    }

    // 支持临时追加白名单名额
    $length = $redis->llen($cacheKeyListInit);
    if ($length < $whiteListNumber) {
        if (!RedisHelper::setNxWithTTL("{$cacheKeyListInit}_nx_{$whiteListNumber}", 1, config('cache.one_hour'))) {
            return false;
        }

        $redis->rpush($cacheKeyListInit, array_fill($length -  1, $whiteListNumber - $length, 1));
        $redis->expire($cacheKeyListInit, $cacheTTL);
    }

    // 避免并发情况下,同一员工占用多个白名单名额
    if (!$redis->hsetnx($cacheKeyHash, $staffId, 1)) {
        return true;
    }

    $index = $redis->rpush($cacheKeyList, [$staffId]);
    $ret = $redis->lindex($cacheKeyListInit, $index -  1);

    // 手慢了,白名单名额已被抢完
    if (empty($ret)) {
        $redis->rpop($cacheKeyList);
        $redis->hdel($cacheKeyHash, [$staffId]);
        return false;
    }

    $redis->expire($cacheKeyList, $cacheTTL);
    $redis->expire($cacheKeyHash, $cacheTTL);

    return true;
}

使用案例

  1. 启用白名单功能,在 .env 文件配置以下参数:

     WHITE_LIST_IS_ENABLE=1
     WHITE_LIST_NUMBER=10
  2. 追加白名单名额,在 .env 文件调整以下参数:

     WHITE_LIST_NUMBER=20
  3. 减少白名单名额,在 .env 文件调整以下参数:

     WHITE_LIST_NUMBER=10
     WHITE_LIST_CACHE_KEY_PREFIX="second"
  4. 觉得该换另一批人使用 xxx 系统了,则重置白名单,在 .env 文件调整以下参数:

     WHITE_LIST_CACHE_KEY_PREFIX="third"
  5. 关闭白名单功能,在 .env 文件调整以下参数:

     WHITE_LIST_IS_ENABLE=0
本作品采用《CC 协议》,转载必须注明作者和本文链接
编程就像呼吸,学会那天起一日不敢荒废。
ZsmHub
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 1

key = 路由+IP+用户+当前时间戳

keyPrefix = 路由+IP+用户

每次请求,scan (keyPrefix . *)(返回结果,需要在客户端去重)

如果返回结果大于阀值,则报错

如果返回结果小于阀值,则set (key, 1)

本地测试过

20w keys

scan,基本上2s内返回

这是在php-fpm模式下做的

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!