Redis?Lua腳本從入門到避坑指南
一、為什么是 Lua?
Redis 從 2.6.0 版本開始內(nèi)置了 Lua 解釋器。這意味著:
- 零依賴:不需要像 Java 加 Maven 依賴、Python pip 安裝庫那樣做任何前置準(zhǔn)備。
- 開箱即用:只要 Redis 版本 ≥ 2.6,直接用
EVAL命令就能跑。
Lua 在 Redis 里的角色是膠水語言,用它把多個(gè) Redis 命令和業(yè)務(wù)判斷邏輯打包成一個(gè)整體,交給 Redis 服務(wù)端一次性執(zhí)行。
二、核心語法速查(Java 開發(fā)者視角)
1. 大小寫敏感
Lua 嚴(yán)格區(qū)分大小寫。return 不能寫成 Return,KEYS 不能寫成 keys。這和 Java 一致。
2. 變量與local
local count = 10 -- 局部變量,強(qiáng)烈推薦 global_var = "hello" -- 全局變量,危險(xiǎn),會(huì)污染環(huán)境
鐵律:所有變量都用 local 聲明。 這既是為了作用域隔離,也是為了性能。
3. 常用類型
Lua 是動(dòng)態(tài)類型語言,變量沒類型,值有類型。
| 類型 | 寫法 | 備注 |
|---|---|---|
| 數(shù)字 | local n = 10 | |
| 字符串 | local s = "hello" | 拼接用 .. |
| 布爾 | local ok = true | 只有 false 和 nil 為假,0 和空字符串都為真! |
| 空 | local x = nil | |
| 數(shù)組 | local arr = {"a", "b"} | 索引從 1 開始 |
| 字典 | local map = {name="Tom"} | 訪問:map.name 或 map["name"] |
4. 邏輯運(yùn)算符
和 Java 的對(duì)照:
| Java | Lua |
|---|---|
| !a | not a |
| a && b | a and b |
| a || b | a or b |
Lua 的 and/or 是短路求值,返回值本身而不是強(qiáng)制轉(zhuǎn) boolean。常用于設(shè)默認(rèn)值:
local count = redis.call('GET', KEYS[1]) or 0
5. 常用內(nèi)置函數(shù)
| 函數(shù) | 作用 | 示例 |
|---|---|---|
| tonumber(str) | 字符串轉(zhuǎn)數(shù)字 | tonumber("123") → 123 |
| tostring(val) | 轉(zhuǎn)字符串 | tostring(100) → "100" |
| type(val) | 判斷類型 | type(nil) → "nil" |
| string.sub(s, i, j) | 截取子串 | string.sub("hello", 1, 3) → "hel" |
| string.len(s) | 字符串長度 | string.len("hi") → 2 |
| math.abs(n) | 絕對(duì)值 | math.abs(-5) → 5 |
| math.floor(n) | 向下取整 | math.floor(3.9) → 3 |
| math.ceil(n) | 向上取整 | math.ceil(3.1) → 4 |
| math.max(a,b) | 最大值 | math.max(1,5) → 5 |
重要:ARGV 里的參數(shù)全部是字符串,需要用 tonumber() 轉(zhuǎn)成數(shù)字才能做算術(shù)運(yùn)算。
三、EVAL 命令詳解
命令格式
EVAL <腳本內(nèi)容> <key的數(shù)量> <key列表...> <arg列表...>
中間那個(gè)數(shù)字是干什么的?
它是 KEYS 和 ARGV 的分割標(biāo)識(shí),告訴 Redis “前面 N 個(gè)參數(shù)是 KEY,剩下的全是 ARGV”。
示例:
EVAL "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 key1 key2 arg1 arg2
- 2 表示前 2 個(gè)是 KEY:key1, key2
- 剩下的全是 ARGV:arg1, arg2
為什么必須寫這個(gè)數(shù)字?
- 集群路由:Redis Cluster 需要知道哪些是 key,才能校驗(yàn)它們是否在同一個(gè) slot,并正確路由。
- 代碼可讀性:顯式聲明讓腳本意圖清晰,不用猜。
- 安全校驗(yàn):Redis 可在執(zhí)行前做靜態(tài)分析。
即使腳本完全不用 KEYS,也必須寫 0:
EVAL "return 'hello'" 0
腳本里只有 KEYS 和 ARGV 兩個(gè)預(yù)置數(shù)組嗎?
是的。 Redis 的 Lua 沙箱里,除了 KEYS 和 ARGV,沒有任何其他外部數(shù)據(jù)來源。你需要的一切數(shù)據(jù),都得在腳本里通過 redis.call 獲取。
四、Redis 與 Lua 的聯(lián)動(dòng)機(jī)制
執(zhí)行流程(四個(gè)步驟)
- 解析參數(shù):Redis 根據(jù) numkeys 把參數(shù)切分成 KEYS 和 ARGV。
- 注入沙箱:啟動(dòng) Lua 解釋器,創(chuàng)建隔離環(huán)境,注入 KEYS 和 ARGV。
- Lua 執(zhí)行,回調(diào) Redis:腳本通過 redis.call 或 redis.pcall 向宿主 Redis 發(fā)請(qǐng)求。腳本暫停→Redis 執(zhí)行命令→返回結(jié)果給腳本→腳本繼續(xù)。
- 返回結(jié)果:Lua 腳本執(zhí)行完,結(jié)果轉(zhuǎn)成 Redis 協(xié)議返回給客戶端。
redis.callvsredis.pcall
- redis.call('GET', KEYS[1]):執(zhí)行 Redis 命令,出錯(cuò)會(huì)中斷腳本并拋錯(cuò)。
- redis.pcall('GET', KEYS[1]):執(zhí)行 Redis 命令,出錯(cuò)不中斷,以 table 形式返回錯(cuò)誤信息。
用 pcall 可以讓你在腳本內(nèi)部做錯(cuò)誤處理:
local res = redis.pcall('GET', KEYS[1])
if type(res) == 'table' and res['err'] then
return "出錯(cuò)了"
end
整個(gè)腳本執(zhí)行期間,其他命令必須排隊(duì)
這是真的,而且是所有命令都排隊(duì)。 原因是 Redis 的單線程模型:主線程在跑 Lua 時(shí),不可能同時(shí)處理其他請(qǐng)求。
后果:腳本必須快(毫秒級(jí)),否則整個(gè) Redis 會(huì)被阻塞,導(dǎo)致服務(wù)雪崩。
邊界:這只阻塞這一個(gè) Redis 實(shí)例的主線程,不影響你的 Java 應(yīng)用、MySQL、Nginx 等其他進(jìn)程。
五、原子性:最大的優(yōu)勢(shì)與最大的陷阱
能保證什么
絕對(duì)的執(zhí)行原子性:腳本一旦開始,絕不會(huì)被其他命令插隊(duì)。Redis 把它當(dāng)成一個(gè)不可分割的“超級(jí)命令”。
不能保證什么
沒有事務(wù)回滾。腳本執(zhí)行中途如果服務(wù)器崩潰,已執(zhí)行的寫操作不會(huì)自動(dòng)回滾。
示例:
redis.call('SET', 'key1', 'a') -- 執(zhí)行了
-- 此時(shí)斷電
redis.call('SET', 'key2', 'b') -- 沒執(zhí)行
結(jié)果:key1 是新值,key2 還是老樣子,數(shù)據(jù)處于不一致狀態(tài)。
解決方案
把所有校驗(yàn)邏輯放在腳本最前面,寫入操作集中在最后幾行,縮小風(fēng)險(xiǎn)窗口:
-- 先做全部檢查(不改變數(shù)據(jù))
local stock = redis.call('GET', KEYS[1])
if not stock or tonumber(stock) <= 0 then
return -1
end
-- 檢查通過后再集中寫入
redis.call('DECR', KEYS[1])
redis.call('SADD', KEYS[2], ARGV[1])
return 1
六、經(jīng)典實(shí)戰(zhàn):秒殺場景的并發(fā)控制
不用 Lua(錯(cuò)誤方案)
stock = redis.get("stock:001")
if int(stock) > 0:
redis.decr("stock:001")
問題:A 和 B 同時(shí) get 到 1,各自判斷 >0 后各自執(zhí)行 decr,結(jié)果庫存變 -1,超賣。
用 Lua(正確方案)
-- KEYS[1]: 庫存key, KEYS[2]: 已搶用戶set, ARGV[1]: 用戶ID
local stock = redis.call('DECR', KEYS[1])
if stock < 0 then
redis.call('INCR', KEYS[1]) -- 回滾
return -1 -- 賣完
end
redis.call('SADD', KEYS[2], ARGV[1]) -- 記錄用戶
return stock
效果:A 的腳本執(zhí)行期間,B 的腳本完全進(jìn)不來。A 執(zhí)行完,B 再執(zhí)行時(shí)看到的庫存已經(jīng)是 0(或 -1 被回滾),無法多搶。
七、Java 實(shí)戰(zhàn):怎么在代碼里用 Lua
1. 引入依賴(Spring Boot 項(xiàng)目)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>這自帶 Lettuce 客戶端,原生支持 Lua。
2. 定義腳本(推薦用DefaultRedisScript)
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
// 放在靜態(tài)變量里,避免重復(fù)創(chuàng)建
private static final String SECKILL_SCRIPT =
"local stock = redis.call('DECR', KEYS[1]) " +
"if stock < 0 then " +
" redis.call('INCR', KEYS[1]) " +
" return -1 " +
"end " +
"redis.call('SADD', KEYS[2], ARGV[1]) " +
"return stock";
private static final RedisScript<Long> SECKILL =
new DefaultRedisScript<>(SECKILL_SCRIPT, Long.class);3. 執(zhí)行腳本
@Autowired
private StringRedisTemplate stringRedisTemplate;
public Long doSeckill(String stockKey, String usersKey, String userId) {
List<String> keys = Arrays.asList(stockKey, usersKey);
Object[] args = { userId };
return stringRedisTemplate.execute(
SECKILL, // 腳本對(duì)象
keys, // KEYS 列表
args // ARGV 列表
);
}4. 用EVALSHA優(yōu)化性能
每次傳完整腳本有網(wǎng)絡(luò)開銷??梢蕴崆凹虞d腳本到 Redis,后續(xù)用 SHA1 哈希調(diào)用:
// 加載腳本
String sha1 = stringRedisTemplate.getConnectionFactory()
.getConnection().scriptLoad(SECKILL_SCRIPT.getBytes());
// 用 SHA1 執(zhí)行
DefaultRedisScript<Long> cachedScript = new DefaultRedisScript<>();
cachedScript.setSha1(sha1);
cachedScript.setResultType(Long.class);
stringRedisTemplate.execute(cachedScript, keys, args);Spring 的 DefaultRedisScript 默認(rèn)行為:第一次執(zhí)行時(shí)用 EVAL,失敗則嘗試 EVALSHA,透明處理緩存,一般無需手動(dòng)干預(yù)。
八、工程化避坑清單(重要?。?/h2>
1. 腳本必須短小精悍
- 禁止:大循環(huán)、復(fù)雜正則、遍歷大集合(如
SMEMBERS百萬級(jí) Set)。 - 原則:只做快速判斷 + 少量原子操作,耗時(shí)控制在毫秒級(jí)。
2. 永遠(yuǎn)設(shè)置超時(shí)
// 應(yīng)用層設(shè)置網(wǎng)絡(luò)超時(shí)
@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<?, ?> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 關(guān)鍵:設(shè)置命令超時(shí),避免被慢腳本卡死
template.setDefaultTimeout(Duration.ofSeconds(2));
return template;
}3. 集群模式下,所有 KEY 必須在同一個(gè) slot
Redis Cluster 要求 Lua 腳本操作的所有 key 必須在同一節(jié)點(diǎn)。常用的技巧是用 hash tag:
EVAL "..." 2 {order}:stock {order}:users
{order} 告訴 Redis,只對(duì)花括號(hào)里的部分做哈希計(jì)算,保證這兩個(gè) key 落在同一個(gè) slot。
4. 腳本里不要有死循環(huán)
while true do
-- Redis 主線程會(huì)卡死,只能靠 `SCRIPT KILL` 強(qiáng)制中斷
-- 如果腳本已經(jīng)執(zhí)行過寫命令,SHUTDOWN NOSAVE 是最后手段
end
Redis 7.0+ 有 lua-time-limit 保護(hù)(默認(rèn) 5 秒),但仍然不建議測(cè)試這個(gè)底線。
5. 合理拆分腳本
不要把一個(gè)大而全的腳本用于所有場景。按職責(zé)拆分:扣庫存一個(gè)腳本、發(fā)優(yōu)惠券一個(gè)腳本、記錄日志一個(gè)腳本。保持原子性邊界最小化。
6. 充分測(cè)試邊界條件
- 庫存為 0 時(shí)
- 參數(shù)為空時(shí)
- 參數(shù)非法時(shí)(傳了非數(shù)字)
- 并發(fā)壓測(cè)驗(yàn)證原子性
九、總結(jié):Lua 在 Redis 中的獨(dú)特定位
| 維度 | 常規(guī)編程語言(Java/Python) | Redis Lua 腳本 |
|---|---|---|
| 執(zhí)行位置 | 客戶端 | 服務(wù)端(靠近數(shù)據(jù)) |
| 網(wǎng)絡(luò)開銷 | N 次往返 | 1 次 |
| 原子性 | 需借助事務(wù)/分布式鎖 | 天然原子性 |
| 邏輯能力 | 無限 | 受限(但夠用) |
| 復(fù)雜度 | 靈活,但容易出錯(cuò) | 簡單,腳本短小即安全 |
一句話總結(jié):Redis Lua 腳本解決的是“需要在一次往返中,原子性地執(zhí)行多條 Redis 命令且夾雜業(yè)務(wù)判斷”這個(gè)特定問題。它不是用來取代 Java 的,而是讓你的 Java 應(yīng)用在處理某些 Redis 操作時(shí),更安全、更高效。
到此這篇關(guān)于Redis Lua腳本從入門到避坑指南的文章就介紹到這了,更多相關(guān)Redis Lua腳本入門內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
用Redis實(shí)現(xiàn)微博關(guān)注關(guān)系
在微博中,每一個(gè)用戶都會(huì)有一個(gè)關(guān)注列表,一個(gè)粉絲列表。用戶可以查看自己的關(guān)注,粉絲列表,也可以查看別人的關(guān)注,粉絲列表。并且,要展示列表里每個(gè)人與當(dāng)前查看者的關(guān)注狀態(tài)。2015-09-09
銀河麒麟V10sp1服務(wù)器系統(tǒng)安裝redis不能使用的快速解決辦法
這篇文章主要介紹了銀河麒麟V10sp1服務(wù)器系統(tǒng)安裝redis不能使用的快速解決辦法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01
redis+lua實(shí)現(xiàn)限流的項(xiàng)目實(shí)踐
redis有很多限流的算法(比如:令牌桶,計(jì)數(shù)器,時(shí)間窗口)等,在分布式里面進(jìn)行限流的話,我們則可以使用redis+lua腳本進(jìn)行限流,下面就來介紹一下redis+lua實(shí)現(xiàn)限流2023-10-10
Windows系統(tǒng)一鍵啟動(dòng)Redis腳本
本文介紹了在Windows系統(tǒng)中創(chuàng)建一鍵啟動(dòng)Redis的腳本,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-12-12
詳解Redis終端操作和Redis-plus-plus接口使用教程
這篇文章主要介紹了詳解Redis終端操作和Redis-plus-plus接口使用教程,本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2026-03-03
Redis中Redisson布隆過濾器的學(xué)習(xí)
布隆過濾器是一個(gè)非常長的二進(jìn)制向量和一系列隨機(jī)哈希函數(shù)的組合,可用于檢索一個(gè)元素是否存在,本文就詳細(xì)的介紹一下Redisson布隆過濾器,具有一定的參考價(jià)值,感興趣的可以了解一下2022-05-05
odoo中使用redis實(shí)現(xiàn)緩存的步驟
這篇文章主要介紹了odoo中使用redis實(shí)現(xiàn)緩存的步驟,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-04-04

