redis eval表达式二
Redis 中的 EVAL 命令用于执行 Lua 脚本。它最大的作用是:将多个 Redis 操作封装成一个原子操作执行,避免并发问题。
1. 基本语法
EVAL script numkeys key [key ...] arg [arg ...]
参数说明:
| 参数 | 含义 |
|---|---|
| script | Lua 脚本 |
| numkeys | KEYS 数组的个数 |
| key | 传给 Lua 的 Redis Key |
| arg | 传给 Lua 的普通参数 |
Lua 中通过:
KEYS[1]
KEYS[2]
ARGV[1]
ARGV[2]
获取这些参数。
2. 最简单例子
执行:
EVAL "return 'hello redis'" 0
结果:
"hello redis"
表示执行了一段 Lua:
return 'hello redis'
3. 获取 Key 的值
Redis:
SET name Jack
执行:
EVAL "return redis.call('GET', KEYS[1])" 1 name
执行过程:
redis.call('GET', KEYS[1])
KEYS[1] = "name"
结果:
"Jack"
4. 修改库存(经典面试题)
假设库存:
SET stock 10
需求:
如果库存大于 0,则扣减库存,否则返回失败。
Lua:
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock > 0 then
redis.call('DECR', KEYS[1])
return 1
else
return 0
end
执行:
EVAL "
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock > 0 then
redis.call('DECR', KEYS[1])
return 1
else
return 0
end
" 1 stock
结果:
1
库存:
GET stock
9
为什么不用:
GET stock
if(stock>0){
DECR stock
}
因为高并发时:
线程A GET=1
线程B GET=1
线程A DECR → 0
线程B DECR → -1
超卖了。
Lua:
GET
判断
DECR
在 Redis 内部一次执行完成。
Redis 是单线程执行 Lua 的,因此是原子的。
5. redis.call 和 redis.pcall
Lua 中调用 Redis 有两种方式:
redis.call()
出现错误直接终止:
redis.call('INCR', 'abc')
若:
abc = "hello"
则报错:
ERR value is not an integer
整个脚本停止。
redis.pcall()
出现错误继续执行:
local r = redis.pcall('INCR', 'abc')
return r
返回:
{
err="ERR value is not an integer"
}
区别:
| 方法 | 出错行为 |
|---|---|
| redis.call | 抛异常,脚本终止 |
| redis.pcall | 返回错误对象 |
6. 分布式锁中的应用
加锁:
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[2])
return 1
else
return 0
end
执行:
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_key uuid 30000
释放锁(防止误删):
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
对应:
EVAL "
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
" 1 lock_key uuid
这是很多分布式锁实现(例如 Redisson 早期版本)中的核心思想。
7. Java 中如何使用 EVAL
以 Spring Data Redis 为例:
String script =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";
RedisScript<Long> redisScript =
new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(
redisScript,
Collections.singletonList("lock"),
requestId
);
8. EVAL 与 EVALSHA
每次执行:
EVAL "..." ...
Redis 都需要解析 Lua。
为了提高效率,可以:
第一步:加载脚本
SCRIPT LOAD "
return redis.call('GET', KEYS[1])
"
返回:
6b1bf486c81ceb7edf3c093f4c48582e38c0e791
第二步:执行
EVALSHA 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 1 name
区别:
| 命令 | 特点 |
|---|---|
| EVAL | 每次传完整 Lua |
| EVALSHA | 只传 SHA1,更高效 |
| SCRIPT LOAD | 提前缓存 Lua |
实际上,像 Redisson、Spring Data Redis 等框架内部都会优先使用 EVALSHA。
9. EVAL 的特点总结
优点:
- 原子性执行;
- 减少网络往返;
- 可以实现复杂业务逻辑;
- 非常适合库存扣减、分布式锁、限流等场景。
缺点:
- Lua 执行期间会阻塞 Redis;
- 不适合耗时计算;
- Redis Cluster 中,Lua 脚本访问的所有 Key 必须位于同一个 Slot。
例如:
redis.call('GET', KEYS[1])
redis.call('GET', KEYS[2])
如果:
KEYS[1] → slot 1000
KEYS[2] → slot 8000
Redis Cluster 会报错:
CROSSSLOT Keys in request don't hash to the same slot
解决办法:
使用 Hash Tag:
user:{100}:balance
user:{100}:coupon
这样:
{100}
参与 Slot 计算,两者会落在同一个 Slot。
一句话概括:
Redis 的 EVAL 就是把多个 Redis 命令封装成 Lua 脚本,在 Redis 服务端以原子方式执行,是解决高并发场景下“多步操作一致性问题”的利器。
浙公网安备 33010602011771号