Redis通过lua脚本模拟一个商品秒杀

为应对高并发秒杀活动,本文介绍使用Redis与Lua脚本实现库存控制的方法。通过编写Lua脚本确保库存扣减的原子性,并提供一个Java实现案例。该方案有效避免了超卖现象,减轻数据库压力。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.使用场景

       公司2周年准备上一个秒杀,针对商品A,价值1W,准备在上午10点整,进行一次秒杀,一共20个库存。

预测当日会有100W用户进行抢购

 

2.遇到的问题

      高并发、库存不能超卖、不能查询数据库 否则数据库抵挡不住压力

 

3.实现方案

      1.使用 redis 保存四件商品的库存数量

测试:0>set seckillGoods:1001 20
"OK"

 

  2.利用lua脚本进行减库存,首先编写好lua脚本

  网上有很多案例,lua脚本如果直接复制,都会存在问题(少分号、少空格、引号错误、忘记转数字等),这里已解决。

  如果只想要脚本则复制以下代码即可

local isExist = redis.call('exists', KEYS[1]);
if (tonumber(isExist) > 0) then
	local goodsNumber = redis.call('get', KEYS[1]);
	if (tonumber(goodsNumber) > 0) then
		redis.call('decr',KEYS[1]);
		return 1;
	else
		redis.call('del', KEYS[1]);
		return 0;
		end;
else
return -1;
end;

 

  3.编写测试案例

@Component
public class LuaReduceStock {

    private static final String STOCK_LUA;

    @Resource
    private RedisTemplate redisTemplate;

    static {
        StringBuilder s = new StringBuilder();
        s.append("local isExist = redis.call('exists', KEYS[1]); ");
        s.append("if (tonumber(isExist) > 0) then ");
        s.append("      local goodsNumber = redis.call('get', KEYS[1]);  ");
        s.append("      if (tonumber(goodsNumber) > 0) then ");
        s.append("          redis.call('decr',KEYS[1]);   ");
        s.append("          return 1;   ");
        s.append("      else "
                          +"redis.call('del', KEYS[1]);    ");
        s.append("          return 0; ");
        s.append("          end; ");
        s.append("else ");
        s.append("      return -1; ");
        s.append("      end;");
        STOCK_LUA = s.toString();
    }

/**
 * 减库存
 * @param key
 * @return
 */
    public boolean reduceStock(String key){
        List<String> keys = new ArrayList<>();
        keys.add(key);

        List<String> args = new ArrayList<>();

        Long result  = (Long) redisTemplate.execute(new RedisCallback() {
            @Override
            public Long doInRedis(RedisConnection connection) throws DataAccessException {
                Object nativeConnection = connection.getNativeConnection();
                Object eval = ((Jedis) nativeConnection).eval(STOCK_LUA, keys, args);
                return Long.valueOf(eval.toString());
            }
        });
        return result  > 0;
    }
}

  

@Resource
private LuaReduceStock luaReduceStock;

@RequestMapping("/api/v4/testLuaReduceStock")
public JsonResult testLuaReduceStock(String key){
    List<Thread> allThread = new ArrayList<>();

    for(int i = 1; i<= 1000; i++){
        Thread thread = new Thread(()-> {
            boolean b = luaReduceStock.reduceStock(key);
            if(b){
                System.out.println("恭喜您,抢到了库存!!!" + Thread.currentThread().getName());
                //MQ.sendSuccessMessage();
                最终抢到库存的用户,可以发送一条消息到队列中,进行异步下单。
            }else{
                System.out.println("对不起,库存已卖光啦!!!" + Thread.currentThread().getName());
            }
        },"线程" + i);
        allThread.add(thread);
    }
    for(Thread thread : allThread){
        thread.start();
    }
    return JsonResult.buildSuccessResult("成功");
}

 

最终看控制台打印的效果可知,只有20个线程抢到了库存。

 

 

 

4.总结lua脚本调用redis的优势

  1. 多个命令合并到脚本统一处理,减少多个命令的网络开销
  2. 脚本能确保操作的原子性,不会受到其他客户端命令的影响
  3. 代码复用,redis将永久存放客户端发送的脚本
### 使用 RedisLua 脚本实现秒杀功能 #### 思路概述 Redis 是一种高性能的键值存储数据库,支持原子操作和事务处理。通过结合 Lua 脚本,可以确保多个并发请求在同一时间内的执行逻辑一致,从而有效防止超卖现象的发生[^1]。 Lua 脚本可以在 Redis 中运行,并且具有原子性特性,这意味着整个脚本会在单线程环境中一次性完成,不会被其他命令打断。因此,在高并发场景下的秒杀活动可以通过 Lua 脚本来控制库存减少以及订单生成的过程[^2]。 --- #### 架构设计 为了实现完整的秒杀系统,通常会采用以下技术栈: - **前端**:提供用户界面供客户参与秒杀。 - **后端服务**:基于 Spring Boot 开发接口,负责接收用户的请求并调用 Redis 进行业务逻辑处理。 - **数据层**: - **Redis**:用于缓存商品库存信息、限流计数器等高频访问的数据。 - **MySQL**:作为持久化存储,保存最终的商品销售记录和订单详情。 表结构建议如下: 1. **产品表 (Product)** | 字段名 | 类型 | 描述 | |--------------|--------------|----------------| | id | INT | 商品ID | | name | VARCHAR(255) | 商品名称 | | stock | INT | 库存数量 | 2. **订单表 (Order)** | 字段名 | 类型 | 描述 | |--------------|--------------|------------------| | order_id | BIGINT | 订单唯一标识 | | product_id | INT | 关联的产品ID | | user_id | INT | 下单用户ID | | create_time | DATETIME | 创建时间 | --- #### 配置文件与依赖项 以下是 Maven `pom.xml` 文件中的部分依赖配置: ```xml <dependencies> <!-- Spring Boot Starter Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Data Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- MySQL Connector --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- MyBatis Integration --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency> </dependencies> ``` --- #### 核心业务代码展示 ##### 1. Lua 脚本编写 (`sha_2.lua`)脚本的主要作用是从 Redis 缓存中获取当前商品的剩余库存量,并判断是否允许下单。如果允许,则扣除库存并将订单信息写入队列以便后续同步至 MySQL 数据库。 ```lua local key = KEYS[1] local userId = ARGV[1] -- 获取当前库存 local stock = tonumber(redis.call('get', key)) if stock and stock > 0 then -- 扣除库存 redis.call('decrby', key, 1) -- 设置已购买标志(避免重复提交) local purchasedKey = 'user:' .. userId .. ':bought' if not redis.call('exists', purchasedKey) then redis.call('setex', purchasedKey, 60, 1) -- 用户限购标记有效期为60秒 return 1 -- 返回成功状态码 end end return 0 -- 失败或无库存 ``` ##### 2. Java 控制器代码 在 Spring Boot 中定义一个控制器方法来触发 RedisLua 脚本执行。 ```java @RestController @RequestMapping("/api/seckill") public class SeckillController { @Autowired private StringRedisTemplate stringRedisTemplate; @Value("${lua.script.path}") private String luaScriptPath; /** * 秒杀接口 */ @PostMapping("/{productId}") public ResponseEntity<String> seckill(@PathVariable Long productId, @RequestParam Long userId) { try { // 加载 Lua 脚本 DefaultRedisScript<Long> script = new DefaultRedisScript<>(); script.setLocation(new ClassPathResource(luaScriptPath)); script.setResultType(Long.class); List<String> keys = Arrays.asList("product:" + productId); Object result = stringRedisTemplate.execute(script, keys, userId.toString()); if ("1".equals(result)) { return ResponseEntity.ok("秒杀成功!"); } } catch (Exception e) { log.error("秒杀失败", e); } return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("秒杀失败或库存不足!"); } } ``` --- #### 测试方案 使用 JMeter 工具模拟大量并发请求验证系统的稳定性和性能表现。需要注意的是,在实际部署前应充分考虑以下几个方面可能存在的问题: 1. 如果未正确初始化 Redis 中的关键字及其对应的初始值可能导致异常行为发生[^3]; 2. 当存在语法错误或者逻辑缺陷时可能会引发程序崩溃等问题[^4]; --- ###
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值