功能说明

应用场景

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

特色功能

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

使用案例

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

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

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

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

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

    1
    
    WHITE_LIST_IS_ENABLE=0
    

源代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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 系统暂不可用,请稍晚一些再登陆使用,谢谢配合!',
];
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 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;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
 * 判断该员工是否在白名单内:只允许部分员工可以正常使用 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;
}