1. 为什么需要Lua脚本与Redisson的深度整合在分布式系统中高并发场景下的数据一致性和性能优化一直是开发者面临的难题。想象一下电商秒杀场景成千上万的用户同时抢购同一件商品如果库存扣减操作不是原子的很可能出现超卖又或者API接口被恶意刷量如果没有可靠的限流机制系统可能瞬间崩溃。传统做法是用Java代码组合多个Redis命令但这存在三个致命问题网络开销大每个命令都要单独与Redis服务器通信非原子性风险多个命令执行期间可能被其他操作打断业务逻辑复杂错误处理、重试机制等代码会让业务层变得臃肿而Lua脚本在Redis中执行时具有天然的原子性——要么全部执行成功要么全部不执行。Redisson作为Redis的Java客户端通过RScript接口提供了Lua脚本的加载、执行和管理能力让开发者既能享受Lua的原子性优势又能保持Java开发的便捷性。我在实际项目中遇到过这样的案例某金融系统的账户余额变更操作最初采用先查询后更新的方式在并发量高时频繁出现余额错乱。改用Lua脚本后将查询、计算、更新三个操作合并为一个原子操作彻底解决了问题QPS还提升了3倍。2. 环境准备与基础配置2.1 引入Redisson依赖推荐使用Spring Boot Starter方式集成这是目前最简洁的配置方案dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.23.2/version /dependency如果是非Spring项目也可以直接引入核心库dependency groupIdorg.redisson/groupId artifactIdredisson/artifactId version3.23.2/version /dependency2.2 配置Redisson客户端单节点Redis的基础配置示例Bean public RedissonClient redissonClient() { Config config new Config(); config.useSingleServer() .setAddress(redis://127.0.0.1:6379) .setPassword(yourpassword) .setDatabase(0); return Redisson.create(config); }生产环境建议配置连接池参数.setConnectionPoolSize(64) .setConnectionMinimumIdleSize(24) .setIdleConnectionTimeout(10000) .setConnectTimeout(5000) .setTimeout(3000)3. Redisson的Lua脚本管理机制3.1 脚本加载与SHA摘要Redisson通过scriptLoad方法预编译Lua脚本并生成SHA1摘要后续执行只需传递这个摘要大幅减少网络传输量。这就像给脚本办了个身份证以后凭身份证号就能快速调用。String luaScript return redis.call(get, KEYS[1]); String shaDigest redissonClient.getScript().scriptLoad(luaScript);实际项目中我习惯把Lua脚本放在resources/scripts目录下用工具类统一加载public String loadScript(String path) { String content new String(Files.readAllBytes(Paths.get(getClass() .getResource(path).toURI()))); return redissonClient.getScript().scriptLoad(content); }3.2 脚本执行模式详解Redisson提供三种执行模式对应不同的Redis节点选择策略模式适用场景执行节点READ_ONLY只读操作副本节点READ_WRITE读写操作默认主节点MASTER强制主节点执行主节点典型执行示例Long result redissonClient.getScript() .evalSha(RScript.Mode.READ_WRITE, shaDigest, ReturnType.INTEGER, Collections.singletonList(myKey), arg1, arg2);踩坑提醒在Redis集群环境下所有Key必须位于同一个slot否则会报错。可以通过hash tag确保这一点比如用{user}:order作为key前缀。4. 分布式锁的Lua实现方案4.1 可重入锁实现原理可重入锁的核心是解决同一线程重复获取锁的问题。我们通过Redis的Hash结构实现Key锁名称Field线程标识Value重入次数加锁Lua脚本示例local key KEYS[1] local threadId ARGV[1] local ttl tonumber(ARGV[2]) if (redis.call(exists, key) 0) then redis.call(hset, key, threadId, 1) redis.call(expire, key, ttl) return 1 end if (redis.call(hexists, key, threadId) 1) then redis.call(hincrby, key, threadId, 1) redis.call(expire, key, ttl) return 1 end return 0对应的Java调用方法public boolean tryLock(String lockKey, String threadId, long ttlSec) { Long result redissonClient.getScript().evalSha( Mode.READ_WRITE, lockScriptSha, ReturnType.INTEGER, Collections.singletonList(lockKey), threadId, String.valueOf(ttlSec)); return result ! null result 1; }4.2 锁续期与死锁预防为了防止业务执行时间超过锁有效期需要实现锁续期机制。我推荐的做法是获取锁成功后启动守护线程每隔ttl/3时间执行一次续期业务完成时取消续期private void scheduleExpirationRenewal(String lockKey, String threadId, long ttl) { ScheduledExecutorService executor Executors.newSingleThreadScheduledExecutor(); executor.scheduleAtFixedRate(() - { if (isLocked(lockKey, threadId)) { redissonClient.getBucket(lockKey).expire(ttl, TimeUnit.SECONDS); } }, ttl / 3, ttl / 3, TimeUnit.SECONDS); }5. 限流算法的Lua实现5.1 令牌桶算法实战令牌桶算法的核心参数桶容量最大突发请求量填充速率每秒新增的令牌数Lua实现要点local key KEYS[1] local capacity tonumber(ARGV[1]) local rate tonumber(ARGV[2]) local now tonumber(ARGV[3]) local tokens tonumber(redis.call(get, key..:tokens) or capacity) local lastTime tonumber(redis.call(get, key..:time) or now) local delta math.max(0, now - lastTime) local newTokens math.min(capacity, tokens delta * rate) if newTokens 1 then redis.call(set, key..:tokens, newTokens - 1) redis.call(set, key..:time, now) return 1 else return 0 endJava调用示例public boolean tryAcquire(String key, int capacity, int rate) { Long result redissonClient.getScript().evalSha( Mode.READ_WRITE, tokenBucketSha, ReturnType.INTEGER, Collections.singletonList(key), String.valueOf(capacity), String.valueOf(rate), String.valueOf(System.currentTimeMillis() / 1000)); return result ! null result 1; }5.2 漏桶算法对比实现漏桶算法与令牌桶的区别特性令牌桶漏桶突发流量允许平滑实现复杂度较高较低适用场景突发流量控制恒定速率控制漏桶Lua实现核心逻辑local key KEYS[1] local capacity tonumber(ARGV[1]) local rate tonumber(ARGV[2]) local now tonumber(ARGV[3]) local water tonumber(redis.call(get, key..:water) or 0) local lastTime tonumber(redis.call(get, key..:time) or now) local leaked math.max(0, (now - lastTime) * rate) water math.max(0, water - leaked) if water capacity then redis.call(set, key..:water, water 1) redis.call(set, key..:time, now) return 1 else return 0 end6. 原子操作进阶技巧6.1 库存扣减的原子性实现电商场景下的库存扣减需要特别注意检查库存是否充足扣减库存记录操作日志这三个操作必须原子化local productKey KEYS[1] local logKey KEYS[2] local userId ARGV[1] local amount tonumber(ARGV[2]) local stock tonumber(redis.call(get, productKey)) if stock amount then return 0 end redis.call(decrby, productKey, amount) redis.call(hset, logKey, userId, amount) return 16.2 多键操作的原子性当需要同时操作多个Key时可以使用Redis的事务特性local account1 KEYS[1] local account2 KEYS[2] local amount tonumber(ARGV[1]) local balance1 tonumber(redis.call(get, account1)) if balance1 amount then return 0 end redis.call(decrby, account1, amount) redis.call(incrby, account2, amount) return 17. 性能优化与错误处理7.1 脚本缓存策略Redisson默认会自动缓存脚本但在集群环境下需要注意脚本需要预先在所有节点加载使用scriptLoad返回的SHA要缓存起来捕获NOSCRIPT错误并重新加载优化后的执行逻辑public Object executeScript(String scriptSha, String scriptContent, RScript.Mode mode, ListObject keys, Object... args) { try { return redissonClient.getScript() .evalSha(mode, scriptSha, ReturnType.VALUE, keys, args); } catch (RedisException e) { if (e.getMessage().contains(NOSCRIPT)) { String newSha redissonClient.getScript().scriptLoad(scriptContent); scriptCache.put(scriptContent, newSha); return redissonClient.getScript() .evalSha(mode, newSha, ReturnType.VALUE, keys, args); } throw e; } }7.2 常见错误排查脚本超时复杂脚本执行时间超过lua-time-limit默认5秒解决方案优化脚本逻辑或调大超时时间内存不足脚本使用过多内存解决方案减少数据处理量分批执行语法错误Lua语法问题调试技巧先用redis-cli测试脚本redis-cli --eval script.lua key1 key2 , arg1 arg2我在实际项目中总结了一套调试方法先在Redis命令行测试脚本确认无误后再集成到Java代码中可以节省大量调试时间。