Lua 深度学习笔记
第一部分:Lua 是什么?(顶层认知)
1.1 一句话定义
Lua 是一种轻量级、快速、高效的编程语言。
但这太抽象了。让我们用更有趣的方式理解。
1.2 类比理解
如果Java是"办公楼"
├─ 功能完整
├─ 大而全
├─ 启动慢
└─ 规则复杂
那么Lua就是"便利店"
├─ 功能基础
├─ 小而精
├─ 响应快
└─ 规则简单
关键点: Lua 被设计成可以嵌入到其他程序中运行的脚本语言。
1.3 Lua 在产业中的位置
编程语言谱系图
系统语言
│
┌──────────┼──────────┐
│ │ │
C/C++ Java Go
(低阶,快速) (中阶,通用) (并发)
脚本语言
│
┌──────────┼──────────┐
│ │ │
Python JavaScript Lua
(通用脚本) (Web脚本) (嵌入脚本)
Lua的核心特点:轻量级 + 可嵌入
第二部分:为什么需要 Lua?(问题导向)
2.1 现实场景分析
场景 1:在 Redis 中执行原子操作
问题: 我需要在 Redis 中做"读-判断-写"三个操作,必须不可分割(原子性)。
// Java中分开执行(不安全)
int count = redis.get("counter"); // 读
if (count > 10) { // 判断
redis.set("flag", "yes"); // 写
}
// 问题:在读和写之间,其他线程可能修改了count
// 导致多个线程同时进入if,数据混乱
Lua 的解决方案: 把所有操作放在一个 Lua 脚本里,Redis 执行脚本时是原子的。
-- checkNeedCaptcha.lua
local count = redis.call('get', 'counter')
if count > 10 then
redis.call('set', 'flag', 'yes')
return 'true'
else
redis.call('set', 'flag', 'no')
return 'false'
end
-- Redis 保证整个脚本从开始到结束不会被中断
场景 2:减少网络往返(RTT)
问题: 如果分开执行,需要 3 次网络请求。
网络延迟时间线(RTT = Round Trip Time)
分开执行方式:
请求1: Java → Redis → Java (读) ← 耗时 T
请求2: Java → Redis → Java (判断) ← 耗时 T (耗时 3T)
请求3: Java → Redis → Java (写) ← 耗时 T
总耗时: 3T
用 Lua 脚本方式:
请求1: Java → Redis → Java (整个脚本) ← 耗时 T
总耗时: T
节省 66% 的网络开销!
场景 3:在 Redis 中完成复杂的业务逻辑
问题: 有些操作逻辑复杂,如果来回折腾网络太低效。
传统方式:
Java代码 ← 网络 → Redis ← 网络 → Java代码 ← 网络 → Redis ...
(多轮交互,耗时且出错风险高)
Lua方式:
Java代码 → Redis(一次)→ [Lua脚本完整执行] → Java代码
(一轮交互,快速且原子)
2.2 Lua 的核心优势总结
| 优势 | 说明 | 示例 |
|---|---|---|
| 原子性 | 脚本从开始到结束不会被中断 | Redis 限流、抢购防超卖 |
| 减少 RTT | 一次网络请求完成多个操作 | 省去多次 Redis 往返 |
| 内置Redis命令 | 能直接调用 redis.call() | 不需要在 Java 和 Redis 之间来回 |
| 轻量级 | 文件小、启动快 | 脚本仅几KB,秒级加载 |
| 可嵌入 | 可集成到其他程序 | Redis、Nginx、游戏引擎都内置Lua |
第三部分:Lua 的语言设计(学习基础)
3.1 Lua 的设计哲学
Lua 的设计者说过一句名言:
"Do one thing, and do it well."
不要什么都会,但要把该会的事做好。
Lua设计原则:
┌─────────────────────────────────┐
│ 1. 简单易学 (语法少) │
│ └─ 没有class、没有接口等 │
│ │
│ 2. 快速执行 (高效) │
│ └─ 字节码+VM优化 │
│ │
│ 3. 轻量级 (资源少) │
│ └─ Lua解释器仅440KB │
│ │
│ 4. 可嵌入 (容易集成) │
│ └─ C/C++可轻松调用 │
│ │
│ 5. 灵活 (适应性强) │
│ └─ 动态类型、table等 │
└─────────────────────────────────┘
3.2 Lua 与其他语言的对比
语言特性对比表
Lua Python Java
────────────────────────────────
简洁度 ★★★★★ ★★★★ ★★
学习难度 ★★ ★★★ ★★★★
执行速度 ★★★★ ★★★ ★★★★★
内存占用 ★★★★★ ★★★ ★★
集成难度 ★★ ★★★★ ★★★★
────────────────────────────────
结论:
Lua最适合"嵌入场景"
3.3 Lua 的核心数据结构
Lua 只有 一种复杂数据结构:Table(表)
-- 几乎所有复杂数据都是 Table
-- 1. 数组(Array)
local arr = {1, 2, 3, 4, 5}
-- 2. 字典(Dictionary / Hash Map)
local dict = {name = "Tom", age = 30}
-- 或者
local dict = {}
dict["key1"] = "value1"
-- 3. 对象(Object)- 本质还是 Table
local person = {
name = "Tom",
age = 30,
greet = function(self)
print("Hello, I'm " .. self.name)
end
}
-- 4. 其他都是Table的变体
关键认知: Lua 用"简单"换"灵活"。没有专门的 class/interface,但 Table 足以表达一切。
第四部分:Lua 在 Redis 中的应用(实战重点)
4.1 Redis 如何执行 Lua 脚本
执行流程
┌──────────────────────────────────────────────────────────┐
│ Java 应用端 │
└────────────────────┬─────────────────────────────────────┘
│ 1. 加载脚本文件
│ (或直接传脚本文本)
│
┌────────────▼──────────────┐
│ 2. 调用 redis.script.load │
│ 或 redis.script.evalsha│
└────────────┬──────────────┘
│
│ 3. 网络传输
│ (脚本+参数)
│
┌────────────────────▼──────────────────────────────────────┐
│ Redis 服务端 │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 4. Lua虚拟机加载脚本 │ │
│ │ (Redis 内置的 Lua 5.1) │ │
│ └────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────────────┐ │
│ │ 5. 执行脚本逻辑 │ │
│ │ • 解析KEYS参数 │ │
│ │ • 解析ARGV参数 │ │
│ │ • 调用redis.call()操作 │ │
│ │ • 执行业务逻辑(if/for等) │ │
│ │ • 返回结果 │ │
│ │ │ │
│ │ 【关键】所有操作在一个事务中 → 原子性保证 │ │
│ └────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ┌────────────────▼─────────────────────────────────────┐ │
│ │ 6. 返回结果给 Java │ │
│ │ (可以是 string/number/table) │ │
│ └────────────────┬─────────────────────────────────────┘ │
└────────────────────┬──────────────────────────────────────┘
│
│ 7. Java接收结果
▼
┌──────────────────────────┐
│ 继续业务逻辑处理 │
└──────────────────────────┘
4.2 关键概念:EVALSHA vs EVAL
两种执行方式对比
方式1:EVAL (传脚本文本)
Java → redis.eval("local x = ...", keys, args)
│
└─ 优点:简单直观
缺点:每次都传脚本,网络浪费
方式2:EVALSHA (传脚本哈希)
步骤1:redis.script.load(脚本) ← 加载一次,返回SHA1
步骤2:redis.evalsha(SHA1, keys, args) ← 后续都用哈希
│
├─ 优点:网络高效,脚本只传一次
└─ 缺点:需要先加载
在高并发场景,通常用 EVALSHA:
• 初始化时 script.load() 一次
• 后续直接 evalsha(),节省网络
4.3 Lua 与 Redis 命令的交互
-- checkNeedCaptcha.lua 中的典型交互
-- 1. 读数据
local count = redis.call('get', counter_count_key)
-- 2. 写数据
redis.call('set', counter_count_key, count)
-- 3. 设置过期时间
redis.call('expire', verify_captcha_id, 60)
-- 4. 条件判断(Lua语言,不是Redis命令)
if count > 10 then
redis.call('set', flag, 'yes')
else
redis.call('set', flag, 'no')
end
-- 5. 返回结果给Java
return 'true' -- 或 'false'
关键分工:
- Lua语言部分(if/for/等):脚本逻辑
- redis.call():调用Redis命令
- 原子性:整个脚本执行过程中,其他客户端的请求被暂停
第五部分:Lua 脚本的原子性保证(核心价值)
5.1 为什么原子性很重要?
问题场景:没有原子性
// Java 中分开执行(非原子)
int count = redis.get("counter"); // 读:count=10
// 【时间点A】其他线程可能已修改 counter
if (count > 10) { // 判断:10 > 10? false
redis.set("flag", "yes"); // 不执行
}
redis.set("flag", "no"); // 执行
// 问题:虽然我们在Java里判断是<=10,
// 但实际上Redis中的counter可能已经是20了
// 逻辑产生了矛盾!
解决方案:用 Lua 脚本(原子)
-- 整个脚本从START到END,其他客户端看不到中间过程
-- 要么全部执行,要么全部不执行
-- START ─────────────────────────────────────
local count = redis.call('get', 'counter') -- 读
if count > 10 then -- 判断
redis.call('set', 'flag', 'yes') -- 写
else
redis.call('set', 'flag', 'no') -- 写
end
return 'done'
-- END ───────────────────────────────────────
-- 【保证】上面的所有操作,对其他客户端是透明的
-- 其他客户端要么看到执行前,要么看到执行后
-- 看不到中间过程
5.2 原子性的实现机制
Redis的单线程架构 + Lua脚本
Redis服务端(单线程)
时间线:
T1: 客户端1 发来 redis.set('a', 1)
├─ Redis执行
├─ 完成 ✓
│
T2: 客户端2 发来 Lua脚本(10行)
├─ Redis 加载脚本到虚拟机
├─ 执行第1行 (redis.call('get', 'x'))
├─ 执行第2行 (if判断)
├─ 执行第3行 (redis.call('set', 'y', ...))
├─ ...
├─ 执行第10行 (return)
├─ 脚本执行完成 ✓
│ 【关键】中间没有其他客户端的命令插入!
│
T3: 客户端3 发来 redis.get('y')
├─ Redis执行
├─ 完成 ✓
结论: Redis单线程 + Lua脚本组合 = 天然原子性
5.3 对比:Java中如何实现类似的原子性
// 方式1:使用同步块(不推荐,在应用层)
private Object lock = new Object();
synchronized(lock) {
int count = redis.get("counter");
if (count > 10) {
redis.set("flag", "yes");
} else {
redis.set("flag", "no");
}
}
// 问题:
// 1. 只能保护Java的操作,Redis还是并发的
// 2. 锁持续时间长(包括网络延迟),竞争激烈
// 3. 多个Java实例无法共享锁
// 方式2:使用Redis Lua脚本(推荐!)
redis.evalsha(scriptSha, keys, args);
// 优点:
// 1. 保护了Redis操作(真正的原子)
// 2. 锁持续时间短(脚本执行时间毫秒级)
// 3. 全局有效(多实例共享Redis)
第六部分:大麦网案例深度解析
6.1 checkNeedCaptcha.lua 为什么需要 Lua?
场景:全局限流,决定是否需要验证码
需求分析:
┌─────────────────────────────────────────┐
│ 1. 读取全局计数器 │
│ 2. 获取上次重置时间 │
│ 3. 判断是否过了1秒 │
│ 4. 如果过了,重置计数器 │
│ 5. 对计数器+1 │
│ 6. 判断是否超过阈值(10) │
│ 7. 根据结果,写入标记('yes'或'no') │
│ 8. 返回结果给Java │
└─────────────────────────────────────────┘
如果分开执行(Java+Redis多次往返):
Java代码 Redis
├─ 发命令1 ────────────>
│ GET COUNTER_COUNT
│<────────────────────(网络延迟)
│
├─ 发命令2 ────────────>
│ GET COUNTER_TIMESTAMP
│<────────────────────(网络延迟)
│
├─ 【Java判断逻辑】
│ (计算count, 判断1秒)
│
├─ 发命令3 ────────────>
│ SET COUNTER_COUNT
│<────────────────────(网络延迟)
│
├─ 发命令4 ────────────>
│ SET VERIFY_CAPTCHA_ID
│<────────────────────(网络延迟)
总耗时:网络延迟 × 4 = 4T(非常慢!)
用 Lua 脚本执行:
Java代码 Redis
├─ 发脚本 ────────────>
│ (包含所有逻辑)
│
│<────────────────────(一次往返)
│ 脚本执行完,返回结果
总耗时:网络延迟 × 1 = T(快4倍!)
而且【原子性】保证:
- 读数据、判断、写标记 一气呵成
- 其他请求看不到中间过程
- 防止了并发竞争导致的计数错误
6.2 Lua 脚本的结构分析
-- 第1部分:接收参数
local counter_count_key = KEYS[1]
local verify_captcha_threshold = tonumber(ARGV[1])
-- 第2部分:强制开启检查(紧急防御)
if always_verify_captcha == 1 then
redis.call('set', verify_captcha_id, 'yes')
return 'true'
end
-- 第3部分:获取当前状态(读操作)
local count = tonumber(redis.call('get', counter_count_key) or "0")
local lastResetTime = tonumber(redis.call('get', counter_timestamp_key) or "0")
-- 第4部分:时间窗口处理(业务逻辑)
if current_time_millis - lastResetTime > 1000 then
count = 0
redis.call('set', counter_count_key, count)
end
-- 第5部分:阈值判定(核心逻辑)
count = count + 1
if count > verify_captcha_threshold then
redis.call('set', verify_captcha_id, 'yes')
return 'true'
else
redis.call('set', verify_captcha_id, 'no')
return 'false'
end
为什么这个逻辑不能在Java中做?
关键原因:Redis 中的操作不能中断
如果逻辑分散在Java和Redis中:
时刻T1:
Java获取count=10
时刻T2:
【其他客户端发来请求】
Redis的count变为11、12、13...
Java还在做判断...
时刻T3:
Java判断:count(10) > 10? No
redis.set(flag, 'no')
问题:实际count已经是13了,但我们设置的是'no'
逻辑错误!
用Lua脚本,整个过程是原子的:
时刻T1:
[Lua脚本开始执行]
├─ 读count=10
├─ 【其他客户端请求来临,但被暂停】
├─ count+1=11
├─ 判断11 > 10? Yes
├─ 设置标记'yes'
[Lua脚本结束]
时刻T2:
【其他客户端的请求现在才执行】
保证逻辑正确!
第七部分:Lua 实战最佳实践
7.1 何时该用 Lua?(决策树)
需要在Redis中执行操作吗?
│
是
│
┌─────────────┴─────────────┐
│ │
【只用一条命令?】 【需要多条命令】
/ \ │
是 否 【命令间有依赖关系?】
│ / \
│ 是 否
│ │ │
不用Lua 【需要原子性?】 可以分开执行
直接用Redis / \ 不用Lua
命令 是 否
│ │ │
用Lua! 用Lua! 一般不用Lua
(高效) (推荐) (e.g.日志)
实践判断标准:
┌────────────────────────────────────────┐
│ ✓ 用 Lua 的场景(高优先级) │
│ │
│ 1. 需要原子性 → 限流、库存、抢购 │
│ 2. 多条命令相互依赖 → 读-判-写 │
│ 3. 减少网络往返 → 高并发场景 │
│ 4. 业务逻辑复杂 → 多个判断分支 │
│ 5. 数据一致性关键 → 金融场景 │
│ │
│ 例子:verify_captcha, 防超卖, 库存减 │
└────────────────────────────────────────┘
┌────────────────────────────────────────┐
│ ✗ 不用 Lua 的场景 │
│ │
│ 1. 简单操作 → SET/GET/INCR │
│ 2. 独立操作 → 日志、统计计数 │
│ 3. 逻辑全在应用层 → 数据处理 │
│ 4. 网络不敏感 → 低频操作 │
│ │
│ 例子:用户点赞数+1, 日志记录 │
└────────────────────────────────────────┘
7.2 开发流程:从编写到运行
Step 1: 编写脚本文件
────────────────────
目录结构:
src/main/resources/
└── lua/
└── checkNeedCaptcha.lua
脚本内容:
local count = redis.call('get', KEYS[1]) or 0
count = count + 1
redis.call('set', KEYS[1], count)
...
Step 2: Spring 初始化加载
────────────────────────
@Component
public class CheckNeedCaptchaOperate {
private DefaultRedisScript<String> redisScript;
@PostConstruct
public void init() {
redisScript = new DefaultRedisScript<>();
// 从classpath加载脚本
redisScript.setScriptSource(
new ResourceScriptSource(
new ClassPathResource("lua/checkNeedCaptcha.lua")
)
);
// 指定返回类型
redisScript.setResultType(String.class);
}
}
好处:
• 项目启动时加载一次(1ms)
• 后续使用EVALSHA,只需传哈希(高效)
• 避免每次都读文件
Step 3: 执行脚本
────────────────
public Boolean checkNeedCaptcha(List<String> keys, String[] args) {
// 调用脚本
Object result = redisCache.getInstance()
.execute(redisScript, keys, args);
// 结果处理
return Boolean.parseBoolean((String)result);
}
参数说明:
keys → Redis Key列表 [counter_count, counter_timestamp, verify_id]
args → Lua参数列表 [10, 1700000000123, 60, 0]
result → Lua脚本返回值
Step 4: 集成测试
────────────────
@Test
public void testCheckNeedCaptcha() {
// 准备参数
List<String> keys = Arrays.asList("counter", "timestamp", "flag_123");
String[] args = {"10", "1700000000000", "60", "0"};
// 执行脚本
Boolean result = captchaOperate.checkNeedCaptchaOperate(keys, args);
// 验证结果
assertTrue(result); // 应该返回true或false
// 验证Redis中的值
String flagValue = redis.get("flag_123");
assertEquals("yes", flagValue);
}
7.3 常见坑与避坑指南
❌ 坑1:脚本太长太复杂(难以维护)
症状:脚本500行,if嵌套20层
原因:试图在脚本中完成所有业务逻辑
解决:
• 保持脚本<100行
• 复杂逻辑放在Java中
• Lua只做"原子操作"部分
正确方式:
Lua脚本: 只做 读-判-写 的原子化
Java代码: 负责复杂的业务逻辑、数据转换
❌ 坑2:脚本返回复杂数据结构(序列化困难)
症状:return {user={name="Tom", age=30}}
问题:Lua的table转Java的Object很复杂
解决:
• 只返回基本类型(string/number)
• 复杂数据在Java层组装
好的实践:
return 'true' -- 字符串
return '{"code":"ok"}' -- JSON字符串(Java parse)
return 100 -- 数字
不好的实践:
return {code=0, data={...}} -- 复杂table
❌ 坑3:忘记设置Key过期时间(内存泄漏)
症状:Redis内存不断增长
原因:created verify_captcha_id_123456 但没有EXPIRE
解决:
redis.call('expire', key, ttl) -- 必须加!
完整模板:
redis.call('set', myKey, myValue)
redis.call('expire', myKey, 60) -- 60秒后自动删除
❌ 坑4:脚本中有网络操作或阻塞操作(锁死Redis)
症状:Redis响应变慢,所有请求堆积
原因:脚本中调用HTTP接口、数据库等耗时操作
解决:禁止!脚本只能做本地运算和Redis操作
禁止清单:
✗ redis.call('HTTP_GET', url) -- 不存在
✗ redis.call('SELECT FROM db...') -- 不存在
✗ sleep(1000) -- 会锁死
✗ io.open('file.txt') -- 文件I/O
允许清单:
✓ redis.call('GET', key) -- Redis操作
✓ math.max(a, b) -- 本地运算
✓ string.format() -- 字符串操作
✓ table operations -- 表操作
❌ 坑5:没有考虑脚本版本管理(升级困难)
症状:脚本更新后,老客户端仍用老版本
原因:没有版本控制机制
解决:
• 使用EVALSHA方式,脚本哈希包含版本信息
• 发布脚本时做好日志记录
• 支持脚本灰度升级
版本管理方案:
v1: checkNeedCaptcha_v1.lua (SHA1: abc123...)
v2: checkNeedCaptcha_v2.lua (SHA1: def456...)
启动时:
scriptSha_v2 = redis.script.load(v2_content)
# 逐步升级,保证兼容性
❌ 坑6:脚本执行超时(Redis卡顿)
症状:Redis管理员告诉你"你的脚本运行1分钟!"
原因:脚本中有死循环或过度复杂
解决:
• 控制脚本逻辑复杂度
• 不要在脚本中做大规模数据处理
Redis脚本限制:
默认超时 = 5秒
超时后 → Redis强制杀死脚本
best practice:
脚本执行时间应该 < 100ms (毫秒级)
✓ 正确做法总结:
1. 脚本简洁 (<100行)
2. 只做原子操作
3. 返回基本类型
4. 记得设置过期时间
5. 禁止网络/IO操作
6. 做好版本管理
7. 监控脚本执行时间
第八部分:Lua vs 其他技术方案(对比分析)
8.1 原子性问题的5种解决方案
需求:在分布式系统中安全地执行"扣款100元"
方案1:Java synchronized(不推荐)
┌──────────────────────────────────────┐
│ synchronized(lock) { │
│ int balance = redis.get("account"); │
│ if (balance >= 100) { │
│ redis.set("account", balance-100);│
│ } │
│ } │
└──────────────────────────────────────┘
分析:
✗ 只保护单机Java代码
✗ Redis操作仍并发
✗ 多实例互不影响,全局不安全
✗ 锁持续时间长(包含网络延迟)
✗ 性能差,竞争激烈
适用场景:单机应用(现在几乎没人用)
方案2:数据库事务(传统方案)
┌──────────────────────────────────────┐
│ @Transactional │
│ public void debit() { │
│ Account acc = query("123"); │
│ acc.balance -= 100; │
│ save(acc); │
│ } │
└──────────────────────────────────────┘
分析:
✓ 保证数据库级别原子性
✓ 安全可靠
✗ 每次都打数据库,网络往返多
✗ 数据库连接池压力大
✗ 高并发时容易超时
适用场景:小并发、强一致性要求高
不适合高并发实时场景
方案3:分布式锁 (Redlock)
┌──────────────────────────────────────┐
│ lock = redlock.lock("account_123"); │
│ try { │
│ balance = redis.get("account"); │
│ redis.set("account", balance-100); │
│ } finally { │
│ lock.unlock(); │
│ } │
└──────────────────────────────────────┘
分析:
✓ 跨实例生效
✓ Redis层面同步
✗ Redlock实现复杂,易出bug
✗ 多线程竞争,吞吐量受限
✗ 仍需多次网络往返
✗ 存在死锁、超时等问题
适用场景:需要分布式锁的通用场景
但不是最优解
方案4:消息队列 (Queue)
┌──────────────────────────────────────┐
│ // 生产者 │
│ queue.send("debit_msg", { │
│ account_id: "123", │
│ amount: 100 │
│ }); │
│ │
│ // 消费者(单线程处理) │
│ msg = queue.consume(); │
│ balance = redis.get(account_id); │
│ redis.set(account_id, balance-100); │
└──────────────────────────────────────┘
分析:
✓ 保证顺序性
✓ 天然的原子操作(单线程消费)
✗ 有延迟(消息在队列中等待)
✗ 复杂(需要额外的MQ系统)
✗ 难以实时反馈结果给用户
适用场景:批量处理、异步操作
不适合实时扣款
方案5:Redis Lua脚本(最优!✓✓✓)
┌──────────────────────────────────────┐
│ redis.evalsha( │
│ "def123...", │
│ ["account_123"], │
│ ["100"] │
│ ); │
│ │
│ -- Lua脚本: │
│ local balance = redis.call('GET',..);│
│ if balance >= 100 then │
│ redis.call('SET',...,balance-100); │
│ return 'success' │
│ end │
└──────────────────────────────────────┘
分析:
✓ Redis原生支持,无额外复杂度
✓ 一次网络请求完成原子操作
✓ 脚本简洁优雅
✓ 性能最高
✓ 天然的分布式安全
✓ 实时返回结果
✓✓✓ 高并发必选!
适用场景:高并发、实时、强一致性
限流、防超卖、库存系统
【结论】
方案1(synchronized) ← 不用
方案2(DB事务) ← 小并发
方案3(Redlock) ← 通用分布式
方案4(MQ) ← 异步场景
方案5(Lua) ← 高并发实时(最优选择!)
大麦网选择Lua的理由:
• 高并发秒杀场景
• 需要真正的原子性
• 实时反馈验证结果
• 性能要求极高
→ Lua完美匹配!
8.2 执行效率对比
假设需要3个操作(READ → JUDGE → WRITE)
网络延迟 = 1ms/次
Redis执行时间 = 0.1ms
方案 操作流程 总耗时 网络往返
──────────────────────────────────────────────────────
synchronized
read(1ms)
judge(无网络) 3.1ms 3次
write(1ms)
DB事务 query db(5ms)
compute(无网络) 15.1ms 3次
update db(5ms)
Redlock lock(1ms)
read(1ms) 4.2ms 4次
write(1ms)
unlock(1ms)
MQ send msg(1ms)
consume & process(50ms) 51ms 2次
[有延迟!]
Lua脚本 evalsha(1ms)
[脚本内:读-判-写] 1.3ms 1次
← 最快!
性能对比图:
耗时 (ms)
│
│ ■ Lua脚本 1.3ms ■
│ ■ Redlock 4.2ms ■■
│ ■ synch... 3.1ms ■■
│ ■ MQ 51ms ■■■■■■■■■■■■
│ ■ DB事务 15.1ms ■■■■
│
└────────────────────
Lua 的优势:
• 网络往返最少(只需1次)
• 执行时间最短(1.3ms)
• 吞吐量最高(可达万级QPS)
第九部分:Lua 的扩展应用(眼界开阔)
9.1 Lua 在其他领域的应用
Lua不只在Redis中应用,它是一种通用脚本语言
┌──────────────────────────────────────────────────┐
│ 1. 游戏引擎(最广泛应用) │
│ │
│ • Unity → C# (不用Lua) │
│ • Cocos2d-x → Lua (热门引擎) │
│ • Love2D → Lua原生 │
│ • 王者荣耀、梦幻西游都用Lua写业务逻辑 │
│ │
│ 优势:可以热更新(不用重编译) │
│ 这对游戏运营很重要 │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ 2. Web服务器 (Nginx) │
│ │
│ nginx.conf: │
│ location /api/check { │
│ content_by_lua_block { │
│ if ngx.var.request_method == "POST" then
│ local body = ngx.req.read_body() │
│ return ngx.say("ok") │
│ end │
│ } │
│ } │
│ │
│ 用途:网关层快速处理、限流、鉴权 │
│ 优势:避免请求到达Java,在网关层直接过滤 │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ 3. 配置管理 (Ansible) │
│ │
│ playbook.yml: │
│ tasks: │
│ - name: Execute config script │
│ script: config.lua │
│ args: │
│ - "production" │
│ │
│ 用途:运维自动化脚本 │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ 4. 数据库脚本 (Tarantool) │
│ │
│ Tarantool = Redis + Lua + DB │
│ 内存数据库,原生支持Lua,比Redis功能更强 │
│ 用于超高并发场景 │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│ 5. 智能家居 (NodeMCU) │
│ │
│ ESP8266芯片运行Lua │
│ 开发IoT设备的首选语言 │
└──────────────────────────────────────────────────┘
9.2 Redis 高级 Lua 应用示例
应用1:库存扣减(防超卖)
-- inventory_deduct.lua
-- 安全地扣减库存,防止超卖
local key = KEYS[1] -- 商品ID
local amount = tonumber(ARGV[1]) -- 购买数量
local current = tonumber(redis.call('GET', key) or "0")
if current >= amount then
redis.call('DECRBY', key, amount)
return 'success'
else
return 'insufficient'
end
应用2:限流通道(令牌桶)
-- rate_limit.lua
-- 令牌桶算法实现
local rate_key = KEYS[1]
local rate_limit = tonumber(ARGV[1]) -- 100 tokens/sec
local current_tokens = tonumber(redis.call('GET', rate_key) or rate_limit)
if current_tokens > 0 then
redis.call('DECR', rate_key)
redis.call('EXPIRE', rate_key, 1)
return 'allowed'
else
return 'blocked'
end
应用3:分布式会话锁
-- distributed_lock.lua
-- 用Lua实现简单的分布式锁
local lock_key = KEYS[1]
local lock_id = ARGV[1]
local expire_time = tonumber(ARGV[2])
-- 原子地检查并设置
if redis.call('EXISTS', lock_key) == 0 then
redis.call('SET', lock_key, lock_id)
redis.call('EXPIRE', lock_key, expire_time)
return 'locked'
else
return 'already_locked'
end
这些都是Lua在实战中的真实应用。
第十部分:学习总结与进阶方向
10.1 Lua 学习脑图
┌─── Lua是什么
│ ├─ 轻量级脚本语言
│ ├─ 可嵌入式设计
│ └─ 简洁高效
│
Lua掌握 ─┼─── 为什么用Lua
│ ├─ 原子性保证
│ ├─ 减少网络往返
│ └─ 业务逻辑聚合
│
├─── Lua基础语法
│ ├─ 数据类型(Table)
│ ├─ 流程控制(if/for)
│ └─ 函数定义
│
├─── Redis中的应用
│ ├─ redis.call()
│ ├─ EVAL vs EVALSHA
│ └─ 脚本加载
│
├─── 实战优化
│ ├─ 脚本简洁化
│ ├─ 参数配置化
│ └─ 性能监控
│
└─── 高级应用
├─ 分布式锁
├─ 限流算法
└─ 库存管理
10.2 进阶学习路线
Level 1: 入门 (已掌握)
□ Lua语法基础
□ Redis.call()使用
□ 简单的限流脚本
Level 2: 中级 (下一步)
□ 脚本优化(减少网络往返)
□ 脚本版本管理
□ 数据结构应用(集合、排序集)
□ 错误处理
Level 3: 高级 (长期目标)
□ 复杂算法实现(HyperLogLog、Bloom Filter)
□ 多Lua脚本协作
□ Lua + Nginx 网关层应用
□ Tarantool(Redis++)
Level 4: 架构 (深度应用)
□ 高并发系统设计
□ 库存管理系统
□ 实时计费系统
□ 分布式事务
10.3 常用资源
推荐学习资源:
官方文档:
• Redis Lua脚本官方文档
https://redis.io/commands/eval
Lua语言教程:
• Lua官网: https://www.lua.org/
• Lua 5.1参考手册
实战参考:
• 大麦网源码(本项目)
• Redis官方示例库
• Nginx + Lua教程
工具支持:
• Redis CLI (redis-cli EVAL)
• Lua IDE (ZeroBrane Studio)
• 在线Lua编译器
进阶书籍:
• 《Redis深度历险》
• 《Redis开发与运维》
• 《Nginx高性能Web服务器》
第十一部分:核心要点回顾(速记)
11.1 一张表总结 Lua
维度 核心要点
────────────────────────────────────────
是什么 轻量级脚本,可嵌入到Redis中
为什么用 ✓ 原子性(不会被中断)
✓ 低延迟(一次网络往返)
✓ 高效率(减少RTT)
怎么用 redis.evalsha(脚本, KEYS, ARGV)
何时用 需要原子性 + 多个Redis操作
关键特性 ├─ redis.call()调用命令
├─ 脚本从START到END无中断
├─ 基于Redis单线程
└─ 返回简单类型给Java
常见模式 ├─ 限流(阈值判定)
├─ 防超卖(库存检查)
├─ 分布式锁(互斥)
└─ 采样触发(柔性防护)
避坑指南 ├─ 脚本<100行(保持简洁)
├─ 禁止网络操作(会锁死)
├─ 记得设置过期(防泄漏)
└─ 返回基本类型(易转换)
性能指标 ├─ 网络往返: 1次
├─ 执行时间: <100ms
├─ 吞吐量: 万级QPS
└─ 原子性: 100%保证
11.2 快速判断是否需要 Lua
问题 判断 方案
────────────────────────────────────
单个Redis操作? 是 不用Lua
否
多个操作独立? 是 不用Lua
否
需要原子性? 是 ───→ 用Lua ✓
否
业务逻辑简单? 是 可以不用
否 ───→ 用Lua ✓
快速判断:
"我需要在Redis中原子地执行多个操作吗?"
是 → 用Lua
否 → 不用
11.3 Lua 的"一图流"记忆法
【Java应用】
│
│ EVALSHA
▼
┌─────────────────┐
│ Redis单线程 │
│ ┌───────────┐ │
│ │ Lua虚拟机 │ │
│ │ ┌─────────┴─┐│
│ │ │脚本执行 ││
│ │ │原子操作 ││
│ │ │无中断 ││
│ │ └─────────┬─┘│
│ └───────────┘ │
│ │返回结果 │
└────────┬────────┘
│
┌─────▼─────┐
│返回值给Java│
└───────────┘
记忆关键:
- 单线程 + 脚本 = 原子性
- 原子性 + 高效 = 解决高并发
- 一次往返 + 无等待 = 极速体验
总结:为什么大麦网选择 Lua
回到原点,理解大麦网为什么在全局流量识别中使用 Lua:
高并发秒杀场景:
├─ 1秒内可能有1万个注册请求
├─ 需要快速判断:是否需要验证码
├─ 决策依据:全局计数器值
└─ 结果:写入标记供下步使用
如果不用Lua:
1秒内1万个请求
↓ (每个请求都读Redis)
1万次网络往返
↓ (每次1ms延迟)
总耗时 10秒! ✗
用了Lua:
1秒内1万个请求
↓ (用EVALSHA执行脚本)
执行原子逻辑(在Redis内部)
计数准确,原子性保证
↓ (EVALSHA只需1ms)
1秒完成 ✓
这就是Lua被广泛应用在高并发系统中的根本原因!
最后:一句话的精髓
Lua 是高并发系统的"快速通道":
把多个操作合并成一个原子脚本,在 Redis 内部一次性执行,避免来回网络开销,保证数据一致性。
这就是 Lua 的全部精华!
企业级项目导航:⬅️ 05-JWT + Redis 双重会话管理 学习笔记 | 06-Lua 深度学习笔记 | ➡️ 07-敏感数据保护:展示脱敏与加密存储深度解析
💬 评论