Redis令牌桶算法是一种基于Redis实现的限流算法,用于控制接口的访问频率,防止接口滥用,保障服务器资源的高可用性。
一、令牌桶算法原理二、基于Redis的令牌桶算法实现1. 数据结构设计- Redis键:使用一个键来存储令牌桶的状态,包括剩余令牌数和上次更新时间。
- 数据格式:使用JSON格式存储剩余令牌数(num)和上次更新时间(time)。
2. 算法流程初始化:
第一次访问时,初始化令牌桶,设置剩余令牌数为初始值(initNum),并记录当前时间戳。
获取令牌:
获取当前剩余令牌数和上次更新时间。
计算自上次更新以来应该添加的令牌数:新增令牌数 = (当前时间 - 上次更新时间) * 速率其中速率为 initNum / 桶的过期时间(秒)。
更新剩余令牌数:剩余令牌数 = min(初始令牌数, 剩余令牌数 - 1 + 新增令牌数)。
如果剩余令牌数大于0,允许访问;否则,拒绝访问。
并发控制:
使用Redis的WATCH命令监视令牌桶的键,确保在事务执行期间键未被其他客户端修改。
使用MULTI和EXEC命令保证更新操作的原子性。
3. PHP代码示例<?phpnamespace ApiLib;class RateLimit { private $minNum = 60; // 单个用户每分钟访问数 private $dayNum = 10000; // 单个用户每天总的访问量 public function minLimit($uid) { $minNumKey = $uid . '_minNum'; $dayNumKey = $uid . '_dayNum'; $resMin = $this->getRedis($minNumKey, $this->minNum, 60); $resDay = $this->getRedis($dayNumKey, $this->dayNum, 86400); if (!$resMin['status'] || !$resDay['status']) { exit($resMin['msg'] . $resDay['msg']); } } public function getRedis($key, $initNum, $expire) { $nowtime = time(); $result = ['status' => true, 'msg' => '']; $redisObj = $this->di->get('redis'); $redis = $redisObj; $redis->watch($key); $limitVal = $redis->get($key); if ($limitVal) { $limitVal = json_decode($limitVal, true); $newNum = min($initNum, ($limitVal['num'] - 1) + (($initNum / $expire) * ($nowtime - $limitVal['time']))); if ($newNum > 0) { $redisVal = json_encode(['num' => $newNum, 'time' => time()]); } else { return ['status' => false, 'msg' => '当前时刻令牌消耗完!']; } } else { $redisVal = json_encode(['num' => $initNum, 'time' => time()]); } $redis->multi(); $redis->set($key, $redisVal); $rob_result = $redis->exec(); if (!$rob_result) { $result = ['status' => false, 'msg' => '访问频次过多!']; } return $result; }}三、Redis乐观锁在令牌桶算法中的应用乐观锁机制:
Redis通过WATCH命令提供乐观锁功能,监视一个或多个键。
如果被监视的键在事务执行前被其他客户端修改,事务将失败,EXEC命令返回nil。
在令牌桶算法中的作用:
确保在并发环境下,令牌的获取和更新操作是原子的。
防止多个客户端同时获取和更新令牌数,导致数据不一致。
四、接入Lua脚本实现原子性操作-- KEYS[1]: 令牌桶的Redis键-- ARGV[1]: 初始令牌数-- ARGV[2]: 桶的过期时间(秒)local key = KEYS[1]local initNum = tonumber(ARGV[1])local expire = tonumber(ARGV[2])local now = redis.call('TIME')[1] -- 获取当前时间戳(秒)local limitVal = redis.call('GET', key)if limitVal then limitVal = cjson.decode(limitVal) local rate = initNum / expire local newNum = math.min(initNum, (limitVal.num - 1) + (rate * (now - limitVal.time))) if newNum > 0 then limitVal = {num = newNum, time = now} redis.call('SET', key, cjson.encode(limitVal)) return 1 -- 允许访问 else return 0 -- 令牌不足,拒绝访问 endelse local limitVal = {num = initNum, time = now} redis.call('SET', key, cjson.encode(limitVal)) return 1 -- 允许访问end五、总结- 令牌桶算法是一种有效的限流算法,适用于控制接口的访问频率。
- Redis提供了高性能的键值存储和乐观锁机制,适合实现令牌桶算法。
- Lua脚本可以进一步提升性能,确保操作的原子性。
- 通过合理设置速率和容量,可以平衡系统的可用性和流量突发处理能力。