
文章目录
-
- 一、Redis的原子性为什么会出问题
- 二、Redis事务命令
- 三、为什么用lua脚本就能解决呢?
- 四、Lua脚本介绍
- 五、在 Spring Boot 中集成 Redis + Lua 脚本实现下单原子性
- 结语:
一、Redis的原子性为什么会出问题
Redis 不是单线程的吗?那所有操作不就天然原子了吗?为什么还需要 Lua 脚本来保证原子性?
Redis 的"单线程"是指命令的执行是串行的,但"多个命令组成的逻辑"并不是原子的。
举个例子,这是你的下单模块代码:
java
stock = redis.get("stock") # 命令1
if stock > 0:
redis.decr("stock") # 命令2
create_order() # 本地逻辑
虽然 Redis 是单线程,但命令1 和 命令2 是两个独立的请求,执行过程如下:
java
客户端A: GET stock → 返回 1
客户端B: GET stock → 返回 1 ← 在A执行DECR前,B也读到了1!
客户端A: DECR stock → stock=0
客户端B: DECR stock → stock=-1
结果就超卖了,很明显redis的确是原子性的,但是这个下单的过程不是原子性的。
二、Redis事务命令
Redis 提供了 MULTI/EXEC 事务,但是能解决这个问题吗?
Redis 提供了一组用于实现事务的命令,允许客户端将多个命令打包,然后一次性、按顺序地执行。Redis 的事务并不支持回滚,但能保证这些命令在执行期间不会被其他客户端的请求打断,具有原子性地排队执行。
以下是 Redis 事务相关的常用命令:
1. MULTI 标记事务块的开始,执行 MULTI 后,后续的命令不会立即执行,而是被放入一个队列中,返回值 :总是返回 OK。
bash
> MULTI
OK
2. EXEC 执行事务块中的所有命令,一旦调用 EXEC,Redis 将按顺序执行从 MULTI 开始以来的所有排队命令,返回一个数组,包含每个命令的执行结果;如果事务未正常启动,则返回错误。
bash
> MULTI
> INCR foo
> INCR bar
> EXEC
1) (integer) 1
2) (integer) 1
补充说明
- Redis 事务 不支持回滚 :如果某个命令在
EXEC阶段出错,该命令会报错,但其余命令仍会继续执行。
一句话来说就是:MULTI 命令将多个 Redis 命令打包成一个事务队列,在 EXEC时按顺序原子性地执行,支持批量、顺序、隔离执行,但不支持错误回滚;配合 WATCH 可实现乐观锁和应用层重试。
如需进一步了解,可参考 Redis 官方文档 - Transactions。
我们看看MUIT命令要怎么做
java
MULTI
GET stock
DECR stock # ← 无论 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)中:xml<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:yamlspring: redis: host: localhost # 或你的 Redis 服务器 port: 6379 password: yourpassword # 如果有 -
注入 RedisTemplate :
Spring Boot 自动提供
RedisTemplate<String, Object>或自定义。用于执行 Lua 脚本。 -
编写 Lua 脚本 :
创建资源文件
src/main/resources/scripts/place_order.lua
lua
-- 输入:KEYS[1] = 库存键, KEYS[2] = 订单键
-- ARGV[1] = 购买数量, ARGV[2] = 订单ID, ARGV[3] = 用户ID, ARGV[4] = 其他订单数据(JSON字符串)
local inventory_key = KEYS[1]
local order_key = KEYS[2]
local quantity = tonumber(ARGV[1])
local order_id = ARGV[2]
local user_id = ARGV[3]
local order_data = ARGV[4] -- e.g., '{"price":100,"item":"book"}'
-- 获取当前库存
local current_inventory = tonumber(redis.call('GET', inventory_key) or 0)
-- 检查库存
if current_inventory < quantity then
return {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 类中加载并执行脚本:java@Service public class OrderService { @Autowired private RedisTemplate<String, Object> redisTemplate; public Object placeOrder(String productId, int quantity, String userId, String orderData) { // 加载 Lua 脚本 RedisScript<List> script = new DefaultRedisScript<>( new ClassPathResource("scripts/place_order.lua"), List.class); // 准备键和参数 String inventoryKey = "product:inventory:" + productId; String orderId = generateOrderId(); // 自定义生成 ID String orderKey = "order:" + orderId; List<String> keys = Arrays.asList(inventoryKey, orderKey); // 执行脚本 List result = redisTemplate.execute(script, keys, quantity, orderId, userId, orderData); // 处理结果 if ((long) result.get(0) == 1) { return "下单成功: " + result.get(2); } else { return "错误: " + result.get(1); } } private String generateOrderId() { // 实现 ID 生成,如 UUID return java.util.UUID.randomUUID().toString(); } }
结语:
Lua 脚本为 Redis 提供了"服务器端可编程能力",让原本只能执行简单命令的 Redis 变成了一个支持复杂原子业务逻辑的强大引擎,广泛应用于高并发场景如秒杀、排行榜、实时统计等,学会使用lua脚本让你的工程能力更上一层。