扣减库存时,先查再减,两个命令之间被其他线程插队------结果库存变成负数。这就是没有原子操作的后果。本文将揭秘Redis Lua脚本如何在高并发场景下实现原子化操作,彻底解决竞态条件问题,让你的代码既安全又高效。

文章目录
-
- 一、场景引入:一次库存超卖事故
-
- [1.1 真实案例](#1.1 真实案例)
- [1.2 传统方案的缺陷](#1.2 传统方案的缺陷)
- [二、解决方案:Redis Lua脚本](#二、解决方案:Redis Lua脚本)
-
- [2.1 为什么Lua脚本能解决原子性问题?](#2.1 为什么Lua脚本能解决原子性问题?)
- [2.2 Lua脚本的核心优势](#2.2 Lua脚本的核心优势)
- 三、实战代码:六大经典场景
-
- [3.1 场景一:库存扣减(最经典)](#3.1 场景一:库存扣减(最经典))
- [3.2 场景二:分布式锁(防死锁)](#3.2 场景二:分布式锁(防死锁))
- [3.3 场景三:滑动窗口限流](#3.3 场景三:滑动窗口限流)
- [3.4 场景四:延迟队列(ZSet实现)](#3.4 场景四:延迟队列(ZSet实现))
- [3.5 场景五:排行榜实时更新](#3.5 场景五:排行榜实时更新)
- [3.6 场景六:幂等性校验](#3.6 场景六:幂等性校验)
- 四、高级进阶:Lua脚本最佳实践
-
- [4.1 脚本预加载(EVALSHA)](#4.1 脚本预加载(EVALSHA))
- [4.2 Lua脚本调试技巧](#4.2 Lua脚本调试技巧)
- 五、预判问题与解答
- 六、面试高频考点
- 七、总结与最佳实践
-
- [7.1 核心要点回顾](#7.1 核心要点回顾)
- [7.2 性能提升数据](#7.2 性能提升数据)
- 八、参考与拓展
一、场景引入:一次库存超卖事故
1.1 真实案例
某电商平台限时抢购活动:
事故现场:
库存剩余:1件
并发请求:
线程A:查询库存 → stock = 1 → stock > 0 → 准备扣减
线程B:查询库存 → stock = 1 → stock > 0 → 准备扣减
线程A:执行扣减 → stock = 0
线程B:执行扣减 → stock = -1 ← 超卖!
后果:
- 1000件商品,实际卖出了1200件
- 每件亏500元,直接损失10万
- 用户投诉:"我付了钱,但没收到货"
- 客服忙不过来,退款排队
问题根源:多个Redis命令之间不是原子的,存在竞态条件(Race Condition)。
1.2 传统方案的缺陷
java
// ❌ 错误方案:先查后减(非原子操作)
public boolean deductStock(String key) {
int stock = Integer.parseInt(redisTemplate.opsForValue().get(key));
if (stock > 0) {
redisTemplate.opsForValue().set(key, String.valueOf(stock - 1));
return true;
}
return false;
}
// 并发时,多个线程同时读到stock=1,都判断>0,都执行扣减
// ❌ 错误方案:DECR(不能判断是否足够)
public boolean deductStock(String key) {
Long stock = redisTemplate.opsForValue().decrement(key);
return stock >= 0; // 已经减成-1了,判断也没用
}
// ✅ 正确方案:Lua脚本(原子操作)
// 判断 + 扣减 在一个脚本中完成,Redis单线程保证原子性
二、解决方案:Redis Lua脚本
2.1 为什么Lua脚本能解决原子性问题?
Redis执行模型:
┌─────────────────────────────────────────────────────────────┐
│ │
│ Redis是单线程执行命令的: │
│ │
│ 普通方式(多次调用): │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ GET stock│ │ 判断>0 │ │ SET stock│ ← 三个命令之间 │
│ └─────────┘ └─────────┘ └─────────┘ 可能被其他命令 │
│ 插队! │
│ Lua脚本方式(一次调用): │
│ ┌─────────────────────────────────────────┐ │
│ │ if GET(stock) > 0 then │ │
│ │ SET stock = stock - 1 │ │
│ │ return 1 │ ← 一个原子操作 │
│ │ else │ 不可被打断 │
│ │ return 0 │ │
│ │ end │ │
│ └─────────────────────────────────────────┘ │
│ │
│ 核心优势: │
│ 1. 原子性:整个脚本在Redis中一次性执行,不会被打断 │
│ 2. 减少网络往返:多个操作合并为一次请求 │
│ 3. 复用性:脚本可以预加载,后续只传参数 │
│ │
└─────────────────────────────────────────────────────────────┘
2.2 Lua脚本的核心优势
| 优势 | 说明 | 性能提升 |
|---|---|---|
| 原子性 | 整个脚本在Redis单线程中执行,不会被其他命令打断 | 彻底解决竞态条件 |
| 减少网络IO | 多个操作合并为一次请求 | 网络往返减少N倍 |
| 脚本复用 | EVALSHA使用SHA1缓存脚本 | 后续调用只传参数 |
| 复杂逻辑 | 支持条件判断、循环等复杂逻辑 | 替代多层应用代码 |
三、实战代码:六大经典场景
3.1 场景一:库存扣减(最经典)
lua
-- deduct_stock.lua
-- 原子操作:判断库存 + 扣减 + 记录
-- KEYS[1]: 库存key
-- ARGV[1]: 扣减数量
-- ARGV[2]: 用户ID(防重复购买)
-- ARGV[3]: 已购集合key
local stockKey = KEYS[1]
local deductQty = tonumber(ARGV[1])
local userId = ARGV[2]
local soldSetKey = ARGV[3]
-- 1. 检查是否已购买
local isBought = redis.call('SISMEMBER', soldSetKey, userId)
if tonumber(isBought) == 1 then
return -2 -- 已购买
end
-- 2. 获取库存
local stock = tonumber(redis.call('GET', stockKey))
if stock == nil then
return -3 -- 库存不存在
end
-- 3. 判断并扣减
if stock >= deductQty then
redis.call('DECRBY', stockKey, deductQty)
redis.call('SADD', soldSetKey, userId)
return stock - deductQty -- 返回剩余库存
else
return -1 -- 库存不足
end
java
/**
* 库存扣减服务
*/
@Service
@Slf4j
public class StockService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String DEDUCT_STOCK_SCRIPT =
"local stockKey = KEYS[1]\n" +
"local deductQty = tonumber(ARGV[1])\n" +
"local userId = ARGV[2]\n" +
"local soldSetKey = ARGV[3]\n" +
"local isBought = redis.call('SISMEMBER', soldSetKey, userId)\n" +
"if tonumber(isBought) == 1 then return -2 end\n" +
"local stock = tonumber(redis.call('GET', stockKey))\n" +
"if stock == nil then return -3 end\n" +
"if stock >= deductQty then\n" +
" redis.call('DECRBY', stockKey, deductQty)\n" +
" redis.call('SADD', soldSetKey, userId)\n" +
" return stock - deductQty\n" +
"else\n" +
" return -1\n" +
"end\n";
private final RedisScript<Long> deductStockScript =
new DefaultRedisScript<>(DEDUCT_STOCK_SCRIPT, Long.class);
/**
* 扣减库存(原子操作)
*/
public StockResult deductStock(Long activityId, Long userId, int quantity) {
String stockKey = "stock:" + activityId;
String soldSetKey = "sold:" + activityId;
Long result = redisTemplate.execute(
deductStockScript,
Arrays.asList(stockKey, soldSetKey),
String.valueOf(quantity),
String.valueOf(userId)
);
if (result == null) return StockResult.error("系统异常");
switch (result.intValue()) {
case -1: return StockResult.fail("库存不足");
case -2: return StockResult.fail("您已购买过");
case -3: return StockResult.fail("活动不存在");
default: return StockResult.success(result.intValue());
}
}
}
3.2 场景二:分布式锁(防死锁)
lua
-- distributed_lock.lua
-- KEYS[1]: 锁key
-- ARGV[1]: 锁值(唯一标识)
-- ARGV[2]: 过期时间(毫秒)
local lockKey = KEYS[1]
local lockValue = ARGV[1]
local expireTime = tonumber(ARGV[2])
-- 尝试加锁(NX:不存在才设置,PX:毫秒过期)
local result = redis.call('SET', lockKey, lockValue, 'NX', 'PX', expireTime)
if result then
return 1 -- 加锁成功
else
return 0 -- 加锁失败(锁已被占用)
end
lua
-- unlock.lua
-- KEYS[1]: 锁key
-- ARGV[1]: 锁值(必须匹配才能释放,防止释放别人的锁)
local lockKey = KEYS[1]
local lockValue = ARGV[1]
-- 比较锁值,匹配才删除(Lua保证原子性)
if redis.call('GET', lockKey) == lockValue then
return redis.call('DEL', lockKey)
else
return 0 -- 不是自己的锁,不能释放
end
java
/**
* Redis分布式锁(Lua脚本实现)
*/
@Component
@Slf4j
public class RedisDistributedLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_SCRIPT =
"local result = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'PX', ARGV[2])\n" +
"if result then return 1 else return 0 end\n";
private static final String UNLOCK_SCRIPT =
"if redis.call('GET', KEYS[1]) == ARGV[1] then\n" +
" return redis.call('DEL', KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end\n";
private final RedisScript<Long> lockScript =
new DefaultRedisScript<>(LOCK_SCRIPT, Long.class);
private final RedisScript<Long> unlockScript =
new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class);
/**
* 尝试加锁
*/
public boolean tryLock(String key, String value, long expireMs) {
Long result = redisTemplate.execute(
lockScript,
Collections.singletonList(key),
value,
String.valueOf(expireMs)
);
return Long.valueOf(1L).equals(result);
}
/**
* 释放锁(只有锁的持有者才能释放)
*/
public boolean unlock(String key, String value) {
Long result = redisTemplate.execute(
unlockScript,
Collections.singletonList(key),
value
);
return Long.valueOf(1L).equals(result);
}
}
3.3 场景三:滑动窗口限流
lua
-- sliding_window_rate_limit.lua
-- KEYS[1]: 限流key(ZSet)
-- ARGV[1]: 窗口大小(毫秒)
-- ARGV[2]: 窗口内最大请求数
-- ARGV[3]: 当前时间戳(毫秒)
-- ARGV[4]: 请求唯一ID
local key = KEYS[1]
local windowSize = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requestId = ARGV[4]
local windowStart = now - windowSize
-- 1. 清除窗口外的旧记录
redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)
-- 2. 获取当前窗口内的请求数
local current = redis.call('ZCARD', key)
-- 3. 判断是否超过限制
if tonumber(current) < limit then
-- 4. 添加当前请求
redis.call('ZADD', key, now, requestId)
-- 5. 设置过期时间(防止内存泄漏)
redis.call('PEXPIRE', key, windowSize)
return 1 -- 允许通过
else
return 0 -- 限流
end
java
/**
* 滑动窗口限流器
*/
@Component
public class SlidingWindowRateLimiter {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String RATE_LIMIT_SCRIPT =
"local key = KEYS[1]\n" +
"local windowSize = tonumber(ARGV[1])\n" +
"local limit = tonumber(ARGV[2])\n" +
"local now = tonumber(ARGV[3])\n" +
"local requestId = ARGV[4]\n" +
"local windowStart = now - windowSize\n" +
"redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)\n" +
"local current = redis.call('ZCARD', key)\n" +
"if tonumber(current) < limit then\n" +
" redis.call('ZADD', key, now, requestId)\n" +
" redis.call('PEXPIRE', key, windowSize)\n" +
" return 1\n" +
"else\n" +
" return 0\n" +
"end\n";
private final RedisScript<Long> rateLimitScript =
new DefaultRedisScript<>(RATE_LIMIT_SCRIPT, Long.class);
/**
* 尝试获取许可
*/
public boolean tryAcquire(String key, long windowMs, int limit) {
Long result = redisTemplate.execute(
rateLimitScript,
Collections.singletonList(key),
String.valueOf(windowMs),
String.valueOf(limit),
String.valueOf(System.currentTimeMillis()),
UUID.randomUUID().toString()
);
return Long.valueOf(1L).equals(result);
}
}
3.4 场景四:延迟队列(ZSet实现)
lua
-- delayed_queue.lua
-- KEYS[1]: 队列key(ZSet)
-- ARGV[1]: 当前时间戳
-- ARGV[2]: 数量限制
local key = KEYS[1]
local now = tonumber(ARGV[1])
local limit = tonumber(ARGV[2])
-- 获取到期的任务(score <= 当前时间)
local tasks = redis.call('ZRANGEBYSCORE', key, 0, now, 'LIMIT', 0, limit)
if #tasks > 0 then
-- 批量删除已取出的任务
redis.call('ZREM', key, unpack(tasks))
end
return tasks
java
/**
* Redis延迟队列
*/
@Component
@Slf4j
public class RedisDelayQueue {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String POLL_SCRIPT =
"local key = KEYS[1]\n" +
"local now = tonumber(ARGV[1])\n" +
"local limit = tonumber(ARGV[2])\n" +
"local tasks = redis.call('ZRANGEBYSCORE', key, 0, now, 'LIMIT', 0, limit)\n" +
"if #tasks > 0 then\n" +
" redis.call('ZREM', key, unpack(tasks))\n" +
"end\n" +
"return tasks\n";
private final RedisScript<List> pollScript =
new DefaultRedisScript<>(POLL_SCRIPT, List.class);
/**
* 添加延迟任务
*/
public void addTask(String queue, String taskId, long delayMs) {
long executeTime = System.currentTimeMillis() + delayMs;
redisTemplate.opsForZSet().add(queue, taskId, executeTime);
}
/**
* 拉取到期任务
*/
public List<String> pollTasks(String queue, int limit) {
List<String> tasks = redisTemplate.execute(
pollScript,
Collections.singletonList(queue),
String.valueOf(System.currentTimeMillis()),
String.valueOf(limit)
);
return tasks != null ? tasks : Collections.emptyList();
}
}
3.5 场景五:排行榜实时更新
lua
-- rank_update.lua
-- KEYS[1]: 排行榜key(ZSet)
-- ARGV[1]: 成员ID
-- ARGV[2]: 分数
local key = KEYS[1]
local memberId = ARGV[1]
local score = tonumber(ARGV[2])
-- 更新分数
redis.call('ZADD', key, score, memberId)
-- 获取当前排名
local rank = redis.call('ZREVRANK', key, memberId)
-- 获取Top 10
local top10 = redis.call('ZREVRANGE', key, 0, 9, 'WITHSCORES')
return {rank + 1, top10}
3.6 场景六:幂等性校验
lua
-- idempotent_check.lua
-- KEYS[1]: 幂等key
-- ARGV[1]: 请求ID
-- ARGV[2]: 过期时间(秒)
local key = KEYS[1]
local requestId = ARGV[1]
local expireTime = tonumber(ARGV[2])
-- 检查是否已处理
local exists = redis.call('SET', key, requestId, 'NX', 'EX', expireTime)
if exists then
return 1 -- 首次处理
else
-- 检查是否是同一个请求
local value = redis.call('GET', key)
if value == requestId then
return 0 -- 重复请求,但幂等
else
return -1 -- 冲突请求
end
end
四、高级进阶:Lua脚本最佳实践
4.1 脚本预加载(EVALSHA)
java
/**
* Lua脚本管理器
* 预加载脚本,后续使用SHA1调用,提升性能
*/
@Component
@Slf4j
public class LuaScriptManager {
@Autowired
private StringRedisTemplate redisTemplate;
// 脚本缓存:脚本名 → SHA1
private final ConcurrentHashMap<String, String> scriptShaCache = new ConcurrentHashMap<>();
/**
* 注册脚本(预加载到Redis)
*/
public void registerScript(String name, String script) {
String sha1 = redisTemplate.execute(
(RedisCallback<String>) connection ->
connection.scriptLoad(script.getBytes())
);
scriptShaCache.put(name, sha1);
log.info("✅ Lua脚本注册成功: name={}, sha1={}", name, sha1);
}
/**
* 执行已注册的脚本(使用EVALSHA,性能更高)
*/
public <T> T executeScript(String name, Class<T> returnType,
List<String> keys, String... args) {
String sha1 = scriptShaCache.get(name);
if (sha1 == null) {
throw new IllegalArgumentException("脚本未注册: " + name);
}
RedisScript<T> script = new DefaultRedisScript<>(sha1, returnType);
return redisTemplate.execute(script, keys, args);
}
@PostConstruct
public void init() {
// 预加载所有脚本
registerScript("deduct_stock", DEDUCT_STOCK_SCRIPT);
registerScript("lock", LOCK_SCRIPT);
registerScript("unlock", UNLOCK_SCRIPT);
registerScript("rate_limit", RATE_LIMIT_SCRIPT);
}
}
4.2 Lua脚本调试技巧
java
/**
* Lua脚本调试工具
*/
@Component
@Slf4j
public class LuaScriptDebugger {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 调试Lua脚本(在Redis中执行并返回详细结果)
*/
public Object debugScript(String script, List<String> keys, String... args) {
log.info("=== Lua脚本调试开始 ===");
log.info("脚本:\n{}", script);
log.info("KEYS: {}", keys);
log.info("ARGV: {}", Arrays.asList(args));
long start = System.currentTimeMillis();
try {
Object result = redisTemplate.execute(
new DefaultRedisScript<>(script, Object.class),
keys,
args
);
long cost = System.currentTimeMillis() - start;
log.info("结果: {}", result);
log.info("耗时: {}ms", cost);
log.info("=== Lua脚本调试结束 ===");
return result;
} catch (Exception e) {
log.error("❌ Lua脚本执行失败", e);
return null;
}
}
}
五、预判问题与解答
Q1:Lua脚本会不会阻塞Redis?
A:
Lua脚本执行是原子的,会阻塞Redis:
1. 执行时间短(< 5ms):
- 对Redis影响很小
- 绝大多数Lua脚本都在这个范围内
2. 执行时间长(> 5ms):
- 会阻塞其他命令
- 影响Redis吞吐量
3. 最佳实践:
- Lua脚本尽量简短
- 避免在Lua中执行循环(特别是大循环)
- 避免在Lua中调用KEYS命令(改用SCAN)
- 使用EVALSHA减少脚本传输时间
4. Redis 7.0+:
- 支持Lua脚本中断(SCRIPT KILL)
- 支持函数(Function)替代脚本
Q2:Lua脚本和Redis事务(MULTI/EXEC)有什么区别?
A:
| 特性 | Lua脚本 | MULTI/EXEC |
|---|---|---|
| 原子性 | ✅ 原子执行 | ✅ 原子执行 |
| 条件判断 | ✅ 支持 | ❌ 不支持 |
| 循环 | ✅ 支持 | ❌ 不支持 |
| 网络往返 | 1次 | N次 |
| 复用性 | EVALSHA缓存 | 每次都要传 |
| 错误处理 | 返回错误值 | 整个事务回滚 |
结论:需要条件判断或复杂逻辑时用Lua脚本,简单的原子操作可以用MULTI/EXEC。
Q3:Lua脚本怎么处理错误?
A:
lua
-- Lua脚本错误处理
local stockKey = KEYS[1]
-- 方式1:pcall捕获异常
local ok, stock = pcall(redis.call, 'GET', stockKey)
if not ok then
return {err = "GET失败: " .. tostring(stock)}
end
-- 方式2:返回错误码
if stock == nil then
return {code = -1, msg = "库存不存在"}
end
-- 方式3:使用redis.error_reply
if tonumber(stock) < 0 then
return redis.error_reply("库存不能为负数")
end
Q4:Lua脚本中可以使用哪些Redis命令?
A:
可用命令:
- 所有Redis命令都可以在Lua中使用
- 但有一些限制:
限制:
1. 不能使用SCRIPT命令(递归调用脚本)
2. 不能使用MULTI/EXEC(脚本本身就是原子的)
3. 不能使用DEBUG命令
4. 不能使用RANDOMKEY(因为脚本需要确定性)
推荐使用的命令:
- GET/SET/DEL(基础操作)
- ZADD/ZREM/ZRANGEBYSCORE(有序集合)
- SADD/SREM/SISMEMBER(集合)
- INCR/DECR/INCRBY(计数器)
- EXPIRE/PEXPIRE/TTL(过期时间)
- HSET/HGET/HGETALL(哈希)
Q5:Lua脚本在集群模式下有什么限制?
A:
集群模式限制:
1. 所有KEY必须在同一个槽位:
- 使用{hashtag}确保KEY在同一个节点
- 如:KEYS[1] = "order:{123}",KEYS[2] = "stock:{123}"
2. 不能跨槽操作:
- Lua脚本中的所有KEY必须在同一个节点
- 否则报错:CROSSSLOT Keys in request don't hash to the same slot
3. 最佳实践:
- 使用{hashtag}前缀
- 在KEY命名时统一加上业务前缀
- 如:seckill:{activityId}:stock 和 seckill:{activityId}:sold
六、面试高频考点
考点1:为什么Lua脚本能保证原子性?
参考答案:
原子性原理:
1. Redis单线程模型:
- Redis使用单线程处理命令
- 一次只能执行一个命令
- Lua脚本作为一个整体被执行
2. 脚本执行期间不会被中断:
- Redis在执行Lua脚本时
- 不会处理其他客户端的命令
- 直到脚本执行完成
3. 等价于MULTI/EXEC:
- 但比MULTI/EXEC更强大
- 支持条件判断和循环
4. 注意事项:
- 脚本执行时间不宜过长
- 否则会阻塞其他命令
考点2:库存扣减的Lua脚本怎么写?
参考答案:
核心逻辑:
1. 检查是否已购买(SISMEMBER)
2. 获取当前库存(GET)
3. 判断库存是否足够
4. 扣减库存(DECRBY)
5. 记录已购买用户(SADD)
6. 返回剩余库存
关键点:
- 使用SISMEMBER防止重复购买
- 先判断再扣减,避免库存变为负数
- 整个操作在一个Lua脚本中完成
考点3:EVAL和EVALSHA的区别?
参考答案:
EVAL:
- 每次都发送完整脚本
- Redis编译脚本后执行
- 适合一次性使用
EVALSHA:
- 只发送脚本的SHA1
- Redis从缓存中查找已编译的脚本
- 适合重复使用
- 性能更高(减少网络传输)
最佳实践:
- 首次使用EVAL加载脚本
- 后续使用EVALSHA调用
- 应用启动时预加载所有脚本
考点4:Lua脚本在Redis集群中有什么限制?
参考答案:
核心限制:
- Lua脚本中的所有KEY必须在同一个槽位(同一个节点)
- 不能跨节点操作
解决方案:
- 使用{hashtag}确保KEY在同一个节点
- 如:KEYS[1] = "user:{123}:info",KEYS[2] = "user:{123}:orders"
- {123}部分会被用来计算槽位
- 所有包含相同{hashtag}的KEY都会在同一个节点
七、总结与最佳实践
7.1 核心要点回顾
Redis Lua脚本核心知识:
┌─────────────────────────────────────────────────────────────┐
│ 1. 原子性保证 │
│ ├── Redis单线程执行Lua脚本 │
│ ├── 不会被其他命令打断 │
│ └── 彻底解决竞态条件 │
│ │
│ 2. 性能优化 │
│ ├── 减少网络往返(N次→1次) │
│ ├── EVALSHA缓存脚本 │
│ └── 脚本尽量简短(< 5ms) │
│ │
│ 3. 六大经典场景 │
│ ├── 库存扣减(判断+扣减+记录) │
│ ├── 分布式锁(加锁+解锁,防死锁) │
│ ├── 滑动窗口限流(ZSet + 时间窗口) │
│ ├── 延迟队列(ZSet + 到期拉取) │
│ ├── 排行榜更新(ZSet + 实时排名) │
│ └── 幂等性校验(SET NX + 过期) │
│ │
│ 4. 最佳实践 │
│ ├── 使用{hashtag}兼容集群模式 │
│ ├── 应用启动时预加载脚本 │
│ └── 脚本出错时返回错误码而非抛异常 │
└─────────────────────────────────────────────────────────────┘
7.2 性能提升数据
某电商平台实测数据:
| 指标 | 优化前(多次调用) | 优化后(Lua脚本) | 提升 |
|---|---|---|---|
| 库存扣减延迟 | 3-5ms(3次网络往返) | 0.5-1ms(1次) | 5倍↑ |
| 并发安全性 | 有竞态条件 | 完全原子 | 100%↑ |
| 超卖率 | 0.1% | 0% | 100%↓ |
| Redis QPS | 1万(多次调用) | 5万(Lua脚本) | 5倍↑ |
八、参考与拓展
互动讨论:你在项目中用过Redis Lua脚本吗?用来解决什么问题?欢迎在评论区分享!
如果本文对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,持续获取更多Java后端技术干货!