一、实现分布式锁

1.1、加锁脚本

  1. -- lua in redis
  2. local key = KEYS[1] -- 锁标识
  3. local value = ARGV[1] -- 全局唯一值
  4. local ttl = tonumber(ARGV[2]) or 0 -- 过期时间,默认不过期
  5. if (redis.call('exists', key) == 0) then
  6. redis.call('hincrby', key, value, 1)
  7. if (ttl > 0) then
  8. redis.call('expire', key, ttl)
  9. end
  10. return 1
  11. end
  12. if (redis.call('hexists', key, value) == 1) then
  13. redis.call('hincrby', key, value, 1)
  14. return 1
  15. end
  16. return 0

1.2、释放锁脚本

  1. -- lua in redis
  2. local key = KEYS[1] -- 锁标识
  3. local value = ARGV[1] -- 全局唯一值
  4. if (redis.call('hexists', key, value) == 1) then
  5. -- 当计数器为0时才真正删除锁
  6. if (redis.call('hincrby', key, value, -1) < 1) then
  7. redis.call('del', key)
  8. end
  9. return 1
  10. end
  11. return 0

1.3、这种实现缺点

  • 业务没完成,锁失效了,可以采用想 Redisson 分布式锁那样的 看门狗线程,进行锁自动续期
  • 注意锁的范围定义,定义太大,则会导致吞吐量下降,可以采用分段加锁
  • 使用 Lua 脚是为了原子性操作

    二、实现分布式限流器

2.1、限流算法

  • 令牌桶算法

生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法可以在短时间内请求拿到大量令牌,而且拿令牌的过程并不是消耗很大的事情。

  • 漏桶算法

漏桶的出水速度是恒定的,那么意味着如果瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。

2.2、简单的限流脚本

  1. local key = "rate.limit:" .. KEYS[1] --限流KEY
  2. local limit = tonumber(ARGV[1]) --限流大小
  3. local ttl = tonumber(ARGV[2]) or 2 -- 过期时间,默认2s
  4. local current = tonumber(redis.call('get', key) or "0")
  5. if current + 1 > limit then --如果超出限流大小
  6. return 0
  7. else --请求数+1,并设置2秒过期
  8. redis.call("INCRBY", key,"1")
  9. if (ttl > 0) then
  10. redis.call('expire', key, ttl)
  11. end
  12. return current + 1
  13. 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

  1. if token_cnt < 0 then
  2. return -1;
  3. end
  4. -- 重新设置过期时间, 避免key过期
  5. redis.call('set', KEYS[1], token_cnt, 'EX', reset_time)
  6. redis.call('set', LAST_TIME_KEY, last_time + multiple * add_interval, 'EX', reset_time)
  7. 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 ```