Redis其实并不是线程安全的

文章目录

    • 一、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 脚本的支持,开发者可以通过 EVALEVALSHA 命令在 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 脚本非常合适,用于高并发场景如电商下单。以下是步骤:

  1. 添加依赖

    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>
  2. 配置 Redis

    application.yml

    yaml 复制代码
    spring:
      redis:
        host: localhost  # 或你的 Redis 服务器
        port: 6379
        password: yourpassword  # 如果有
  3. 注入 RedisTemplate

    Spring Boot 自动提供 RedisTemplate<String, Object> 或自定义。用于执行 Lua 脚本。

  4. 编写 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}  -- 返回成功
  1. 服务层实现
    在 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脚本让你的工程能力更上一层。

相关推荐
_ziva_18 小时前
MAC-SQL 多智能体协作框架解析
数据库·oracle
一勺菠萝丶18 小时前
Java 后端想学 Vue,又想写浏览器插件?
java·前端·vue.js
乾元18 小时前
无线定位与链路质量预测——从“知道你在哪”,到“提前知道你会不会掉线”的网络服务化实践
运维·开发语言·人工智能·网络协议·重构·信息与通信
xie_pin_an18 小时前
C++ 类和对象全解析:从基础语法到高级特性
java·jvm·c++
AC赳赳老秦18 小时前
Unity游戏开发实战指南:核心逻辑与场景构建详解
开发语言·spring boot·爬虫·搜索引擎·全文检索·lucene·deepseek
武子康18 小时前
大数据-208 岭回归与Lasso回归:区别、应用与选择指南
大数据·后端·机器学习
Tao____18 小时前
企业级物联网平台
java·网络·物联网·mqtt·网络协议
代码不停18 小时前
MySQL事务
android·数据库·mysql
山峰哥18 小时前
数据库工程与SQL调优实战:从原理到案例的深度解析
java·数据库·sql·oracle·性能优化·编辑器