文章目录
- 一、Redis的原子性为什么会出问题
- 二、Redis事务命令
- 三、为什么用lua脚本就能解决呢?
- 四、Lua脚本介绍
- 五、在 Spring Boot 中集成 Redis + Lua 脚本实现下单原子性
- 结语:
一、Redis的原子性为什么会出问题
Redis 不是单线程的吗?那所有操作不就天然原子了吗?为什么还需要 Lua 脚本来保证原子性?
Redis 的“单线程”是指命令的执行是串行的,但“多个命令组成的逻辑”并不是原子的。
举个例子,这是你的下单模块代码:
stock=redis.get("stock")# 命令1ifstock>0:redis.decr("stock")# 命令2create_order()# 本地逻辑虽然 Redis 是单线程,但命令1 和 命令2 是两个独立的请求,执行过程如下:
客户端A:GETstock → 返回1客户端B:GETstock → 返回1← 在A执行DECR前,B也读到了1! 客户端A:DECRstock → stock=0客户端B:DECRstock → stock=-1结果就超卖了,很明显redis的确是原子性的,但是这个下单的过程不是原子性的。
二、Redis事务命令
Redis 提供了 MULTI/EXEC 事务,但是能解决这个问题吗?
Redis 提供了一组用于实现事务的命令,允许客户端将多个命令打包,然后一次性、按顺序地执行。Redis 的事务并不支持回滚,但能保证这些命令在执行期间不会被其他客户端的请求打断,具有原子性地排队执行。
以下是 Redis 事务相关的常用命令:
1.MULTI标记事务块的开始,执行MULTI后,后续的命令不会立即执行,而是被放入一个队列中,返回值:总是返回OK。
>MULTI OK2.EXEC执行事务块中的所有命令,一旦调用EXEC,Redis 将按顺序执行从MULTI开始以来的所有排队命令,返回一个数组,包含每个命令的执行结果;如果事务未正常启动,则返回错误。
>MULTI>INCR foo>INCR bar>EXEC1)(integer)12)(integer)1补充说明
- Redis 事务不支持回滚:如果某个命令在
EXEC阶段出错,该命令会报错,但其余命令仍会继续执行。
一句话来说就是:MULTI 命令将多个 Redis 命令打包成一个事务队列,在 EXEC时按顺序原子性地执行,支持批量、顺序、隔离执行,但不支持错误回滚;配合 WATCH 可实现乐观锁和应用层重试。
如需进一步了解,可参考 Redis 官方文档 - Transactions。
我们看看MUIT命令要怎么做
MULTIGETstockDECRstock # ← 无论GET结果是什么,DECR都会执行EXEC结果是否定的,Redis 有事务MULTI / EXEC,但它只是打包命令,并不支持回滚或条件判断,所以并不能实现逻辑的原子性。
三、为什么用lua脚本就能解决呢?
Redis是单线程的,把整个下单的逻辑封装到脚本里面实现了逻辑和命令的双重原子性保证。
都是脚本封装命令,不能用python吗?python脚本使用场景也更广
Redis 允许在内部执行 Lua 脚本来保证多步操作的原子性,因为 Lua 脚本是在 Redis 进程内、无 I/O 阻塞、可预测地一次性执行完的;而Python 等外部脚本无法在 Redis 内部运行,必须通过网络多次交互,破坏了原子性。
四、Lua脚本介绍
1.背景
Lua是一种轻量级、嵌入式脚本语言,由巴西的Pontifical Catholic University of Rio de Janeiro的团队在1993年开发。它最初设计用于嵌入到其他应用程序中作为脚本引擎,比如游戏开发、自动化脚本和配置工具。Lua的发音是“loo-ah”,源自葡萄牙语,意思是“月亮”。
2.功能
Lua 脚本是一种轻量级、高效的嵌入式脚本语言,以简单语法和强大表数据结构著称,常用于游戏开发、自动化和嵌入式系统。Redis 从 2.6 版本开始内置了对 Lua 脚本的支持,开发者可以通过EVAL或EVALSHA命令在 Redis 服务器端执行一段 Lua 代码。
最核心的优势在于原子性:整个 Lua 脚本作为一个不可分割的单元执行,在运行期间 Redis 会阻塞其他命令,确保脚本中的所有 Redis 操作(通过redis.call()或redis.pcall()调用)不会被其他客户端中断。这解决了多客户端并发时常见的“读取-修改-写入”竞态问题,比如实现原子递增、库存扣减、分布式锁、限流、CAS(Check-And-Set)等复杂逻辑。
相比 Redis 原生的MULTI/EXEC事务,Lua 脚本更灵活,支持条件判断、循环、变量计算,执行效率更高,但也要求脚本尽量短小,避免长时间阻塞服务器。
五、在 Spring Boot 中集成 Redis + Lua 脚本实现下单原子性
以订单下单为例,在 Spring Boot 项目中集成 Redis 和 Lua 脚本非常合适,用于高并发场景如电商下单。以下是步骤:
添加依赖:
在pom.xml(Maven)中:<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><!-- 或 lettuce,根据偏好 --></dependency></dependencies>配置 Redis:
在application.yml:spring:redis:host:localhost# 或你的 Redis 服务器port:6379password:yourpassword# 如果有注入 RedisTemplate:
Spring Boot 自动提供RedisTemplate<String, Object>或自定义。用于执行 Lua 脚本。编写 Lua 脚本:
创建资源文件src/main/resources/scripts/place_order.lua
-- 输入:KEYS[1] = 库存键, KEYS[2] = 订单键-- ARGV[1] = 购买数量, ARGV[2] = 订单ID, ARGV[3] = 用户ID, ARGV[4] = 其他订单数据(JSON字符串)localinventory_key=KEYS[1]localorder_key=KEYS[2]localquantity=tonumber(ARGV[1])localorder_id=ARGV[2]localuser_id=ARGV[3]localorder_data=ARGV[4]-- e.g., '{"price":100,"item":"book"}'-- 获取当前库存localcurrent_inventory=tonumber(redis.call('GET',inventory_key)or0)-- 检查库存ifcurrent_inventory<quantitythenreturn{0,"库存不足"}-- 返回错误end-- 扣减库存redis.call('DECRBY',inventory_key,quantity)-- 创建订单(用HSET存储哈希)redis.call('HSET',order_key,'id',order_id,'user_id',user_id,'quantity',quantity,'data',order_data)-- 可选:设置过期时间或推入订单队列-- redis.call('EXPIRE', order_key, 3600) -- 1小时过期return{1,"下单成功",order_id}-- 返回成功- 服务层实现:
在 Service 类中加载并执行脚本:@ServicepublicclassOrderService{@AutowiredprivateRedisTemplate<String,Object>redisTemplate;publicObjectplaceOrder(StringproductId,intquantity,StringuserId,StringorderData){// 加载 Lua 脚本RedisScript<List>script=newDefaultRedisScript<>(newClassPathResource("scripts/place_order.lua"),List.class);// 准备键和参数StringinventoryKey="product:inventory:"+productId;StringorderId=generateOrderId();// 自定义生成 IDStringorderKey="order:"+orderId;List<String>keys=Arrays.asList(inventoryKey,orderKey);// 执行脚本Listresult=redisTemplate.execute(script,keys,quantity,orderId,userId,orderData);// 处理结果if((long)result.get(0)==1){return"下单成功: "+result.get(2);}else{return"错误: "+result.get(1);}}privateStringgenerateOrderId(){// 实现 ID 生成,如 UUIDreturnjava.util.UUID.randomUUID().toString();}}
结语:
Lua 脚本为 Redis 提供了“服务器端可编程能力”,让原本只能执行简单命令的 Redis 变成了一个支持复杂原子业务逻辑的强大引擎,广泛应用于高并发场景如秒杀、排行榜、实时统计等,学会使用lua脚本让你的工程能力更上一层。