Redis Lua脚本深度解析:原理、实践与大厂应用

一、Redis Lua脚本核心原理

Redis Lua脚本功能允许开发者将多个Redis命令组合成一个原子操作在服务端执行。其核心特点包括:

  • 原子性:整个脚本作为一个命令执行,执行期间不会插入其他命令
  • 高性能:减少网络往返时延,特别适合复杂多命令操作
  • 灵活性:可使用Lua语言的控制结构和Redis命令组合
发送Lua脚本
客户端
Redis服务器
脚本是否缓存
使用SHA1执行缓存脚本
加载脚本到引擎
执行脚本
返回执行结果

二、Lua脚本在分布式锁中的应用

1. 分布式锁实现时序

客户端 Redis集群 EVAL "if redis.call('setnx',KEYS[1],ARGV[1])==1 then return redis.call('pexpire',KEYS[1],ARGV[2]) else return 0 end" 1 lock_key client_id 30000 返回1 返回0 alt [获取成功] [获取失败] 执行业务逻辑 loop [业务处理] EVAL "if redis.call('get',KEYS[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end" 1 lock_key client_id 客户端 Redis集群

2. 电商秒杀项目实战

在某电商平台秒杀系统中,我们使用Lua脚本实现库存扣减的原子操作:

// 库存扣减Lua脚本
String STOCK_DEDUCTION_SCRIPT =
    "local stock = tonumber(redis.call('get', KEYS[1])) " +
    "if stock >= tonumber(ARGV[1]) then " +
    "    redis.call('decrby', KEYS[1], ARGV[1]) " +
    "    redis.call('sadd', KEYS[2], ARGV[2]) " +
    "    return 1 " +
    "else " +
    "    return 0 " +
    "end";

// 预加载脚本
String scriptSha = redisTemplate.scriptLoad(STOCK_DEDUCTION_SCRIPT);

// 执行脚本
public boolean deductStock(String productId, int quantity, String userId) {
    List<String> keys = Arrays.asList("stock:" + productId, "order:" + productId);
    Object result = redisTemplate.execute(
        new DefaultRedisScript<>(STOCK_DEDUCTION_SCRIPT, Long.class),
        keys, String.valueOf(quantity), userId);
    return ((Long)result) == 1L;
}

性能对比

  • 普通多命令操作:平均耗时15ms(网络往返2次)
  • Lua脚本操作:平均耗时3ms(网络往返1次)
  • QPS提升:从2000提升到8000+

三、大厂面试深度追问与解决方案

追问1:如何保证Lua脚本的原子性?Redis崩溃时脚本会怎样?

问题分析
Redis单线程模型保证了脚本执行的原子性,但需要考虑崩溃恢复场景。

解决方案

  1. 原子性保障机制
  • Redis使用单线程处理命令,脚本执行期间不会处理其他请求
  • 脚本中的所有命令要么全部执行,要么全部不执行
  1. 崩溃恢复方案
应用 Redis 数据库 1. 开始执行Lua脚本 2. 执行部分命令 发生崩溃 3. 连接中断 4. 查询事务状态 返回未完成状态 5. 重试脚本(SHA1) 返回已完成 alt [脚本未完成] [已完成] 应用 Redis 数据库
  1. 生产环境最佳实践
// 带幂等控制的脚本执行
public Object executeScriptSafely(RedisScript<?> script, List<String> keys, Object... args) {
    int maxRetries = 3;
    for (int i = 0; i < maxRetries; i++) {
        try {
            return redisTemplate.execute(script, keys, args);
        } catch (RedisConnectionFailureException e) {
            // 检查数据库确认状态
            boolean completed = transactionDao.checkStatus(keys.get(0));
            if (completed) {
                return null; // 已成功无需重试
            }
            if (i == maxRetries - 1) {
                throw e;
            }
            Thread.sleep(100 * (i + 1));
        }
    }
    return null;
}
  1. 监控与告警
# 监控脚本执行情况
redis-cli info commandstats | grep eval
redis-cli info persistence | grep rdb_last_bgsave_status

追问2:如何处理Lua脚本中的大Key问题?如何优化执行性能?

问题场景
当脚本操作大Value(如10MB的Hash)时,会导致执行阻塞,影响Redis整体性能。

解决方案

  1. 大Key拆分方案
-- 将大Hash拆分为多个子Hash
local function processBigHash(keyPrefix, fields)
    local results = {}
    for i, field in ipairs(fields) do
        local subKey = keyPrefix .. (i % 10) -- 拆分为10个子Key
        table.insert(results, redis.call('HGET', subKey, field))
    end
    return results
end
  1. 性能优化技巧
// 使用SCRIPT KILL终止长时间运行的脚本
public void monitorScriptTimeout(Duration timeout) {
    ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
    scheduler.schedule(() -> {
        Long currentScript = redisTemplate.execute(
            "return redis.call('SCRIPT', 'KILL')", Collections.emptyList());
        if (currentScript != null) {
            log.warn("Killed long running script");
        }
    }, timeout.toMillis(), TimeUnit.MILLISECONDS);
}

// 分批次处理
String BATCH_PROCESS_SCRIPT =
    "local cursor = tonumber(ARGV[1]) " +
    "local limit = tonumber(ARGV[2]) " +
    "local result = {} " +
    "local nextCursor = redis.call('SCAN', cursor, 'COUNT', limit) " +
    "for i, key in ipairs(nextCursor[2]) do " +
    "    table.insert(result, redis.call('GET', key)) " +
    "end " +
    "return {nextCursor[1], result}";
  1. 内存优化方案
-- 使用游标增量处理
local cursor = 0
local results = {}
repeat
    local reply = redis.call('SCAN', cursor, 'MATCH', ARGV[1], 'COUNT', 1000)
    cursor = tonumber(reply[1])
    for _, key in ipairs(reply[2]) do
        -- 处理每个key
        table.insert(results, redis.call('GET', key))
    end
until cursor == 0
return results
  1. 生产环境配置
# 限制脚本执行时间(单位:毫秒)
config set lua-time-limit 5000

# 监控脚本执行
redis-cli info commandstats | grep -E "eval|script"

四、高级应用场景

1. 分布式限流器实现

-- 令牌桶限流算法
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)

local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
    last_tokens = capacity
end

local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
    last_refreshed = 0
end

local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
    new_tokens = filled_tokens - requested
end

redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)

return { allowed, new_tokens }

2. 热点数据发现与处理

// 热点Key探测脚本
String HOT_KEY_DETECT_SCRIPT =
    "local counts = {} " +
    "local keys = redis.call('SCAN', 0, 'MATCH', ARGV[1], 'COUNT', 1000)[2] " +
    "for _, key in ipairs(keys) do " +
    "    local count = redis.call('PFCOUNT', 'access:'..key) " +
    "    if count > tonumber(ARGV[2]) then " +
    "        table.insert(counts, {key, count}) " +
    "    end " +
    "end " +
    "return counts";

public List<Pair<String, Long>> detectHotKeys(String pattern, long threshold) {
    return (List<Pair<String, Long>>) redisTemplate.execute(
        new DefaultRedisScript<>(HOT_KEY_DETECT_SCRIPT, List.class),
        Collections.emptyList(), pattern, String.valueOf(threshold));
}

五、总结与最佳实践

Redis Lua脚本在大厂高并发场景中的核心价值:

  1. 原子操作:解决分布式环境下的竞态条件
  2. 性能优化:减少网络往返,提升吞吐量
  3. 复杂逻辑封装:实现高级数据结构和算法

生产环境使用建议:

  • 脚本应保持简单,执行时间控制在50ms内
  • 使用SCRIPT LOAD预加载脚本,通过SHA1执行
  • 避免在脚本中进行大量计算,聚焦数据操作
  • 实现完善的超时控制和重试机制
  • 监控脚本执行时间和内存使用情况

掌握Lua脚本高级用法,能够帮助Java工程师设计出更高效、更可靠的分布式系统架构,这也是阿里/字节等大厂面试中的重点考察内容。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐