java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot Redis Lua高并发秒杀

基于SpringBoot+Redis+Lua 实现高并发秒杀系统

作者:三水不滴

这篇文章主要介绍了基于SpringBoot+Redis+Lua 实现高并发秒杀系统,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

基于 SpringBoot+Redis+Lua 构建高并发秒杀系统的设计思路,本文将聚焦核心代码实现,从环境配置、库存预热、Lua 脚本、秒杀接口到异步下单、限流防护,完整呈现可直接落地的代码方案,帮你快速搭建稳定高效的秒杀系统。

一、环境准备与依赖配置

1. 项目依赖(pom.xml)

首先在 SpringBoot 项目中引入核心依赖,包括 Redis、消息队列(以 RabbitMQ 为例)、SpringBoot 核心组件等:

<!-- SpringBoot核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 消息队列(RabbitMQ) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- Lua脚本支持 -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.7.0</version>
</dependency>
<!-- 工具类 -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.11</version>
</dependency>
<!--  lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

2. 核心配置(application.yml)

配置 Redis、RabbitMQ 连接信息,以及秒杀系统核心参数:

spring:
  # Redis配置
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    database: 0
    lettuce:
      pool:
        max-active: 16  # 最大连接数
        max-idle: 8     # 最大空闲连接
        min-idle: 4     # 最小空闲连接
  # RabbitMQ配置
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtual-host: /
    listener:
      simple:
        acknowledge-mode: manual  # 手动ACK确认
        concurrency: 5            # 消费者并发数

# 秒杀系统配置
seckill:
  redis:
    stock-key-prefix: "seckill:goods:"       # 商品库存Key前缀
    user-key-prefix: "seckill:goods:users:"  # 已秒杀用户Key前缀
  rabbitmq:
    queue-name: "seckill_order_queue"        # 订单消息队列名
    exchange-name: "seckill_order_exchange"  # 订单交换机名
    routing-key: "seckill.order"             # 路由键
  rate-limit:
    qps: 1000  # 全局限流QPS

二、核心工具类与常量定义

1. 秒杀常量类(SeckillConstant)

统一管理 Redis 键前缀、消息队列参数等常量,避免硬编码:

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 秒杀系统常量类
 */
@Getter
@AllArgsConstructor
public class SeckillConstant {
    // Redis键前缀
    public static final String STOCK_KEY_PREFIX = "seckill:goods:";
    public static final String USER_KEY_PREFIX = "seckill:goods:users:";
    public static final String REQUEST_ID_KEY_PREFIX = "seckill:request:";
    
    // 秒杀结果状态码
    public static final int SECKILL_SUCCESS = 1;  // 秒杀成功
    public static final int SECKILL_FAIL_STOCK = 0; // 库存不足
    public static final int SECKILL_FAIL_REPEAT = 2; // 重复秒杀
    
    // 消息队列参数
    public static final String ORDER_QUEUE_NAME = "seckill_order_queue";
    public static final String ORDER_EXCHANGE_NAME = "seckill_order_exchange";
    public static final String ORDER_ROUTING_KEY = "seckill.order";
}

2. Redis 工具类(RedisTemplateConfig)

配置 RedisTemplate,支持 String、Hash 等数据结构操作,以及 Lua 脚本执行:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisTemplateConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        
        // String序列化器
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        // JSON序列化器
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
        
        // Key使用String序列化
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        
        // Value使用JSON序列化
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        
        template.afterPropertiesSet();
        return template;
    }
}

三、库存预热实现(活动前数据加载)

秒杀活动开始前,将商品库存、活动状态从数据库加载到 Redis,避免活动中频繁访问数据库:

1. 商品实体类(GoodsDTO)

import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;

/**
 * 秒杀商品DTO
 */
@Data
public class GoodsDTO {
    private Long goodsId;        // 商品ID
    private String goodsName;    // 商品名称
    private BigDecimal seckillPrice; // 秒杀价格
    private Integer stock;       // 秒杀库存
    private Date startTime;      // 活动开始时间
    private Date endTime;        // 活动结束时间
    private Integer status;      // 活动状态:0-未开始,1-进行中,2-已结束
}

2. 库存预热服务(StockWarmUpService)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;

/**
 * 库存预热服务
 */
@Service
public class StockWarmUpService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 预热单个商品库存到Redis
     */
    public void warmUpGoodsStock(GoodsDTO goodsDTO) {
        if (goodsDTO == null || goodsDTO.getStock() <= 0) {
            throw new IllegalArgumentException("商品信息或库存无效");
        }
        
        // 1. 存储商品库存和状态(Hash结构)
        String stockKey = SeckillConstant.STOCK_KEY_PREFIX + goodsDTO.getGoodsId();
        Map<String, Object> goodsMap = new HashMap<>();
        goodsMap.put("stock", goodsDTO.getStock());
        goodsMap.put("status", goodsDTO.getStatus());
        goodsMap.put("startTime", goodsDTO.getStartTime().getTime());
        goodsMap.put("endTime", goodsDTO.getEndTime().getTime());
        redisTemplate.opsForHash().putAll(stockKey, goodsMap);
        
        // 2. 初始化已秒杀用户集合(Set结构,用于去重)
        String userKey = SeckillConstant.USER_KEY_PREFIX + goodsDTO.getGoodsId();
        redisTemplate.opsForSet().add(userKey, new Object[]{}); // 初始化空集合
    }

    /**
     * 批量预热商品库存(适用于多商品秒杀活动)
     */
    public void batchWarmUpGoodsStock(java.util.List<GoodsDTO> goodsDTOList) {
        for (GoodsDTO goodsDTO : goodsDTOList) {
            warmUpGoodsStock(goodsDTO);
        }
    }
}

3. 定时预热触发(可选)

通过定时任务在活动开始前 10 分钟自动预热库存:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;

@Component
public class SeckillScheduledTask {

    @Autowired
    private StockWarmUpService stockWarmUpService;
    @Autowired
    private GoodsMapper goodsMapper; // 自定义Mapper,查询待秒杀商品

    /**
     * 每天00:00预热当天秒杀商品库存(可根据实际活动时间调整)
     */
    @Scheduled(cron = "0 0 0 * * ?")
    public void scheduledWarmUpStock() {
        List<GoodsDTO> seckillGoods = goodsMapper.selectTodaySeckillGoods();
        stockWarmUpService.batchWarmUpGoodsStock(seckillGoods);
    }
}

四、核心:Lua 脚本实现原子扣减库存

通过 Lua 脚本将 “库存检查、重复秒杀拦截、库存扣减” 封装为原子操作,从根本上解决并发冲突:

1. Lua 脚本文件(seckill_stock.lua)

resources/lua目录下创建 Lua 脚本:

-- 传入参数:KEYS[1] = 商品ID,ARGV[1] = 用户ID
local goodsId = KEYS[1]
local userId = ARGV[1]

-- 定义Redis键
local stockKey = "seckill:goods:" .. goodsId
local userKey = "seckill:goods:users:" .. goodsId

-- 1. 检查商品活动状态和库存
local goodsStatus = redis.call("HGET", stockKey, "status")
if goodsStatus ~= "1" then
    return 3  -- 活动未开始或已结束
end

local stock = redis.call("HGET", stockKey, "stock")
if not stock or tonumber(stock) <= 0 then
    return 0  -- 库存不足
end

-- 2. 检查用户是否已秒杀(避免重复下单)
if redis.call("SISMEMBER", userKey, userId) == 1 then
    return 2  -- 重复秒杀
end

-- 3. 原子扣减库存并记录用户
redis.call("HINCRBY", stockKey, "stock", -1)
redis.call("SADD", userKey, userId)

return 1  -- 秒杀成功

2. Lua 脚本加载与执行服务(LuaScriptService)

import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.StaticScriptSource;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;

/**
 * Lua脚本执行服务
 */
@Service
public class LuaScriptService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private DefaultRedisScript<Integer> seckillStockScript;

    /**
     * 初始化加载Lua脚本
     */
    @PostConstruct
    public void initLuaScript() {
        seckillStockScript = new DefaultRedisScript<>();
        // 加载脚本文件
        seckillStockScript.setScriptSource(new StaticScriptSource(
                new ClassPathResource("lua/seckill_stock.lua").getContentAsString()
        ));
        // 设置返回值类型
        seckillStockScript.setResultType(Integer.class);
    }

    /**
     * 执行秒杀库存扣减脚本
     */
    public Integer executeSeckillScript(Long goodsId, Long userId) {
        List<String> keys = Collections.singletonList(goodsId.toString());
        // 执行脚本:keys=商品ID,args=用户ID
        return redisTemplate.execute(
                seckillStockScript,
                keys,
                userId.toString()
        );
    }
}

五、秒杀接口实现(含限流、防重复提交)

1. 限流注解与 AOP 实现(RateLimitAspect)

基于 Redis 实现分布式限流,拦截超 QPS 请求:

import cn.hutool.core.util.StrUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Collections;

/**
 * 限流AOP切面
 */
@Aspect
@Component
public class RateLimitAspect {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Value("${seckill.rate-limit.qps}")
    private int qps;

    private DefaultRedisScript<Long> rateLimitScript;

    @PostConstruct
    public void initRateLimitScript() {
        // 令牌桶限流Lua脚本
        String script = "local key = KEYS[1]\n" +
                        "local now = tonumber(ARGV[1])\n" +
                        "local qps = tonumber(ARGV[2])\n" +
                        "local window = 1\n" +  // 窗口时间1秒
                        "local maxToken = qps\n" +
                        "local lastTime = redis.call('HGET', key, 'lastTime')\n" +
                        "local tokenCount = redis.call('HGET', key, 'tokenCount')\n" +
                        "if not lastTime or not tokenCount then\n" +
                        "    redis.call('HSET', key, 'lastTime', now)\n" +
                        "    redis.call('HSET', key, 'tokenCount', maxToken - 1)\n" +
                        "    redis.call('EXPIRE', key, window)\n" +
                        "    return 1\n" +
                        "end\n" +
                        "local interval = now - tonumber(lastTime)\n" +
                        "local addToken = math.floor(interval / 1000 * qps)\n" +
                        "tokenCount = math.min(addToken + tonumber(tokenCount), maxToken)\n" +
                        "if tokenCount <= 0 then\n" +
                        "    return 0\n" +
                        "end\n" +
                        "redis.call('HSET', key, 'lastTime', now)\n" +
                        "redis.call('HSET', key, 'tokenCount', tokenCount - 1)\n" +
                        "return 1";
        
        rateLimitScript = new DefaultRedisScript<>(script, Long.class);
    }

    // 定义限流注解切入点
    @Pointcut("@annotation(com.seckill.annotation.RateLimit)")
    public void rateLimitPointcut() {}

    @Around("rateLimitPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        String key = "seckill:rate:limit:global"; // 全局限流Key(可扩展为IP/用户维度)
        Long now = System.currentTimeMillis();
        Long result = redisTemplate.execute(
                rateLimitScript,
                Collections.singletonList(key),
                now.toString(),
                String.valueOf(qps)
        );
        
        if (result == 1) {
            return joinPoint.proceed(); // 获得令牌,执行方法
        } else {
            throw new RuntimeException("请求过于频繁,请稍后再试");
        }
    }
}

2. 防重复提交注解(RepeatSubmit)

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
    // 防重复提交过期时间(默认5秒)
    int expireSeconds() default 5;
}

3. 秒杀接口(SeckillController)

import cn.hutool.core.util.IdUtil;
import com.seckill.annotation.RateLimit;
import com.seckill.annotation.RepeatSubmit;
import com.seckill.dto.SeckillRequestDTO;
import com.seckill.dto.SeckillResponseDTO;
import com.seckill.service.LuaScriptService;
import com.seckill.service.OrderMessageService;
import com.seckill.service.StockWarmUpService;
import com.seckill.util.SeckillConstant;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.util.Objects;

/**
 * 秒杀接口
 */
@RestController
@RequestMapping("/seckill")
public class SeckillController {

    @Autowired
    private LuaScriptService luaScriptService;
    @Autowired
    private OrderMessageService orderMessageService;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 秒杀核心接口(含限流、防重复提交)
     */
    @PostMapping("/doSeckill")
    @RateLimit  // 限流注解
    @RepeatSubmit(expireSeconds = 5)  // 防重复提交(5秒内同一请求ID不可重复提交)
    public SeckillResponseDTO doSeckill(@Valid @RequestBody SeckillRequestDTO requestDTO) {
        Long goodsId = requestDTO.getGoodsId();
        Long userId = requestDTO.getUserId();
        String requestId = requestDTO.getRequestId(); // 前端生成的唯一请求ID

        // 1. 防重复提交校验(基于Redis缓存请求ID)
        String requestKey = SeckillConstant.REQUEST_ID_KEY_PREFIX + requestId;
        Boolean hasRequest = redisTemplate.hasKey(requestKey);
        if (Boolean.TRUE.equals(hasRequest)) {
            return SeckillResponseDTO.fail("请勿重复提交请求");
        }
        redisTemplate.opsForValue().set(requestKey, "1", 5); // 缓存5秒

        try {
            // 2. 执行Lua脚本扣减库存
            Integer seckillResult = luaScriptService.executeSeckillScript(goodsId, userId);

            // 3. 处理秒杀结果
            if (SeckillConstant.SECKILL_SUCCESS == seckillResult) {
                // 秒杀成功,发送订单消息到消息队列
                orderMessageService.sendOrderMessage(goodsId, userId);
                return SeckillResponseDTO.success("秒杀成功,正在创建订单...");
            } else if (SeckillConstant.SECKILL_FAIL_STOCK == seckillResult) {
                return SeckillResponseDTO.fail("库存不足,秒杀失败");
            } else if (SeckillConstant.SECKILL_FAIL_REPEAT == seckillResult) {
                return SeckillResponseDTO.fail("您已参与过该商品秒杀,请勿重复抢购");
            } else {
                return SeckillResponseDTO.fail("活动未开始或已结束");
            }
        } finally {
            // 4. 移除防重复提交缓存(可选,根据业务需求调整)
            redisTemplate.delete(requestKey);
        }
    }
}

4. 秒杀请求 / 响应 DTO

import lombok.Data;
import javax.validation.constraints.NotNull;

/**
 * 秒杀请求DTO
 */
@Data
public class SeckillRequestDTO {
    @NotNull(message = "商品ID不能为空")
    private Long goodsId;

    @NotNull(message = "用户ID不能为空")
    private Long userId;

    @NotNull(message = "请求ID不能为空")
    private String requestId; // 前端生成的唯一请求ID,用于防重复提交
}

/**
 * 秒杀响应DTO
 */
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SeckillResponseDTO {
    private int code; // 响应码:0-失败,1-成功
    private String message; // 响应消息

    public static SeckillResponseDTO success(String message) {
        return new SeckillResponseDTO(1, message);
    }

    public static SeckillResponseDTO fail(String message) {
        return new SeckillResponseDTO(0, message);
    }
}

六、消息队列异步下单实现

1. 订单消息实体(OrderMessage)

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;

/**
 * 订单消息实体
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OrderMessage {
    private Long orderId;        // 订单ID(可在消息发送时生成)
    private Long goodsId;        // 商品ID
    private Long userId;         // 用户ID
    private BigDecimal seckillPrice; // 秒杀价格
    private Long createTime;     // 创建时间戳
}

2. 消息队列配置(RabbitMQConfig)

import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * RabbitMQ配置
 */
@Configuration
public class RabbitMQConfig {

    /**
     * 声明订单队列
     */
    @Bean
    public Queue orderQueue() {
        // 队列持久化
        return new Queue(SeckillConstant.ORDER_QUEUE_NAME, true);
    }

    /**
     * 声明订单交换机
     */
    @Bean
    public DirectExchange orderExchange() {
        return new DirectExchange(SeckillConstant.ORDER_EXCHANGE_NAME, true, false);
    }

    /**
     * 绑定队列和交换机
     */
    @Bean
    public Binding orderBinding() {
        return BindingBuilder.bind(orderQueue())
                .to(orderExchange())
                .with(SeckillConstant.ORDER_ROUTING_KEY);
    }
}

3. 订单消息发送服务(OrderMessageService)

import cn.hutool.core.util.IdUtil;
import com.seckill.dto.OrderMessage;
import com.seckill.util.SeckillConstant;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;

/**
 * 订单消息发送服务
 */
@Service
public class OrderMessageService {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 发送秒杀订单消息
     */
    public void sendOrderMessage(Long goodsId, Long userId) {
        // 生成订单ID(雪花算法或UUID)
        Long orderId = IdUtil.getSnowflake().nextId();
        
        // 构建订单消息
        OrderMessage orderMessage = new OrderMessage();
        orderMessage.setOrderId(orderId);
        orderMessage.setGoodsId(goodsId);
        orderMessage.setUserId(userId);
        orderMessage.setSeckillPrice(new BigDecimal("99.00")); // 示例秒杀价格
        orderMessage.setCreateTime(System.currentTimeMillis());

        // 发送消息到RabbitMQ
        rabbitTemplate.convertAndSend(
                SeckillConstant.ORDER_EXCHANGE_NAME,
                SeckillConstant.ORDER_ROUTING_KEY,
                orderMessage
        );
    }
}

4. 订单消息消费服务(OrderConsumerService)

import com.rabbitmq.client.Channel;
import com.seckill.dto.OrderMessage;
import com.seckill.mapper.OrderMapper;
import com.seckill.pojo.Order;
import com.seckill.util.SeckillConstant;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.io.IOException;

/**
 * 订单消息消费服务(异步创建订单)
 */
@Service
public class OrderConsumerService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @RabbitListener(queues = SeckillConstant.ORDER_QUEUE_NAME)
    public void consumeOrderMessage(OrderMessage orderMessage, Channel channel, Message message) throws IOException {
        try {
            // 1. 校验消息合法性
            if (orderMessage == null || orderMessage.getGoodsId() == null || orderMessage.getUserId() == null) {
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
                return;
            }

            // 2. 异步创建订单
            Order order = new Order();
            order.setOrderId(orderMessage.getOrderId());
            order.setGoodsId(orderMessage.getGoodsId());
            order.setUserId(orderMessage.getUserId());
            order.setSeckillPrice(orderMessage.getSeckillPrice());
            order.setCreateTime(orderMessage.getCreateTime());
            order.setStatus(0); // 0-待支付
            orderMapper.insert(order);

            // 3. 手动ACK确认消息
            channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        } catch (Exception e) {
            // 4. 处理异常:消息重投或记录日志(根据业务需求调整)
            channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
            // 回滚Redis库存(补偿机制)
            rollbackStock(orderMessage.getGoodsId());
        }
    }

    /**
     * 订单创建失败,回滚Redis库存
     */
    private void rollbackStock(Long goodsId) {
        String stockKey = SeckillConstant.STOCK_KEY_PREFIX + goodsId;
        redisTemplate.opsForHash().increment(stockKey, "stock", 1);
    }
}

七、防重复提交 AOP 实现(RepeatSubmitAspect)

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import com.seckill.annotation.RepeatSubmit;
import com.seckill.dto.SeckillRequestDTO;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * 防重复提交AOP切面
 */
@Aspect
@Component
public class RepeatSubmitAspect {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Pointcut("@annotation(repeatSubmit)")
    public void repeatSubmitPointcut(RepeatSubmit repeatSubmit) {}

    @Around("repeatSubmitPointcut(repeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) throws Throwable {
        // 获取请求参数中的SeckillRequestDTO
        Object[] args = joinPoint.getArgs();
        SeckillRequestDTO requestDTO = null;
        for (Object arg : args) {
            if (arg instanceof SeckillRequestDTO) {
                requestDTO = (SeckillRequestDTO) arg;
                break;
            }
        }

        if (requestDTO == null || Objects.isNull(requestDTO.getRequestId())) {
            throw new IllegalArgumentException("请求ID不能为空");
        }

        // 构建请求Key
        String requestKey = "seckill:request:" + requestDTO.getRequestId();
        Boolean hasKey = redisTemplate.hasKey(requestKey);
        if (Boolean.TRUE.equals(hasKey)) {
            throw new RuntimeException("请勿重复提交请求");
        }

        // 缓存请求ID
        redisTemplate.opsForValue().set(requestKey, "1", repeatSubmit.expireSeconds(), TimeUnit.SECONDS);
        try {
            return joinPoint.proceed();
        } finally {
            // 可根据业务需求决定是否移除缓存
            // redisTemplate.delete(requestKey);
        }
    }
}

八、核心代码总结与部署建议

1. 代码核心亮点

  1. 原子性保障:通过 Lua 脚本封装库存校验、扣减和用户去重,避免超卖与重复秒杀;
  2. 流量分层拦截:结合 Redis 令牌桶限流、防重复提交 AOP,从源头减少无效请求;
  3. 异步解耦:RabbitMQ 异步处理订单创建,提升接口响应速度,降低数据库压力;
  4. 数据一致性:消息消费失败时回滚 Redis 库存,确保缓存与数据库数据同步;
  5. 可扩展性:支持库存分片、预售模式等复杂场景扩展,适配不同业务需求。

2. 部署与测试建议

  1. Redis 部署:采用主从复制 + 哨兵模式,确保高可用;超大规模秒杀建议使用 Redis Cluster 分片存储库存;
  2. 消息队列:开启 RabbitMQ 持久化与镜像队列,避免消息丢失;根据流量调整消费者并发数;
  3. 压力测试:使用 JMeter 模拟 10 万 + 并发请求,重点测试库存准确性、接口响应时间和系统稳定性;
  4. 监控告警:通过 Prometheus+Grafana 监控 Redis 库存、消息队列堆积量、接口 QPS 等核心指标,设置阈值告警。

本文完整呈现了 SpringBoot+Redis+Lua 秒杀系统的核心代码,覆盖从库存预热到异步下单的全流程,代码可直接复制到项目中使用,只需根据实际业务调整参数与扩展功能。

到此这篇关于基于SpringBoot+Redis+Lua 实现高并发秒杀系统的文章就介绍到这了,更多相关SpringBoot Redis Lua高并发秒杀内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文