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 的特点总结

优点:

  1. 原子性执行;
  2. 减少网络往返;
  3. 可以实现复杂业务逻辑;
  4. 非常适合库存扣减、分布式锁、限流等场景。

缺点:

  1. Lua 执行期间会阻塞 Redis;
  2. 不适合耗时计算;
  3. 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 服务端以原子方式执行,是解决高并发场景下“多步操作一致性问题”的利器。

 

posted on 2026-07-05 16:06  日思日睿  阅读(6)  评论(0)    收藏  举报