redis+lua
Lua 的定位
轻量级脚本语言
解释执行
极其简单
嵌入式友好(Redis / Nginx / 游戏引擎)
最常见的 3 个地方
| 场景 | 用途 |
|---|---|
| Redis | 原子操作、事务逻辑 |
| Nginx (OpenResty) | 鉴权、限流、灰度 |
| 游戏服务器 | 配置、热更新 |
其操作保证原子性
基础语法
变量&类型
1 | local a = 10 --number |
Lua只有4种基本类型,
string -> int
用tonumber
Lua没有struct,map之类,全靠table实现:
当数组用
1
2local arr = {1, 2, 3}
print(arr[1]) -- 1(下标从1开始)当map用
1
2local m = {}
m["xx"] = "yy"混用
1
2
3
4
5local t = {
a = 1,
b = 2,
[3] = "c"
}
声明时候不加local默认全部是全局变量,官方建议所有变量加local
条件if else
1 | if count > limit then |
循环
1 | for i = 1, 10 do |
函数
1 | function A(a, b) |
Go + redis + Lua 实现限流
Go → Redis → Lua(单线程)→ 返回
调用方法:
1. 滑动窗口限流
新建一个脚本,实现滑动窗口限流1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19var rateLimitLua = redis.NewScript(`
local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
local count = redis.call('ZCARD', key)
if count >= limit then
return 0
else
-- member 必须唯一!
redis.call('ZADD', key, now, now .. '-' .. math.random())
redis.call('PEXPIRE', key, window)
return 1
end
`)
1 | // 限流判断 |
在Go对应接口中调用这个方法,对单一接口实现限流,实现了一个简单的限流接口,使用滑动窗口对单一ip进行次数检验,如果某段时间内调用接口次数过多,会被限流
2. 令牌桶
思想: 系统以特定的速率往桶里放置令牌,请求来了拿到令牌就放行,拿不到就拒绝
需要的数学模型1
2
3
4capacity 桶容量(最大令牌数)
rate 令牌生成速率(token / second)
tokens 当前令牌数
last_time 上次补充令牌的时间
计算思路:
- 计算距离上次的时间差 Δt
- 新增令牌 = Δt × rate
- tokens = min(capacity, tokens + 新增)
- 如果 tokens ≥ 1 → tokens -= 1,放行
- 否则 → 拒绝
1 | -- KEYS[1] 限流 key |
手写踩坑:HMGET为空的时候返回的是false,不是nil,nil会炸
Go封装:
tokenBuctet.Go
1 | package auth |
使用时候在接口运行入口处加上Allow验证,1放行,0拦截
cpp脚本模拟接口调用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
int curlRequest() {
FILE* fp = popen(
"curl -s -o /dev/null -w \"%{http_code}\" http://localhost:8080/getDetail",
"r"
);
if (!fp) return -1;
char buf[16] = {0};
fgets(buf, sizeof(buf), fp);
pclose(fp);
return std::atoi(buf);
}
int main() {
const int N = 20;
std::vector<int> httpCodes(N);
std::vector<std::thread> threads;
for (int i = 0; i < N; i++) {
threads.emplace_back([i, &httpCodes]() {
httpCodes[i] = curlRequest();
});
}
for (auto& t : threads) {
t.join();
}
// 按顺序输出
std::cout << "==== HTTP Codes (ordered) ====" << std::endl;
for (int i = 0; i < N; i++) {
std::cout << "req " << i << " -> " << httpCodes[i] << std::endl;
}
return 0;
}
可看到后面的输出429,被限流了
注意
Redis Lua 硬限制:
| 限制 | 原因 |
|---|---|
| 不能 sleep | 会阻塞 Redis |
| 不能访问网络 | 保证确定性 |
| 不能有无限循环 | 防止卡死 |
| 不能调用非确定性函数 | 一致性问题 |
redisLua会把redis卡死,所以要有lua-time-limit
Go + redis + Lua 实现分布式锁
为什么要有这个东西?
单机实例容器加锁时候不必考虑多实例问题,只需要直接加锁,比如平时咱自己写的项目,相对某个东西限制并发,就直接对他枷锁,但是当有多个集群都需要拿到或者修改同一个实例的东西时候,就不能直接在某个单实例集群里面加锁了,需要有一个能管理所有集群的或者是不在这些集群运行里面的单独中间件来操作,这里用redis实现分布式锁最合理,因为redis满足上面所有条件,是独立于运行集群的单独实例
使用redis + lua实现分布式锁,了解了redis + lua使用方法后,这件事情就变成了在redis上存一个变量,用这个变量来标识锁是否存在,实现资源的控制
一般来说,可以直接用redis setnx来设置一个变量实现锁的功能,但是这样只能上锁,无法控制锁的释放和安全性,因为不是原子操作,例如:1
2
3setnx lock 1
expire lock 10
setnx后进程刚刚好挂了
expire没操作
lock没有释放
出现死锁
lua的原子性恰好补足了这一缺点
加锁1
redis.call("SET", KEYS[1], ARGV[1], "NX", "EX", ARGV[2])
解锁:1
2
3
4
5if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
- 锁的 value 是 唯一标识(UUID)
- 只有 owner 才能删除
GET + DEL原子执行
实际使用场景
场景 1:定时任务防止重复执行
真实背景
实例 A:执行任务 实例 B:执行任务 实例 C:执行任务
同一任务被执行 5 次
- 重复发消息
- 重复跑脚本
- 重复生成数据
分布式锁在保护什么?
“这个时间点,只允许一个实例执行”
场景 2:订单 / 支付回调
真实背景
“一次业务逻辑,只能成功执行一次”
场景 3:缓存重建(经典缓存击穿)
真实背景
- 热 key 过期
- 瞬间 1000 个请求进来
没锁的后果
1000 个请求 → 1000 次 DB 查询
加锁
1 个请求重建缓存 999 个等待 / 返回旧值
锁在保护什么?
“同一时刻,只允许一个人查 DB 回源”
