一、实现分布式锁
1.1、加锁脚本
-- lua in redis
local key = KEYS[1] -- 锁标识
local value = ARGV[1] -- 全局唯一值
local ttl = tonumber(ARGV[2]) or 0 -- 过期时间,默认不过期
if (redis.call('exists', key) == 0) then
redis.call('hincrby', key, value, 1)
if (ttl > 0) then
redis.call('expire', key, ttl)
end
return 1
end
if (redis.call('hexists', key, value) == 1) then
redis.call('hincrby', key, value, 1)
return 1
end
return 0
1.2、释放锁脚本
-- lua in redis
local key = KEYS[1] -- 锁标识
local value = ARGV[1] -- 全局唯一值
if (redis.call('hexists', key, value) == 1) then
-- 当计数器为0时才真正删除锁
if (redis.call('hincrby', key, value, -1) < 1) then
redis.call('del', key)
end
return 1
end
return 0
1.3、这种实现缺点
- 业务没完成,锁失效了,可以采用想 Redisson 分布式锁那样的 看门狗线程,进行锁自动续期
- 注意锁的范围定义,定义太大,则会导致吞吐量下降,可以采用分段加锁
- 使用 Lua 脚是为了原子性操作
二、实现分布式限流器
2.1、限流算法
- 令牌桶算法
生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。
- 漏桶算法
漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。
2.2、简单的限流脚本
local key = "rate.limit:" .. KEYS[1] --限流KEY
local limit = tonumber(ARGV[1]) --限流大小
local ttl = tonumber(ARGV[2]) or 2 -- 过期时间,默认2s
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then --如果超出限流大小
return 0
else --请求数+1,并设置2秒过期
redis.call("INCRBY", key,"1")
if (ttl > 0) then
redis.call('expire', key, ttl)
end
return current + 1
end
2.3、解决时间粒度精细问题
- 实现 不可预消费的令牌桶
- 通过计算令牌恢复速率,达到控制时间粒度精细问题 ```lua — 令牌桶限流: 不支持预消费, 初始桶是满的 — KEYS[1] string 限流的key
— ARGV[1] int 桶最大容量 — ARGV[2] int 每次添加令牌数 — ARGV[3] int 令牌添加间隔(秒) — ARGV[4] int 当前时间戳
local bucket_capacity = tonumber(ARGV[1]) local add_token = tonumber(ARGV[2]) local add_interval = tonumber(ARGV[3]) local now = tonumber(ARGV[4])
— 保存上一次更新桶的时间的key
local LAST_TIME_KEY = KEYS[1]..”_time”;
— 获取当前桶中令牌数
local token_cnt = redis.call(“get”, KEYS[1])
— 桶完全恢复需要的最大时长
local reset_time = math.ceil(bucket_capacity / add_token) * add_interval;
if token_cnt then — 令牌桶存在 — 上一次更新桶的时间 local last_time = redis.call(‘get’, LAST_TIME_KEY) — 恢复倍数 local multiple = math.floor((now - last_time) / add_interval) — 恢复令牌数 local recovery_cnt = multiple * add_token — 确保不超过桶容量 local token_cnt = math.min(bucket_capacity, token_cnt + recovery_cnt) - 1
if token_cnt < 0 then
return -1;
end
-- 重新设置过期时间, 避免key过期
redis.call('set', KEYS[1], token_cnt, 'EX', reset_time)
redis.call('set', LAST_TIME_KEY, last_time + multiple * add_interval, 'EX', reset_time)
return token_cnt
else — 令牌桶不存在
token_cnt = bucket_capacity - 1
— 设置过期时间避免key一直存在
redis.call(‘set’, KEYS[1], token_cnt, ‘EX’, reset_time);
redis.call(‘set’, LAST_TIME_KEY, now, ‘EX’, reset_time + 1);
return token_cnt
end
```