Redis + MySQL 构建定时秒杀功能详解(初学者从 0 到 1 实战)

Redis + MySQL 实现定时秒杀功能详解(初学者从 0 到 1 实战)

        结合我们之前学习的 Redis 缓存、MySQL 持久化、Spring Boot 集成等知识,本文将聚焦定时秒杀功能的完整实现 —— 从需求分析、技术选型,到代码拆解、步骤细化,每个环节都解释 “为什么这么做”,帮初学者搞懂秒杀的核心痛点(超卖、重复购买、高并发)及解决方案。

一、秒杀需求分析:我们要解决什么问题?

        在写代码前,必须先明确秒杀的核心痛点,否则代码会漏洞百出(比如库存超卖、系统扛不住高并发)。定时秒杀的核心需求的是:

  1. 定时控制:秒杀只在指定时间starttime-endtime内开放,之外的请求拒绝;
  2. 防超卖:商品库存有限(如 100 件),不能因为高并发导致卖出 101 件
  3. 防重复购买:同一用户不能重复秒杀同一商品;
  4. 高并发扛住:秒杀时可能有 10 万用户同时请求,不能让系统崩溃
  5. 限流保护:防止恶意请求(如每秒 1 万次请求)压垮系统。

二、技术选型:为什么用这些工具?

        针对上述需求,我们选择以下技术栈,每一个选型都对应解决一个痛点:

技术工具解决的问题核心作用
Redis高并发减库存、防重复购买、缓存商品信息原子操作decrement防超卖Set 存用户 ID 防重复缓存商品减少 MySQL 压力
MySQL持久化商品信息、订单信息存储商品原始库存、用户订单(Redis 崩溃后数据不丢失)
Spring Boot快速开发微服务接口简化配置,整合 Redis、MySQL、AOP
Spring AOP接口限流限制每秒请求数(如每秒 5 次),保护系统
MyBatis操作 MySQL 数据库执行商品库存更新、订单创建 SQL

三、步骤 1:环境准备(数据库 + 实体类)

3.1 MySQL 数据库设计(核心表结构)

        秒杀需要 2 张核心表:seckill_goods(秒杀商品表)和seckill_order(秒杀订单表),字段设计对应解决 “防超卖”“防重复” 需求。

1. 秒杀商品表(seckill_goods)
CREATE TABLE `seckill_goods` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID',
  `name` varchar(255) NOT NULL COMMENT '商品名称(如“秒杀手机”)',
  `count` int(11) NOT NULL COMMENT '商品总库存(如100件)',
  `starttime` datetime NOT NULL COMMENT '秒杀开始时间(如2025-10-01 10:00:00)',
  `endtime` datetime NOT NULL COMMENT '秒杀结束时间(如2025-10-01 11:00:00)',
  `version` int(11) DEFAULT '0' COMMENT '乐观锁版本号(防超卖)',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 插入测试数据(100件手机,10点开始,11点结束)
INSERT INTO `seckill_goods` (`name`, `count`, `starttime`, `endtime`, `version`)
VALUES ('秒杀手机', 100, '2025-10-01 10:00:00', '2025-10-01 11:00:00', 0);

字段解释

  • version:乐观锁,更新库存时判断版本号是否一致,防止并发超卖
  • starttime/endtime:控制秒杀的开始和结束时间。
2. 秒杀订单表(seckill_order)
CREATE TABLE `seckill_order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID(如1001)',
  `goods_id` bigint(20) NOT NULL COMMENT '商品ID(关联seckill_goods.id)',
  `createtime` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '订单创建时间',
  PRIMARY KEY (`id`),
  -- 唯一索引:同一用户不能重复秒杀同一商品(防重复购买)
  UNIQUE KEY `idx_user_goods` (`user_id`,`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

字段解释

  • idx_user_goods唯一索引,当同一用户(user_id)秒杀同一商品(goods_id)时,数据库会报唯一约束错误,防止重复订单

3.2 实体类设计(包名:com.lh.seckill.entity)

        实体类字段需与数据库表一一对应,时间类型用LocalDateTime(Java 8 + 推荐)。

1. 秒杀商品实体(SeckillGoods.java)
package com.lh.seckill.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data // Lombok注解:自动生成Getter/Setter/toString
public class SeckillGoods {
    private Long id; // 商品ID(对应表中id)
    private String name; // 商品名称(对应表中name)
    private Integer count; // 总库存(对应表中count)
    private LocalDateTime startTime; // 秒杀开始时间(对应表中starttime)
    private LocalDateTime endTime; // 秒杀结束时间(对应表中endtime)
    private Integer version; // 乐观锁版本号(对应表中version)
}
2. 秒杀订单实体(SeckillOrder.java)
package com.lh.seckill.entity;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class SeckillOrder {
    private Long id; // 订单ID(对应表中id)
    private Long userId; // 用户ID(对应表中user_id)
    private Long goodsId; // 商品ID(对应表中goods_id)
    private LocalDateTime createTime; // 订单创建时间(对应表中createtime)
}

四、步骤 2:项目依赖配置(pom.xml)

        在 Spring Boot 项目中引入核心依赖。



    
        org.springframework.boot
        spring-boot-starter-parent
        2.7.5 
        
    
    com.lh
    seckill-demo
    1.0-SNAPSHOT
    
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
            mysql
            mysql-connector-java
            runtime 
        
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            2.2.0
        
        
        
            org.springframework.boot
            spring-boot-starter-aop
        
        
        
            org.projectlombok
            lombok
            true
        
    

五、步骤 3:核心配置(Redis+MySQL)

5.1 Redis 配置(解决序列化 + 工具类)

        Redis 默认序列化会导致 key 乱码、对象无法直接存储,我们配置 JSON 序列化,包名:com.lh.seckill.config

package com.lh.seckill.config;
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 // 标记为配置类,Spring启动时加载
public class RedisConfig {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate template = new RedisTemplate<>();
        template.setConnectionFactory(factory); // 绑定Redis连接工厂
        // 1. Key序列化:用StringRedisSerializer,避免key乱码(如“seckill:goods:1”)
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        template.setKeySerializer(keySerializer);
        template.setHashKeySerializer(keySerializer); // Hash的key也用String序列化
        // 2. Value序列化:用GenericJackson2JsonRedisSerializer,支持对象JSON序列化
        GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
        template.setValueSerializer(valueSerializer);
        template.setHashValueSerializer(valueSerializer); // Hash的value也用JSON序列化
        template.afterPropertiesSet(); // 初始化RedisTemplate
        return template;
    }
}

配置解释

  • StringRedisSerializer:处理 key,确保 key 是明文(如seckill:stock:1),方便 Redis 控制台查看;
  • GenericJackson2JsonRedisSerializer处理 value,将对象(如SeckillGoods转为 JSON 存储,读取时自动转为对象。

5.2 MySQL 配置(application.yml)

        配置 MySQL 连接信息和 MyBatis,放在src/main/resources/application.yml

spring:
  # 1. MySQL数据库配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver # MySQL 8.x驱动
    url: jdbc:mysql://localhost:3306/seckill_db?useSSL=false&serverTimezone=Asia/Shanghai
    username: root # 你的MySQL用户名
    password: root # 你的MySQL密码
  # 2. Redis配置
  data:
    redis:
      host: 127.0.0.1 # Redis地址(本地)
      port: 6379 # Redis默认端口
      database: 0 # Redis数据库编号(0-15)
# 3. MyBatis配置
mybatis:
  mapper-locations: classpath:mapper/*.xml # Mapper.xml文件路径
  type-aliases-package: com.lh.seckill.entity # 实体类包名(简化XML中的类名)
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名转换(如starttime→startTime)

六、步骤 4:数据访问层(Mapper 接口 + XML)

        用 MyBatis 操作 MySQL,实现商品查询、库存更新、订单创建,包名:com.lh.seckill.mapper

6.1 商品 Mapper(SeckillGoodsMapper.java)

package com.lh.seckill.mapper;
import com.lh.seckill.entity.SeckillGoods;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper // 标记为MyBatis Mapper接口,Spring自动扫描
public interface SeckillGoodsMapper {
    /**
     * 查询所有秒杀商品(项目启动时预加载到Redis)
     */
    List findAll();
    /**
     * 乐观锁减库存:version一致才更新(防超卖)
     * @param goodsId 商品ID
     * @param reduceCount 减少的库存数(这里固定为1)
     * @param version 版本号(当前商品的version)
     * @return 影响行数:1=成功,0=失败(version不一致,说明已被其他线程更新)
     */
    int reduceStockWithVersion(Long goodsId, int reduceCount, Integer version);
}

6.2 订单 Mapper(SeckillOrderMapper.java)

package com.lh.seckill.mapper;
import com.lh.seckill.entity.SeckillOrder;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SeckillOrderMapper {
    /**
     * 创建秒杀订单
     * @param order 订单对象
     * @return 影响行数:1=成功,0=失败(如唯一索引冲突,重复秒杀)
     */
    int insert(SeckillOrder order);
}

6.3 Mapper XML 文件(src/main/resources/mapper)

1. SeckillGoodsMapper.xml



    
    
    
    
        UPDATE seckill_goods
        SET count = count - #{reduceCount},
            version = version + 1  
        WHERE id = #{goodsId}
          AND count > 0  
          AND version = #{version}  
    
2. SeckillOrderMapper.xml



    
    
        INSERT INTO seckill_order (user_id, goods_id, createtime)
        VALUES (#{userId}, #{goodsId}, NOW())
    

七、步骤 5:核心业务层(秒杀逻辑实现)

业务层是秒杀的核心,解决 “定时控制、防超卖、防重复”,包名:com.lh.seckill.service

7.1 秒杀服务(SeckillService.java)

package com.lh.seckill.service;
import com.lh.seckill.entity.SeckillGoods;
import com.lh.seckill.entity.SeckillOrder;
import com.lh.seckill.mapper.SeckillGoodsMapper;
import com.lh.seckill.mapper.SeckillOrderMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.util.List;
@Service // 标记为服务层Bean,Spring管理
public class SeckillService {
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private SeckillGoodsMapper goodsMapper;
    @Autowired
    private SeckillOrderMapper orderMapper;
    // Redis Key常量:规范命名,避免混乱
    private static final String SECKILL_GOODS_KEY = "seckill:goods:"; // 商品信息Key前缀(Hash)
    private static final String SECKILL_STOCK_KEY = "seckill:stock:"; // 商品库存Key前缀(String)
    private static final String SECKILL_USER_KEY = "seckill:user:";  // 用户购买记录Key前缀(Set)
    /**
     * 项目启动时执行:将秒杀商品预加载到Redis(减少MySQL压力)
     * @PostConstruct:Spring Bean初始化后自动执行
     */
    @PostConstruct
    public void initSeckillGoods() {
        // 1. 从MySQL查询所有秒杀商品
        List goodsList = goodsMapper.findAll();
        if (goodsList.isEmpty()) {
            System.out.println("没有秒杀商品数据,预加载跳过");
            return;
        }
        // 2. 遍历商品,存入Redis
        for (SeckillGoods goods : goodsList) {
            Long goodsId = goods.getId();
            // 2.1 商品信息存入Redis Hash:key=seckill:goods:1,field=info,value=SeckillGoods对象
            redisTemplate.opsForHash().put(SECKILL_GOODS_KEY + goodsId, "info", goods);
            // 2.2 商品库存存入Redis String:key=seckill:stock:1,value=库存数(如100)
            redisTemplate.opsForValue().set(SECKILL_STOCK_KEY + goodsId, goods.getCount());
            System.out.println("预加载商品到Redis:ID=" + goodsId + ",名称=" + goods.getName());
        }
    }
    /**
     * 检查秒杀是否在有效期内
     * @param goodsId 商品ID
     * @return true=在有效期内,false=不在
     */
    public boolean checkSeckillTime(Long goodsId) {
        // 1. 从Redis获取商品信息(避免查MySQL)
        SeckillGoods goods = (SeckillGoods) redisTemplate.opsForHash()
                .get(SECKILL_GOODS_KEY + goodsId, "info");
        if (goods == null) {
            System.out.println("商品不存在:ID=" + goodsId);
            return false;
        }
        // 2. 判断当前时间是否在[startTime, endTime]之间
        LocalDateTime now = LocalDateTime.now();
        boolean isAfterStart = now.isAfter(goods.getStartTime());
        boolean isBeforeEnd = now.isBefore(goods.getEndTime());
        if (!isAfterStart) {
            System.out.println("秒杀未开始:ID=" + goodsId + ",开始时间=" + goods.getStartTime());
        }
        if (!isBeforeEnd) {
            System.out.println("秒杀已结束:ID=" + goodsId + ",结束时间=" + goods.getEndTime());
        }
        return isAfterStart && isBeforeEnd;
    }
    /**
     * 秒杀核心方法(事务保证订单创建和库存更新一致)
     * @param userId 用户ID(如1001,实际项目从登录态获取)
     * @param goodsId 商品ID
     * @return true=秒杀成功,false=秒杀失败
     */
    @Transactional // 开启事务:确保订单创建和库存更新要么都成功,要么都失败
    public boolean seckill(Long userId, Long goodsId) {
        // 步骤1:检查秒杀时间(不在时间内直接拒绝)
        if (!checkSeckillTime(goodsId)) {
            throw new RuntimeException("秒杀未开始或已结束");
        }
        // 步骤2:Redis原子减库存(防超卖核心:decrement是原子操作,不会并发超卖)
        String stockKey = SECKILL_STOCK_KEY + goodsId;
        Long remainingStock = redisTemplate.opsForValue().decrement(stockKey);
        // 库存不足:remainingStock<0说明库存已被抢完,恢复库存(因为decrement已减1,需要加回1)
        if (remainingStock == null || remainingStock < 0) {
            redisTemplate.opsForValue().increment(stockKey); // 恢复库存
            throw new RuntimeException("库存不足,秒杀失败");
        }
        // 步骤3:检查用户是否已秒杀过(防重复购买:用Redis Set存用户ID)
        String userKey = SECKILL_USER_KEY + goodsId;
        Boolean isMember = redisTemplate.opsForSet().isMember(userKey, userId.toString());
        if (Boolean.TRUE.equals(isMember)) {
            redisTemplate.opsForValue().increment(stockKey); // 恢复库存
            throw new RuntimeException("您已秒杀过该商品,不能重复购买");
        }
        try {
            // 步骤4:从Redis获取商品信息(用于乐观锁减库存)
            SeckillGoods goods = (SeckillGoods) redisTemplate.opsForHash()
                    .get(SECKILL_GOODS_KEY + goodsId, "info");
            if (goods == null) {
                redisTemplate.opsForValue().increment(stockKey); // 恢复库存
                throw new RuntimeException("商品不存在");
            }
            // 步骤5:创建秒杀订单(MySQL)
            SeckillOrder order = new SeckillOrder();
            order.setUserId(userId);
            order.setGoodsId(goodsId);
            int orderResult = orderMapper.insert(order);
            if (orderResult == 0) {
                redisTemplate.opsForValue().increment(stockKey); // 恢复库存
                throw new RuntimeException("创建订单失败,可能已重复秒杀");
            }
            // 步骤6:MySQL乐观锁减库存(兜底防超卖:Redis库存可能因异常不一致,MySQL再校验)
            int stockResult = goodsMapper.reduceStockWithVersion(goodsId, 1, goods.getVersion());
            if (stockResult == 0) {
                // 乐观锁减库存失败(说明Redis库存和MySQL不一致),抛出异常回滚订单
                throw new RuntimeException("库存不足,秒杀失败");
            }
            // 步骤7:记录用户购买记录(Redis Set),后续请求直接拦截
            redisTemplate.opsForSet().add(userKey, userId.toString());
            System.out.println("秒杀成功:用户ID=" + userId + ",商品ID=" + goodsId);
            return true;
        } catch (Exception e) {
            // 出现任何异常,恢复Redis库存(避免Redis库存为负)
            redisTemplate.opsForValue().increment(stockKey);
            throw new RuntimeException("秒杀失败:" + e.getMessage());
        }
    }
}

核心逻辑解释

  1. 预加载商品@PostConstruct在项目启动时将商品存入 Redis,秒杀时直接读 Redis,不查 MySQL;
  2. 原子减库存decrement是 Redis 原子操作,10 万并发下也不会超卖(不会出现多个线程同时减到负数);
  3. 防重复购买Redis Set 存已秒杀用户 IDisMember判断是否已存在,比查 MySQL 快 100 倍;
  4. 乐观锁兜底即使 Redis 库存出问题,MySQL 的reduceStockWithVersion会校验版本号,确保库存不超卖
  5. 异常恢复库存:任何步骤失败都要恢复 Redis 库存,避免 “减了库存但没创建订单” 导致库存不准。

八、步骤 6:AOP 限流(防止恶意请求压垮系统)

        秒杀时可能有恶意用户每秒发 100 次请求,用 AOP 实现 “滑动窗口限流”,限制每秒最大请求数,包名:com.lh.seckill.aop

8.1 自定义限流注解(RateLimit.java)

package com.lh.seckill.aop;
import java.lang.annotation.*;
/**
 * 自定义限流注解:标注在方法上,控制每秒最大请求数
 */
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时生效
@Target(ElementType.METHOD) // 注解只能用在方法上
@Documented
public @interface RateLimit {
    int max() default 10; // 时间窗口内最大请求数(默认10次)
    int window() default 1; // 时间窗口(默认1秒)
}

8.2 限流切面(RateLimitAspect.java)

package com.lh.seckill.aop;
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 java.util.concurrent.TimeUnit;
@Aspect // 标记为AOP切面
@Component // 交给Spring管理
public class RateLimitAspect {
    @Autowired
    private RedisTemplate redisTemplate;
    // Redis Key前缀:区分不同方法的限流
    private static final String RATE_LIMIT_KEY = "rate:limit:";
    // 切入点:拦截所有加了@RateLimit注解的方法
    @Pointcut("@annotation(rateLimit)")
    public void rateLimitPointcut(RateLimit rateLimit) {}
    /**
     * 环绕通知:在方法执行前后实现限流逻辑
     */
    @Around("rateLimitPointcut(rateLimit)")
    public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable {
        // 1. 生成限流Key:方法名作为Key(区分不同接口)
        String methodName = joinPoint.getSignature().getName();
        String limitKey = RATE_LIMIT_KEY + methodName;
        // 2. 获取当前时间戳(毫秒)
        long currentTime = System.currentTimeMillis();
        // 3. 滑动窗口限流核心:移除时间窗口外的请求(如窗口1秒,移除1秒前的请求)
        long windowStartTime = currentTime - rateLimit.window() * 1000;
        redisTemplate.opsForZSet().removeRangeByScore(limitKey, 0, windowStartTime);
        // 4. 统计当前窗口内的请求数
        long requestCount = redisTemplate.opsForZSet().zCard(limitKey);
        // 5. 请求数超过max,拒绝请求
        if (requestCount >= rateLimit.max()) {
            throw new RuntimeException("请求过于频繁,请" + rateLimit.window() + "秒后再试");
        }
        // 6. 将当前请求加入ZSet:value=时间戳字符串,score=时间戳(用于排序和移除)
        redisTemplate.opsForZSet().add(limitKey, String.valueOf(currentTime), currentTime);
        // 7. 设置ZSet过期时间:窗口时间+1秒,避免内存泄漏
        redisTemplate.expire(limitKey, rateLimit.window() + 1, TimeUnit.SECONDS);
        // 8. 执行原方法(秒杀接口)
        return joinPoint.proceed();
    }
}

限流逻辑解释

  • 滑动窗口:用 Redis ZSet 存储请求时间戳,每个请求对应一个 ZSet 元素,score 是时间戳;
  • 移除过期请求:每次请求前,移除窗口外的元素(如 1 秒窗口,移除 1 秒前的请求);
  • 统计请求数zCard统计当前窗口内的请求数,超过max则拒绝;
  • 内存优化:设置 ZSet 过期时间,避免长期存储无用数据。

九、步骤 7:控制层(秒杀接口暴露)

编写 HTTP 接口,接收用户 ID 和商品 ID,调用秒杀服务,包名:com.lh.seckill.controller

package com.lh.seckill.controller;
import com.lh.seckill.aop.RateLimit;
import com.lh.seckill.service.SeckillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/seckill") // 接口统一前缀
public class SeckillController {
    @Autowired
    private SeckillService seckillService;
    /**
     * 秒杀接口:@RateLimit(max=5, window=1)表示每秒最多5次请求
     * @param goodsId 商品ID(路径参数)
     * @param userId 用户ID(请求参数,实际项目从登录态获取,这里简化)
     * @return 响应结果
     */
    @PostMapping("/{goodsId}")
    @RateLimit(max = 5, window = 1) // 限流:每秒最多5次请求
    public ResponseEntity seckill(
            @PathVariable Long goodsId,
            @RequestParam Long userId
    ) {
        try {
            boolean result = seckillService.seckill(userId, goodsId);
            return ResponseEntity.ok("秒杀成功!");
        } catch (Exception e) {
            // 捕获异常,返回友好提示(如“库存不足”“重复购买”)
            return ResponseEntity.badRequest().body(e.getMessage());
        }
    }
}

接口说明

  • 实际项目中,userId不应从请求参数获取,而是从登录态(如 Token)解析,避免用户伪造 ID;
  • ResponseEntitySpring 提供的响应封装,方便返回 HTTP 状态码(200 = 成功,400 = 失败)。

十、步骤 8:测试验证(确保秒杀功能正常)

10.1 启动项目

  1. 启动 Redis 服务(本地启动:redis-server.exe redis.windows.conf);
  2. 启动 MySQL,确保seckill_db库和表已创建,测试数据已插入;
  3. 启动 Spring Boot 项目,控制台会打印 “预加载商品到 Redis” 的日志。

10.2 测试秒杀接口

        用 Postman 或 curl 发送 POST 请求,测试不同场景:

1. 秒杀未开始(如 9:59 请求 10:00 开始的商品)
  • 请求 URL:http://localhost:8080/seckill/1?userId=1001
  • 响应:秒杀未开始或已结束
2. 秒杀正常(10:00-11:00 之间请求)
  • 请求 URL:http://localhost:8080/seckill/1?userId=1001
  • 响应:秒杀成功!
  • 验证:
    • Redis:get seckill:stock:1 → 库存从 100 变为 99;
    • MySQL:select * from seckill_order where user_id=1001 and goods_id=1 → 有一条订单记录;
    • MySQL:select count from seckill_goods where id=1 → 库存从 100 变为 99。
3. 重复秒杀(同一用户再次请求)
  • 请求 URL:http://localhost:8080/seckill/1?userId=1001
  • 响应:您已秒杀过该商品,不能重复购买
4. 库存不足(100 件被抢完后请求)
  • 请求 URL:http://localhost:8080/seckill/1?userId=2001
  • 响应:库存不足,秒杀失败
5. 请求频繁(每秒发送 6 次请求)
  • 响应:请求过于频繁,请1秒后再试

十一、核心要点总结(初学者必记)

  1. 防超卖:Redis 原子减库存(第一道防线)+ MySQL 乐观锁(第二道防线);
  2. 防重复:Redis Set 存已秒杀用户 ID(快)+ MySQL 唯一索引(兜底);
  3. 高并发:商品预加载到 Redis(减少 MySQL 读压力)+ AOP 限流(保护接口);
  4. 定时控制:Redis 缓存商品时间,判断当前是否在秒杀窗口内;
  5. 异常处理:任何步骤失败都要恢复 Redis 库存,避免数据不一致。

十二、实际优化方向(生产环境需补充)

  1. Redis 集群:单机 Redis 扛不住 10 万并发,需部署 Redis Cluster(3 主 3 从);
  2. 消息队列:秒杀成功后,用 RabbitMQ 异步创建订单(避免 MySQL 压力过大);
  3. 分布式锁:如果是多服务部署,需用 Redis 分布式锁(setIfAbsent)替代本地锁;
  4. 用户认证userId从 Token 解析(如 JWT),避免伪造;
  5. 监控告警:监控 Redis 库存、MySQL 订单量、接口 QPS,异常时告警。

通过以上步骤,我们可以完整实现一个 “能扛高并发、无超卖、无重复” 的定时秒杀功能,同时理解每个环节的设计思路,为后续复杂项目打下基础

posted on 2025-11-04 11:09  wgwyanfs  阅读(1)  评论(0)    收藏  举报

导航