Lua核心认知

一、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,脚本执行高效)

经典场景(缓存 / 分布式核心)

  1. 缓存一致性:解决「更新数据库后删除缓存」的原子性(避免中间被其他请求打断,导致脏数据);
  2. 分布式锁:实现更安全的锁抢占 / 释放逻辑(比如带过期时间的锁,避免死锁);
  3. 接口限流:基于 Redis 实现令牌桶 / 计数器限流(原子性统计请求数);
  4. 缓存预热 / 批量操作:一次性执行多条缓存读写命令(如批量删除前缀匹配的 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);

五、注意事项(生产避坑)

  1. 避免脚本阻塞 Redis :Redis 是单线程执行 Lua 脚本,脚本执行时间不能超过 redis.conf 中的 lua-time-limit(默认 5 秒),否则 Redis 会终止脚本并报错。→ 优化:脚本中只做「短平快」的操作(如查询 + 删除 + 判断),不做循环、IO 等耗时操作。

  2. 脚本幂等性 :Lua 脚本执行失败后可能重试,需保证脚本「重复执行不会产生副作用」(如用 SETNX 而非 SET,用 DEL 前先校验)。

  3. 参数传递规范

    • KEYS 用于传递 Redis 键(如缓存 key、锁 key),Redis 会对 KEYS 做集群路由优化;
    • ARGV 用于传递普通参数(如阈值、过期时间),避免将键放在 ARGV 中。
  4. 调试方法

    • 本地用 redis-cli 调试脚本:redis-cli --eval lua/deleteCache.lua user:1 , (注意 , 前后空格,分隔 KEYS 和 ARGV);
    • 生产用 Redis 日志(开启 lua-debug-log-level)查看脚本执行情况。

六、和之前缓存架构的结合(Caffeine+Redis+Lua)

咱们之前聊的「本地缓存(Caffeine)+ 分布式缓存(Redis)」架构,Lua 是「Redis 层的原子性保障」:

  1. 本地缓存(Caffeine):负责应用内热点数据提速(<1ms 响应);
  2. 分布式缓存(Redis):负责跨应用数据共享;
  3. Lua 脚本:负责 Redis 层的原子操作(如缓存更新、分布式锁、限流),解决并发问题,保证缓存一致性。

比如「更新用户数据」的完整流程:

plaintext

复制代码
Java 层:更新数据库 → 调用 Lua 脚本删除 Redis 缓存 → 清除当前应用的 Caffeine 缓存
  • Lua 保证「Redis 缓存删除」的原子性;
  • Caffeine 本地缓存随应用进程,清除后重新从 Redis 加载最新数据,避免脏数据。

总结

对 Java 后端(Spring Boot)来说,Lua 不是一门需要深入学习的语言,而是「Redis 原子操作的工具」:

  1. 核心场景:Redis 缓存一致性、分布式锁、接口限流(解决并发问题);
  2. 集成方式:Spring Boot + StringRedisTemplate,支持硬编码脚本或外部文件加载;
  3. 优势:原子性、低网络开销、简化复杂逻辑,配合 Caffeine+Redis 架构,提升系统稳定性和性能。
相关推荐
hazhanglvfang1 小时前
使用curl测试java后端post接口
java·开发语言
杀死那个蝈坦1 小时前
Redis 缓存预热
java·开发语言·青少年编程·kotlin·lua
秦jh_1 小时前
【Qt】Qt 概述
开发语言·qt
稚辉君.MCA_P8_Java1 小时前
在Java中,将`Short`(包装类)或`short`(基本类型)转换为`int`
java·开发语言
木易 士心1 小时前
Node.js 性能诊断利器 Clinic.js:原理剖析与实战指南
开发语言·javascript·node.js
一只乔哇噻1 小时前
java后端工程师+AI大模型进修ing(研一版‖day59)
java·开发语言·算法·语言模型
报错小能手1 小时前
C++流类库 概述及流的格式化输入/输出控制
开发语言·c++
2301_789015621 小时前
C++:list(带头双向链表)增删查改模拟实现
c语言·开发语言·c++·list
扣脚大汉在网络1 小时前
关于一句话木马
开发语言·网络安全