在 Redis 中使用 Lua 脚本可以**实现原子性操作**,避免多命令竞态条件,同时减少网络开销(只需一次通信)。以下是详细使用方法及示例:
---
## **一、Lua 脚本基础语法**
### **1. 脚本执行命令**
```bash
# 直接执行脚本
EVAL "return 'Hello, Redis!'" 0
# 使用参数
EVAL "return ARGV[1]" 0 "Hello"
```
- **`EVAL`**:执行脚本的核心命令。
- **参数说明**:
- 第一个参数:Lua 脚本内容。
- 第二个参数:`KEYS` 的数量(即使不使用也必须声明)。
- 后续参数:`ARGV` 数组传递的额外参数。
---
### **2. 键(KEYS)与参数(ARGV)**
- **`KEYS`**:用于标识 Redis 中的键(保证原子性操作的资源)。
- **`ARGV`**:其他非键参数。
```bash
EVAL "return {KEYS[1], ARGV[1]}" 1 "user:1001" "name"
```
**输出**:
```json
["user:1001", "name"]
```
---
## **二、实际应用场景与示例**
### **1. 原子性计数器递增**
**需求**:实现一个带过期时间的计数器,避免竞态条件。
```lua
-- KEYS[1]: 计数器键
-- ARGV[1]: 过期时间(秒)
-- ARGV[2]: 递增步长
local current = redis.call('GET', KEYS[1])
if not current then
redis.call('SET', KEYS[1], 0, 'EX', ARGV[1])
end
return redis.call('INCRBY', KEYS[1], ARGV[2])
```
**执行命令**:
```bash
EVAL "local current=redis.call('GET',KEYS[1]);if not current then redis.call('SET',KEYS[1],0,'EX',ARGV[1]) end;return redis.call('INCRBY',KEYS[1],ARGV[2])" 1 "counter:page_view" 60 1
```
---
### **2. 分布式锁实现**
**需求**:获取锁时设置过期时间,避免死锁。
```lua
-- KEYS[1]: 锁键
-- ARGV[1]: 锁值(唯一标识)
-- ARGV[2]: 过期时间(毫秒)
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1 -- 获取锁成功
else
return 0 -- 获取锁失败
end
```
**执行命令**:
```bash
EVAL "if redis.call('SETNX',KEYS[1],ARGV[1])==1 then redis.call('PEXPIRE',KEYS[1],ARGV[2]);return 1 else return 0 end" 1 "lock:order" "uuid-1234" 5000
```
---
### **3. 限流器(滑动窗口)**
**需求**:限制用户每分钟最多 10 次操作。
```lua
-- KEYS[1]: 限流键(如 "rate_limit:user123")
-- ARGV[1]: 当前时间戳(毫秒)
-- ARGV[2]: 窗口大小(毫秒,如 60000)
-- ARGV[3]: 最大请求数(如 10)
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
-- 允许请求,记录时间戳
redis.call('ZADD', key, now, now)
redis.call('PEXPIRE', key, window)
return 1 -- 允许
else
return 0 -- 拒绝
end
```
**执行命令**:
```bash
EVAL "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 redis.call('ZADD',key,now,now);redis.call('PEXPIRE',key,window);return 1 else return 0 end" 1 "rate_limit:user123" 1630000000000 60000 10
```
---
## **三、性能优化技巧**
### **1. 使用 `SCRIPT LOAD` + `EVALSHA`**
- **减少网络传输**:先加载脚本生成 SHA1 摘要,后续通过摘要调用。
```bash
# 加载脚本
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
# 返回: "abc123sha1hash..."
# 通过 SHA1 执行
EVALSHA "abc123sha1hash..." 1 "user:1001"
```
### **2. 避免长脚本**
- Redis 是单线程模型,长脚本会阻塞其他命令。
- 复杂逻辑拆分为多个短脚本。
### **3. 错误处理**
- Lua 脚本中调用 Redis 命令出错时会抛出异常:
```lua
local ok, err = pcall(redis.call, 'GET', 'nonexistent_key')
if not ok then
return "Error: " .. err
end
```
---
## **四、Java 集成示例(Spring Data Redis)**
```java
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
// 定义 Lua 脚本
String luaScript = "return redis.call('GET', KEYS[1])";
// 创建脚本对象
DefaultRedisScript<String> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(String.class);
// 执行脚本
String result = redisTemplate.execute(
script,
Collections.singletonList("user:1001"), // KEYS
"optional_arg" // ARGV
);
```
---
## **五、注意事项**
1. **原子性**:Lua 脚本在 Redis 中单线程执行,天然原子性。
2. **调试**:脚本中可用 `redis.log(redis.LOG_NOTICE, "Debug message")` 输出日志。
3. **资源限制**:
- 脚本默认最长执行时间:5秒(可通过 `lua-time-limit` 配置)。
- 超出限制后,Redis 会记录警告但**不会终止脚本**(需用 `SCRIPT KILL` 手动干预)。
---