1 前言
1.1 为什么要用 Lua 操作 Redis?
Redis本身支持强大的命令操作,但是在高并发场景下肯呢个存在如下问题:
- 原子性:多个命令组合操作可能被并发打断,Lua脚本在Redis内部执行,保证单线程原子性。
- 减少网络往返:多个命令一次性执行,减少客户端与Redis的多次通信。
- 复杂逻辑处理:Lua能写 if-else、循环,封装复杂的业务逻辑。
所以秒杀、库存扣减、分布式锁,都会大量使用到 Lua。
2 Redis执行Lua脚本的方式
2.1 EVAL
执行执行脚本
语法:
EVAL script numkeys key [key ...] arg [arg ...]
script
:Lua脚本内容
numkeys
:传入的 key 数量
key [key ...]
:传入的key
arg [arg ...]
:额外参数(Lua中通过 ARGV
获取)
2.2 EVALSHA
- 为了避免脚本过长重复传输,Redis会缓存脚本。
- 使用
SCRIPT LOAD
加载脚本,返回 sha1
。
- 用
EVAALSHA sha1 numkeys ...
来执行。
3 Lua 脚本在 Redis 中的变量
在Redis的Lua脚本中,所有参数和key都要通过一下两个内置变量传入:
- KEYS:数组,存放传入的key。
- ARGV:数组,存放传入的参数。
案例
local key = KEYS[1]
local value = ARGV[1]
return redis.call("set", key, value)
执行
EVAL "local key=KEYS[1]; local value=ARGV[1]; return redis.call('SET', key, value)" 1 mykey hello
4 Lua 操作 Redis 的常用方法
4.1 redis.call
redis.call("set", "foo", "bar")
如果执行失败会抛出异常。
4.2 redis.pcall
redis.pcall("GET", "foo")
失败不会报错,会返回错误对象。
5 跨语言调用
5.1 python
案例一
redis==6.0.0
import redis
r = redis.Redis(host='127.0.0.1', port=6379, db=0, decode_responses=True)
print(r)
lua_script = """
return redis.call("set", KEYS[1], ARGV[1])
"""
script = r.register_script(lua_script)
res = script(keys=['nickname'], args=['有勇气的牛排'])
print(res)
案例二:异步案例
redis==6.4.0
import asyncio
import redis.asyncio as aioredis
async def main():
r = aioredis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
lua_script = """
return redis.call("set", KEYS[1], ARGV[1])
"""
script = r.register_script(lua_script)
res = await script(keys=['nickname'], args=['有勇气的牛排'])
print(res)
await r.aclose()
asyncio.run(main())

6 实战案例
6.1 原子性库存扣减(秒杀场景)
输出初始化
product:1 9
product:2 0
LUA
local key = KEYS[1]
local num = tonumber(redis.call("GET", key))
if num > 0 then
redis.call("DECR", "product:1")
return 1
else
return 0
end
Python
import asyncio
import redis.asyncio as aioredis
async def main():
r = aioredis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
lua_script = """
local key = KEYS[1]
local num = tonumber(redis.call("GET", key))
if num > 0 then
redis.call("DECR", "product:1")
return 1
else
return 0
end
"""
script = r.register_script(lua_script)
res = await script(keys=["product:1"], args=[])
print(res)
res = await script(keys=["product:2"], args=[])
print(res)
await r.aclose()
asyncio.run(main())

6.2 分布式锁
加锁
if redis.call("SETNX", KEYS[1], ARGV[1]) == 1 then
redis.call("PEXPIRE", KEYS[1], ARGV[2])
return 1
else
return 0
end
解锁
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
6.3 计数器(限流)
local count = redis.call("INCR", KEYS[1])
if count == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
if count > tonumber(ARGV[2]) then
return 0
else
return 1
end
EVAL "local count=redis.call('INCR',KEYS[1]);if count==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]);end;if count>tonumber(ARGV[2]) then return 0 else return 1 end" 1 user:123 60 5
7 最佳实践
短小脚本优先:脚本越短越好,复杂逻辑放到应用层。
避免长阻塞:Lua 在 Redis 内部执行,脚本执行期间 Redis 不会处理其他请求,避免死循环。
保证幂等性:脚本最好设计为幂等操作,避免重复执行造成数据错误。
- 可以给每个操作分配唯一id,判断是否处理过。
- 常见于:网络重试、MQ多次发送同一条消息、应用崩溃重试。
脚本调试:可以用 redis-cli --ldb --eval
调试 Lua。
<h2><a id="1__0"></a>1 前言</h2>
<h3><a id="11__Lua__Redis_2"></a>1.1 为什么要用 Lua 操作 Redis?</h3>
<p>Redis本身支持强大的命令操作,但是在高并发场景下肯呢个存在如下问题:</p>
<ol>
<li><strong>原子性</strong>:多个命令组合操作可能被并发打断,Lua脚本在Redis内部执行,保证<strong>单线程原子性</strong>。</li>
<li><strong>减少网络往返</strong>:多个命令一次性执行,减少客户端与Redis的多次通信。</li>
<li><strong>复杂逻辑处理</strong>:Lua能写 if-else、循环,封装复杂的业务逻辑。</li>
</ol>
<p>所以秒杀、库存扣减、分布式锁,都会大量使用到 Lua。</p>
<h2><a id="2_RedisLua_12"></a>2 Redis执行Lua脚本的方式</h2>
<h3><a id="21_EVAL_14"></a>2.1 EVAL</h3>
<p>执行执行脚本</p>
<p>语法:</p>
<pre><div class="hljs"><code class="lang-shell">EVAL script numkeys key [key ...] arg [arg ...]
</code></div></pre>
<p><code>script</code>:Lua脚本内容</p>
<p><code>numkeys</code>:传入的 key 数量</p>
<p><code>key [key ...]</code>:传入的key</p>
<p><code>arg [arg ...]</code>:额外参数(Lua中通过 <code>ARGV</code> 获取)</p>
<h3><a id="22_EVALSHA_32"></a>2.2 EVALSHA</h3>
<ul>
<li>为了避免脚本过长重复传输,Redis会缓存脚本。</li>
<li>使用 <code>SCRIPT LOAD</code> 加载脚本,返回 <code>sha1</code>。</li>
<li>用 <code>EVAALSHA sha1 numkeys ...</code> 来执行。</li>
</ul>
<h2><a id="3_Lua__Redis__40"></a>3 Lua 脚本在 Redis 中的变量</h2>
<p>在Redis的Lua脚本中,所有参数和key都要通过一下两个内置变量传入:</p>
<ul>
<li>KEYS:数组,存放传入的key。</li>
<li>ARGV:数组,存放传入的参数。</li>
</ul>
<p>案例</p>
<pre><div class="hljs"><code class="lang-lua"><span class="hljs-comment">-- Lua 脚本</span>
<span class="hljs-keyword">local</span> key = KEYS[<span class="hljs-number">1</span>]
<span class="hljs-keyword">local</span> value = ARGV[<span class="hljs-number">1</span>]
<span class="hljs-keyword">return</span> redis.call(<span class="hljs-string">"set"</span>, key, value)
</code></div></pre>
<p>执行</p>
<pre><div class="hljs"><code class="lang-shell">EVAL "local key=KEYS[1]; local value=ARGV[1]; return redis.call('SET', key, value)" 1 mykey hello
</code></div></pre>
<h2><a id="4_Lua__Redis__62"></a>4 Lua 操作 Redis 的常用方法</h2>
<h3><a id="41_rediscall_64"></a>4.1 redis.call</h3>
<pre><div class="hljs"><code class="lang-shell">redis.call("set", "foo", "bar")
</code></div></pre>
<p>如果执行失败会抛出异常。</p>
<h3><a id="42_redispcall_72"></a>4.2 redis.pcall</h3>
<pre><div class="hljs"><code class="lang-shell">redis.pcall("GET", "foo")
</code></div></pre>
<p>失败不会报错,会返回错误对象。</p>
<h2><a id="5__80"></a>5 跨语言调用</h2>
<h3><a id="51_python_82"></a>5.1 python</h3>
<p>案例一</p>
<pre><div class="hljs"><code class="lang-shell">redis==6.0.0
</code></div></pre>
<pre><div class="hljs"><code class="lang-python"><span class="hljs-comment"># -*- coding: utf-8 -*-</span>
<span class="hljs-keyword">import</span> redis
r = redis.Redis(host=<span class="hljs-string">'127.0.0.1'</span>, port=<span class="hljs-number">6379</span>, db=<span class="hljs-number">0</span>, decode_responses=<span class="hljs-literal">True</span>)
<span class="hljs-built_in">print</span>(r)
<span class="hljs-comment"># 正确的 Lua 脚本:设置 key-value</span>
lua_script = <span class="hljs-string">"""
return redis.call("set", KEYS[1], ARGV[1])
"""</span>
<span class="hljs-comment"># 注册脚本</span>
script = r.register_script(lua_script)
<span class="hljs-comment"># 执行脚本</span>
res = script(keys=[<span class="hljs-string">'nickname'</span>], args=[<span class="hljs-string">'有勇气的牛排'</span>]) <span class="hljs-comment"># nickname=有勇气的牛排</span>
<span class="hljs-built_in">print</span>(res) <span class="hljs-comment"># 输出 OK</span>
</code></div></pre>
<p>案例二:异步案例</p>
<pre><div class="hljs"><code class="lang-shell">redis==6.4.0
</code></div></pre>
<pre><div class="hljs"><code class="lang-python"><span class="hljs-keyword">import</span> asyncio
<span class="hljs-keyword">import</span> redis.asyncio <span class="hljs-keyword">as</span> aioredis
<span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">main</span>():
r = aioredis.Redis(host=<span class="hljs-string">"127.0.0.1"</span>, port=<span class="hljs-number">6379</span>, db=<span class="hljs-number">0</span>, decode_responses=<span class="hljs-literal">True</span>)
<span class="hljs-comment"># 正确的 Lua 脚本:设置 key-value</span>
lua_script = <span class="hljs-string">"""
return redis.call("set", KEYS[1], ARGV[1])
"""</span>
<span class="hljs-comment"># 注册脚本</span>
script = r.register_script(lua_script)
<span class="hljs-comment"># 执行脚本</span>
res = <span class="hljs-keyword">await</span> script(keys=[<span class="hljs-string">'nickname'</span>], args=[<span class="hljs-string">'有勇气的牛排'</span>]) <span class="hljs-comment"># nickname=有勇气的牛排</span>
<span class="hljs-built_in">print</span>(res) <span class="hljs-comment"># 输出 OK</span>
<span class="hljs-comment"># 关闭</span>
<span class="hljs-keyword">await</span> r.aclose()
asyncio.run(main())
</code></div></pre>
<p><img src="https://static.couragesteak.com/article/a530ca7eafdba0a745b4a96a07f4c997.png" alt="Python操作Lua脚本" /></p>
<h2><a id="6__145"></a>6 实战案例</h2>
<h3><a id="61__147"></a>6.1 原子性库存扣减(秒杀场景)</h3>
<p>输出初始化</p>
<pre><div class="hljs"><code class="lang-shell">product:1 9
product:2 0
</code></div></pre>
<p>LUA</p>
<pre><div class="hljs"><code class="lang-lua"><span class="hljs-comment">-- 判断库存是否充足,如果大于0则减1,否则返回0</span>
<span class="hljs-keyword">local</span> key = KEYS[<span class="hljs-number">1</span>]
<span class="hljs-keyword">local</span> num = <span class="hljs-built_in">tonumber</span>(redis.call(<span class="hljs-string">"GET"</span>, key))
<span class="hljs-keyword">if</span> num > <span class="hljs-number">0</span> <span class="hljs-keyword">then</span>
redis.call(<span class="hljs-string">"DECR"</span>, <span class="hljs-string">"product:1"</span>)
<span class="hljs-keyword">return</span> <span class="hljs-number">1</span>
<span class="hljs-keyword">else</span>
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>
<span class="hljs-keyword">end</span>
</code></div></pre>
<p>Python</p>
<pre><div class="hljs"><code class="lang-python"><span class="hljs-keyword">import</span> asyncio
<span class="hljs-keyword">import</span> redis.asyncio <span class="hljs-keyword">as</span> aioredis
<span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">main</span>():
r = aioredis.Redis(host=<span class="hljs-string">"127.0.0.1"</span>, port=<span class="hljs-number">6379</span>, db=<span class="hljs-number">0</span>, decode_responses=<span class="hljs-literal">True</span>)
<span class="hljs-comment"># 正确的 Lua 脚本:设置 key-value</span>
lua_script = <span class="hljs-string">"""
local key = KEYS[1]
local num = tonumber(redis.call("GET", key))
if num > 0 then
redis.call("DECR", "product:1")
return 1
else
return 0
end
"""</span>
<span class="hljs-comment"># 注册脚本</span>
script = r.register_script(lua_script)
<span class="hljs-comment"># 执行脚本</span>
res = <span class="hljs-keyword">await</span> script(keys=[<span class="hljs-string">"product:1"</span>], args=[]) <span class="hljs-comment"># nickname=有勇气的牛排</span>
<span class="hljs-built_in">print</span>(res) <span class="hljs-comment"># 输出 1</span>
res = <span class="hljs-keyword">await</span> script(keys=[<span class="hljs-string">"product:2"</span>], args=[]) <span class="hljs-comment"># nickname=有勇气的牛排</span>
<span class="hljs-built_in">print</span>(res) <span class="hljs-comment"># 输出 0</span>
<span class="hljs-comment"># 关闭</span>
<span class="hljs-keyword">await</span> r.aclose()
asyncio.run(main())
</code></div></pre>
<p><img src="https://static.couragesteak.com/article/8d1855d52196fa3f5ee7f1785c027e98.png" alt="原子性库存扣减(秒杀场景)" /></p>
<h3><a id="62__212"></a>6.2 分布式锁</h3>
<p>加锁</p>
<pre><div class="hljs"><code class="lang-lua"><span class="hljs-comment">-- 尝试加锁</span>
<span class="hljs-comment">-- KEYS[1] 锁的key</span>
<span class="hljs-comment">-- ARGV[1] 锁的唯一标识(请求ID)</span>
<span class="hljs-comment">-- ARGV[2] 过期时间</span>
<span class="hljs-keyword">if</span> redis.call(<span class="hljs-string">"SETNX"</span>, KEYS[<span class="hljs-number">1</span>], ARGV[<span class="hljs-number">1</span>]) == <span class="hljs-number">1</span> <span class="hljs-keyword">then</span>
redis.call(<span class="hljs-string">"PEXPIRE"</span>, KEYS[<span class="hljs-number">1</span>], ARGV[<span class="hljs-number">2</span>])
<span class="hljs-keyword">return</span> <span class="hljs-number">1</span>
<span class="hljs-keyword">else</span>
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>
<span class="hljs-keyword">end</span>
</code></div></pre>
<p>解锁</p>
<pre><div class="hljs"><code class="lang-lua"><span class="hljs-comment">-- 只允许加锁者自己解锁</span>
<span class="hljs-keyword">if</span> redis.call(<span class="hljs-string">"GET"</span>, KEYS[<span class="hljs-number">1</span>]) == ARGV[<span class="hljs-number">1</span>] <span class="hljs-keyword">then</span>
<span class="hljs-keyword">return</span> redis.call(<span class="hljs-string">"DEL"</span>, KEYS[<span class="hljs-number">1</span>])
<span class="hljs-keyword">else</span>
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>
<span class="hljs-keyword">end</span>
</code></div></pre>
<h3><a id="63__241"></a>6.3 计数器(限流)</h3>
<pre><div class="hljs"><code class="lang-lua"><span class="hljs-comment">-- 限制某用户每分钟最多访问5次</span>
<span class="hljs-comment">-- KEYS[1] 用户访问计数Key</span>
<span class="hljs-comment">-- ARGV[1] 过期时间(秒)</span>
<span class="hljs-comment">-- ARGV[2] 最大次数</span>
<span class="hljs-keyword">local</span> count = redis.call(<span class="hljs-string">"INCR"</span>, KEYS[<span class="hljs-number">1</span>])
<span class="hljs-keyword">if</span> count == <span class="hljs-number">1</span> <span class="hljs-keyword">then</span>
redis.call(<span class="hljs-string">"EXPIRE"</span>, KEYS[<span class="hljs-number">1</span>], ARGV[<span class="hljs-number">1</span>])
<span class="hljs-keyword">end</span>
<span class="hljs-keyword">if</span> count > <span class="hljs-built_in">tonumber</span>(ARGV[<span class="hljs-number">2</span>]) <span class="hljs-keyword">then</span>
<span class="hljs-keyword">return</span> <span class="hljs-number">0</span> <span class="hljs-comment">-- 超过限制</span>
<span class="hljs-keyword">else</span>
<span class="hljs-keyword">return</span> <span class="hljs-number">1</span> <span class="hljs-comment">-- 允许访问</span>
<span class="hljs-keyword">end</span>
</code></div></pre>
<pre><div class="hljs"><code class="lang-shell">EVAL "local count=redis.call('INCR',KEYS[1]);if count==1 then redis.call('EXPIRE',KEYS[1],ARGV[1]);end;if count>tonumber(ARGV[2]) then return 0 else return 1 end" 1 user:123 60 5
</code></div></pre>
<h2><a id="7__264"></a>7 最佳实践</h2>
<p><strong>短小脚本优先</strong>:脚本越短越好,复杂逻辑放到应用层。</p>
<p><strong>避免长阻塞</strong>:Lua 在 Redis 内部执行,脚本执行期间 Redis 不会处理其他请求,避免死循环。</p>
<p><strong>保证幂等性</strong>:脚本最好设计为幂等操作,避免重复执行造成数据错误。</p>
<ul>
<li>可以给每个操作分配唯一id,判断是否处理过。</li>
<li>常见于:网络重试、MQ多次发送同一条消息、应用崩溃重试。</li>
</ul>
<p><strong>脚本调试</strong>:可以用 <code>redis-cli --ldb --eval</code> 调试 Lua。</p>
评论区