一、Lua 核心认知(后端视角)
1. 是什么?
- 定位:轻量级嵌入式脚本语言(C 语言编写),语法简单、执行高效,可嵌入 Redis、Nginx、MySQL 等系统中执行。
- 核心特点 :
- 语法简洁(类似 Python,上手成本低);
- 执行速度快(比 Java 调用 Redis 多条命令快,减少网络开销);
- 原子性(Redis 执行 Lua 脚本时,会阻塞其他命令,保证脚本内所有操作的原子性);
- 可扩展性强(支持调用 Redis 原生命令,也能自定义逻辑)。
- 后端常用场景 :
- Redis 原子操作(如缓存更新、分布式锁、限流);
- Nginx 网关脚本(如接口鉴权、流量转发);
- 数据库存储过程扩展(较少用,优先 Redis 场景)。
2. 基础语法(够用就行,不用深钻)
Lua 语法简单,核心掌握以下几点,就能写 Redis 脚本:
lua
-- 1. 变量定义(无需声明类型,默认全局变量,局部变量加 local)
local key = "user:1" -- 局部变量(推荐)
value = "张三" -- 全局变量(不推荐,避免污染)
-- 2. 函数定义
local function getAndSet(key, newValue)
local oldValue = redis.call("GET", key) -- 调用 Redis GET 命令
redis.call("SET", key, newValue) -- 调用 Redis SET 命令
return oldValue -- 返回旧值
end
-- 3. 流程控制(if-else、循环)
local count = redis.call("INCR", "counter")
if count > 10 then
return "限流触发"
else
return "正常访问"
end
-- 4. 接收外部参数(Redis 调用 Lua 时传递,ARGV[1] 是第一个参数)
local userId = ARGV[1]
local userName = ARGV[2]
二、Redis+Lua 核心价值(后端必用)
咱们之前聊过 Redis 分布式缓存的问题(比如缓存更新时「先更数据库再删缓存」的原子性、分布式锁并发竞争),而 Lua 正是解决这些问题的关键 ------Redis 单条命令是原子的,但多条命令组合不是,Lua 能让多条命令「打包成一个原子操作」。
核心优势(对比 Java 代码调用 Redis)
| 场景 | Java 调用 Redis(多条命令) | Redis+Lua 脚本 |
|---|---|---|
| 原子性 | 不保证(需加分布式锁,复杂) | 天然保证(Redis 单线程执行脚本) |
| 网络开销 | 多次网络往返(如 GET+SET 两次请求) | 一次网络往返(脚本内包含所有命令) |
| 代码复杂度 | 高(需处理并发、重试、异常) | 低(脚本内集中逻辑,Java 只需调用) |
| 性能 | 一般(网络开销大) | 高(减少网络 IO,脚本执行高效) |
经典场景(缓存 / 分布式核心)
- 缓存一致性:解决「更新数据库后删除缓存」的原子性(避免中间被其他请求打断,导致脏数据);
- 分布式锁:实现更安全的锁抢占 / 释放逻辑(比如带过期时间的锁,避免死锁);
- 接口限流:基于 Redis 实现令牌桶 / 计数器限流(原子性统计请求数);
- 缓存预热 / 批量操作:一次性执行多条缓存读写命令(如批量删除前缀匹配的 key)。
三、Spring Boot 集成 Redis+Lua(实战核心)
Spring Boot 中通过 StringRedisTemplate 调用 Lua 脚本,支持「直接写脚本字符串」或「加载外部 .lua 文件」,以下是生产级实战:
1. 前置依赖(已集成 Redis 可跳过)
xml
<!-- Spring Boot Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 连接池(可选,提升性能) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2. Redis 配置(application.yml)
yaml
spring:
redis:
host: localhost # Redis 地址
port: 6379
password: 123456 # 你的 Redis 密码(无则省略)
lettuce:
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接
min-idle: 2 # 最小空闲连接
3. 实战案例(3 个后端高频场景)
案例 1:缓存一致性(原子更新数据库 + 删除缓存)
之前聊过「先更数据库,再删缓存」的问题:如果两步之间服务宕机,会导致缓存残留脏数据。用 Lua 脚本将「查缓存→更数据库→删缓存」打包成原子操作(实际中数据库操作在 Java 层,Lua 负责缓存原子操作)。
Lua 脚本(缓存删除 + 校验)
lua
-- 脚本功能:删除指定缓存 key,并返回删除结果
-- 参数:KEYS[1] = 缓存 key
if redis.call("EXISTS", KEYS[1]) == 1 then -- 先判断缓存是否存在
redis.call("DEL", KEYS[1]) -- 存在则删除
return "缓存删除成功"
else
return "缓存不存在"
end
Spring Boot 调用代码
java
运行
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
@Service
public class UserService {
@Resource
private StringRedisTemplate redisTemplate;
@Resource
private UserMapper userMapper;
// 更新用户:Java 更数据库 + Lua 删缓存(原子操作)
public void updateUser(User user) {
// 步骤1:更新数据库(Java 层执行)
userMapper.updateById(user);
// 步骤2:调用 Lua 脚本删除缓存(原子操作)
String luaScript = "if redis.call('EXISTS', KEYS[1]) == 1 then redis.call('DEL', KEYS[1]) return '缓存删除成功' else return '缓存不存在' end";
String cacheKey = "user:" + user.getId();
// 执行 Lua 脚本:KEYS 是键列表,ARGV 是参数列表(此处无参数)
String result = redisTemplate.execute(
redisScript, // Lua 脚本
Collections.singletonList(cacheKey), // KEYS[1] = cacheKey
Collections.emptyList() // ARGV 无参数
);
System.out.println("Lua 执行结果:" + result);
}
}
案例 2:分布式锁(Redis+Lua 实现安全锁)
分布式锁的核心需求:「抢占锁→执行业务→释放锁」原子性,避免锁被误删(比如线程 A 持有锁,线程 B 释放)。用 Lua 脚本保证「释放锁时校验持有者」的原子性。
Lua 脚本(抢占锁 + 释放锁)
lua
-- 1. 抢占锁脚本:KEYS[1] = 锁 key,ARGV[1] = 锁持有者 ID(如 UUID),ARGV[2] = 过期时间(秒)
if redis.call("SETNX", KEYS[1], ARGV[1]) == 1 then -- SETNX 成功(锁未被占用)
redis.call("EXPIRE", KEYS[1], ARGV[2]) -- 设置过期时间,避免死锁
return "锁抢占成功"
else
return "锁已被占用"
end
-- 2. 释放锁脚本:KEYS[1] = 锁 key,ARGV[1] = 锁持有者 ID
if redis.call("GET", KEYS[1]) == ARGV[1] then -- 校验持有者(避免误删)
redis.call("DEL", KEYS[1]) -- 释放锁
return "锁释放成功"
else
return "无权限释放锁"
end
Spring Boot 调用代码
java
运行
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Component
public class RedisDistributedLock {
@Resource
private StringRedisTemplate redisTemplate;
private static final String LOCK_KEY_PREFIX = "lock:";
private ThreadLocal<String> lockHolder = new ThreadLocal<>(); // 存储当前线程的锁持有者 ID
// 抢占锁
public boolean tryLock(String businessKey, long expireSeconds) {
String lockKey = LOCK_KEY_PREFIX + businessKey;
String holderId = UUID.randomUUID().toString(); // 唯一持有者 ID
lockHolder.set(holderId); // 存入线程本地
// 执行抢占锁 Lua 脚本
String lockScript = "if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]) return 1 else return 0 end";
Long result = redisTemplate.execute(
lockScript,
Collections.singletonList(lockKey),
Collections.singletonList(holderId),
String.valueOf(expireSeconds)
);
return result != null && result == 1; // 1 表示抢占成功
}
// 释放锁
public boolean releaseLock(String businessKey) {
String lockKey = LOCK_KEY_PREFIX + businessKey;
String holderId = lockHolder.get();
if (holderId == null) {
return false;
}
// 执行释放锁 Lua 脚本
String releaseScript = "if redis.call('GET', KEYS[1]) == ARGV[1] then redis.call('DEL', KEYS[1]) return 1 else return 0 end";
Long result = redisTemplate.execute(
releaseScript,
Collections.singletonList(lockKey),
Collections.singletonList(holderId)
);
lockHolder.remove(); // 清除线程本地变量
return result != null && result == 1;
}
}
案例 3:接口限流(Redis+Lua 计数器限流)
需求:限制某接口每秒最多 10 次请求(基于 Redis 计数器,Lua 保证「计数 + 判断」原子性)。
Lua 脚本(计数器限流)
lua
-- KEYS[1] = 限流 key(如接口路径),ARGV[1] = 限流阈值,ARGV[2] = 限流时间窗口(秒)
local count = redis.call("INCR", KEYS[1]) -- 计数器+1
if count == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[2]) -- 第一次请求,设置过期时间
end
if count > tonumber(ARGV[1]) then
return 0 -- 超过阈值,限流
else
return 1 -- 正常访问
end
Spring Boot 调用代码(拦截器实现)
java
运行
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Collections;
// 限流拦截器
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
@Resource
private StringRedisTemplate redisTemplate;
// 限流配置:接口路径→(阈值, 时间窗口秒)
private static final String LIMIT_SCRIPT = "local count = redis.call('INCR', KEYS[1]) if count == 1 then redis.call('EXPIRE', KEYS[1], ARGV[2]) end if count > tonumber(ARGV[1]) then return 0 else return 1 end";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestUri = request.getRequestURI(); // 限流 key(如 /users/query)
int limit = 10; // 每秒最多 10 次
int window = 1; // 时间窗口 1 秒
// 执行限流 Lua 脚本
Long result = redisTemplate.execute(
LIMIT_SCRIPT,
Collections.singletonList(requestUri),
String.valueOf(limit),
String.valueOf(window)
);
if (result != null && result == 1) {
return true; // 正常访问
} else {
response.setStatus(429); // 429 Too Many Requests
response.getWriter().write("请求过于频繁,请稍后重试");
return false;
}
}
}
四、高级用法:加载外部 Lua 文件(生产推荐)
上面的脚本是写在 Java 代码中的「硬编码」,不利于维护。生产中建议将 Lua 脚本放在 resources/lua 目录下,通过文件加载:
1. 目录结构
plaintext
src/main/resources/
└── lua/
├── deleteCache.lua
├── distributedLock.lua
└── rateLimit.lua
2. 外部脚本示例(deleteCache.lua)
lua
-- resources/lua/deleteCache.lua
if redis.call("EXISTS", KEYS[1]) == 1 then
redis.call("DEL", KEYS[1])
return "success"
else
return "not_exists"
end
3. Spring Boot 加载脚本工具类
java
运行
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.StaticScriptSource;
import javax.annotation.Resource;
import java.util.List;
@Component
public class LuaScriptExecutor {
@Resource
private StringRedisTemplate redisTemplate;
// 执行外部 Lua 脚本
public <T> T executeScript(String scriptPath, List<String> keys, List<String> args, Class<T> returnType) {
DefaultRedisScript<T> redisScript = new DefaultRedisScript<>();
// 加载外部脚本文件
ClassPathResource resource = new ClassPathResource("lua/" + scriptPath);
redisScript.setScriptSource(new StaticScriptSource(resource));
redisScript.setResultType(returnType); // 指定返回类型(如 String、Long)
// 执行脚本
return redisTemplate.execute(redisScript, keys, args);
}
}
4. 调用外部脚本
java
运行
// 调用 deleteCache.lua
String result = luaScriptExecutor.executeScript(
"deleteCache.lua", // 脚本路径
Collections.singletonList("user:1"), // KEYS
Collections.emptyList(), // ARGV
String.class // 返回类型
);
System.out.println("脚本执行结果:" + result);
五、注意事项(生产避坑)
-
避免脚本阻塞 Redis :Redis 是单线程执行 Lua 脚本,脚本执行时间不能超过
redis.conf中的lua-time-limit(默认 5 秒),否则 Redis 会终止脚本并报错。→ 优化:脚本中只做「短平快」的操作(如查询 + 删除 + 判断),不做循环、IO 等耗时操作。 -
脚本幂等性 :Lua 脚本执行失败后可能重试,需保证脚本「重复执行不会产生副作用」(如用
SETNX而非SET,用DEL前先校验)。 -
参数传递规范:
KEYS用于传递 Redis 键(如缓存 key、锁 key),Redis 会对KEYS做集群路由优化;ARGV用于传递普通参数(如阈值、过期时间),避免将键放在ARGV中。
-
调试方法:
- 本地用
redis-cli调试脚本:redis-cli --eval lua/deleteCache.lua user:1 ,(注意,前后空格,分隔 KEYS 和 ARGV); - 生产用 Redis 日志(开启
lua-debug-log-level)查看脚本执行情况。
- 本地用
六、和之前缓存架构的结合(Caffeine+Redis+Lua)
咱们之前聊的「本地缓存(Caffeine)+ 分布式缓存(Redis)」架构,Lua 是「Redis 层的原子性保障」:
- 本地缓存(Caffeine):负责应用内热点数据提速(<1ms 响应);
- 分布式缓存(Redis):负责跨应用数据共享;
- Lua 脚本:负责 Redis 层的原子操作(如缓存更新、分布式锁、限流),解决并发问题,保证缓存一致性。
比如「更新用户数据」的完整流程:
plaintext
Java 层:更新数据库 → 调用 Lua 脚本删除 Redis 缓存 → 清除当前应用的 Caffeine 缓存
- Lua 保证「Redis 缓存删除」的原子性;
- Caffeine 本地缓存随应用进程,清除后重新从 Redis 加载最新数据,避免脏数据。
总结
对 Java 后端(Spring Boot)来说,Lua 不是一门需要深入学习的语言,而是「Redis 原子操作的工具」:
- 核心场景:Redis 缓存一致性、分布式锁、接口限流(解决并发问题);
- 集成方式:Spring Boot + StringRedisTemplate,支持硬编码脚本或外部文件加载;
- 优势:原子性、低网络开销、简化复杂逻辑,配合 Caffeine+Redis 架构,提升系统稳定性和性能。