蜜雪冰城1分钱奶茶秒杀活动下,使用分片锁替代分布式锁去做秒杀系统

需求复述

基于 spring-boot-starter-data-redis 和 Redisson 实现分片锁(分布式锁的分片优化,避免单锁竞争),打造一个通用的秒杀组件,支持秒杀任意数量(n种)的商品,每种商品可配置指定库存(m件),核心要解决秒杀场景的超卖、重复下单、高并发锁竞争问题。

一、完整方案设计

1. 核心问题与解决方案

秒杀核心问题 解决方案
超卖 Redis + Lua 脚本(原子性执行库存检查、扣减)
重复下单 Redis Set 存储已下单用户ID(商品维度),Lua脚本中校验
分布式锁竞争激烈 分片锁(按商品ID哈希分片,不同商品的锁落在不同"分片",降低单锁竞争)
锁超时/死锁 Redisson 可重入锁 + 自动过期时间 + finally 保证解锁
接口高并发 (可选)接口层限流(如Guava RateLimiter),减轻后端压力

2. 技术选型

  • 基础框架:Spring Boot 2.x+
  • 分布式锁:Redisson(支持分片锁、可重入锁、自动续期)
  • 缓存/库存存储:Redis(String存库存、Set存已下单用户)
  • 原子操作:Lua脚本(保证库存扣减+用户记录的原子性)
  • 依赖:spring-boot-starter-data-redisredisson-spring-boot-starter

3. 核心流程

css 复制代码
graph TD
    A[用户秒杀请求] --> B[接口层限流(可选)]
    B --> C[参数校验(商品ID/用户ID非空)]
    C --> D[按商品ID获取分片锁(Redisson RLock)]
    D --> E{加锁成功?}
    E -- 否 --> F[返回"秒杀太火爆,请重试"]
    E -- 是 --> G[执行Lua脚本(校验库存+校验重复下单+扣减库存+记录用户)]
    G --> H{Lua执行结果?}
    H -- 失败(库存不足/重复下单) --> I[解锁+返回秒杀失败]
    H -- 成功 --> J[生成订单(模拟,实际可落库)]
    J --> K[解锁]
    K --> L[返回秒杀成功]

4. 分片锁设计思路

  • 分片锁核心:将锁按「商品ID」哈希到不同的锁key(分片),例如:seckill:lock:shard:{shardIndex}:{productId}
  • 分片数配置:建议设置为 Redis 节点数或CPU核心数(如8/16),平衡锁竞争和复杂度
  • 优势:避免所有商品的秒杀请求竞争同一把锁,提升并发能力

二、完整代码实现

1. 依赖配置(pom.xml)

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.15</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>seckill-redisson</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>seckill-redisson</name>
    <description>秒杀通用组件(Redisson分片锁)</description>
    
    <dependencies>
        <!-- Spring Boot Web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Redis Starter -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- Redisson Starter -->
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>3.23.3</version>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

2. 配置文件(application.yml)

yaml 复制代码
spring:
  # Redis配置
  redis:
    host: 127.0.0.1
    port: 6379
    password:  # 无密码则留空
    database: 0
    timeout: 10000ms
  # 应用配置
  application:
    name: seckill-redisson

# Redisson配置
redisson:
  address: redis://127.0.0.1:6379
  password:  # 无密码则留空
  database: 0

# 秒杀组件配置
seckill:
  lock:
    # 分片数(建议8/16)
    shard-count: 8
    # 锁超时时间(秒)
    lock-timeout: 30
    # 锁等待时间(秒)
    lock-wait-time: 1
  # 库存初始化(示例:商品1001库存100件,商品1002库存200件)
  init-stock:
    1001: 100
    1002: 200

3. Redisson配置类

kotlin 复制代码
package com.example.seckillredisson.config;

import lombok.Data;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Redisson配置类
 */
@Configuration
@Data
public class RedissonConfig {

    @Value("${redisson.address}")
    private String address;

    @Value("${redisson.password:}")
    private String password;

    @Value("${redisson.database:0}")
    private int database;

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        // 单机模式(集群模式可修改为config.useClusterServers())
        config.useSingleServer()
                .setAddress(address)
                .setPassword(password.isEmpty() ? null : password)
                .setDatabase(database)
                .setConnectionPoolSize(64)
                .setConnectionMinimumIdleSize(16);
        return Redisson.create(config);
    }
}

4. 自定义异常

scala 复制代码
package com.example.seckillredisson.exception;

/**
 * 秒杀自定义异常
 */
public class SeckillException extends RuntimeException {

    public SeckillException(String message) {
        super(message);
    }

    public SeckillException(String message, Throwable cause) {
        super(message, cause);
    }

    // 秒杀异常枚举
    public enum SeckillError {
        PRODUCT_NOT_EXIST("商品不存在"),
        STOCK_INSUFFICIENT("库存不足"),
        REPEAT_SECKILL("请勿重复秒杀"),
        LOCK_ACQUIRE_FAILED("秒杀太火爆,请稍后重试"),
        SYSTEM_ERROR("系统异常");

        private final String message;

        SeckillError(String message) {
            this.message = message;
        }

        public String getMessage() {
            return message;
        }
    }
}

5. 秒杀通用组件(核心)

java 复制代码
package com.example.seckillredisson.service;

import com.example.seckillredisson.exception.SeckillException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
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.Service;

import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 秒杀通用组件(基于Redisson分片锁)
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class SeckillService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final RedissonClient redissonClient;

    // 分片数
    @Value("${seckill.lock.shard-count:8}")
    private int shardCount;

    // 锁超时时间(秒)
    @Value("${seckill.lock.lock-timeout:30}")
    private long lockTimeout;

    // 锁等待时间(秒)
    @Value("${seckill.lock.lock-wait-time:1}")
    private long lockWaitTime;

    // 初始化库存配置
    @Value("#{${seckill.init-stock}}")
    private Map<String, Integer> initStockMap;

    // Redis Key前缀
    private static final String STOCK_KEY_PREFIX = "seckill:stock:";       // 库存Key:seckill:stock:{productId}
    private static final String USER_KEY_PREFIX = "seckill:user:";         // 已下单用户Key:seckill:user:{productId}
    private static final String LOCK_KEY_PREFIX = "seckill:lock:shard:";   // 分片锁Key:seckill:lock:shard:{shardIndex}:{productId}

    // Lua脚本:原子性执行「校验库存+校验重复下单+扣减库存+记录用户」
    private static final String SECKILL_LUA_SCRIPT = """
            -- 参数:KEYS[1]=库存Key, KEYS[2]=用户Key, ARGV[1]=用户ID
            -- 1. 校验库存
            local stock = tonumber(redis.call('get', KEYS[1]))
            if not stock or stock <= 0 then
                return 0  -- 库存不足
            end
            -- 2. 校验是否重复下单
            local isExist = redis.call('sismember', KEYS[2], ARGV[1])
            if isExist == 1 then
                return 2  -- 重复下单
            end
            -- 3. 扣减库存
            redis.call('decr', KEYS[1])
            -- 4. 记录用户
            redis.call('sadd', KEYS[2], ARGV[1])
            return 1  -- 秒杀成功
            """;

    private final DefaultRedisScript<Long> seckillLuaScript = new DefaultRedisScript<>();

    /**
     * 初始化Lua脚本和库存
     */
    @PostConstruct
    public void init() {
        // 配置Lua脚本
        seckillLuaScript.setScriptText(SECKILL_LUA_SCRIPT);
        seckillLuaScript.setResultType(Long.class);
        // 初始化商品库存
        initStockMap.forEach((productId, stock) -> {
            String stockKey = STOCK_KEY_PREFIX + productId;
            if (redisTemplate.opsForValue().get(stockKey) == null) {
                redisTemplate.opsForValue().set(stockKey, stock);
                log.info("初始化商品[{}]库存:{}件", productId, stock);
            }
        });
    }

    /**
     * 通用秒杀方法
     * @param productId 商品ID
     * @param userId    用户ID
     * @return 秒杀结果
     */
    public boolean seckill(String productId, String userId) {
        // 1. 参数校验
        if (productId == null || productId.isEmpty() || userId == null || userId.isEmpty()) {
            throw new SeckillException(SeckillException.SeckillError.PRODUCT_NOT_EXIST.getMessage());
        }

        // 2. 获取分片锁(按商品ID哈希计算分片索引)
        int shardIndex = Math.abs(productId.hashCode()) % shardCount;
        String lockKey = LOCK_KEY_PREFIX + shardIndex + ":" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        boolean lockAcquired = false;

        try {
            // 尝试获取锁(等待时间lockWaitTime秒,超时时间lockTimeout秒)
            lockAcquired = lock.tryLock(lockWaitTime, lockTimeout, TimeUnit.SECONDS);
            if (!lockAcquired) {
                log.warn("用户[{}]秒杀商品[{}]:获取锁失败", userId, productId);
                throw new SeckillException(SeckillException.SeckillError.LOCK_ACQUIRE_FAILED.getMessage());
            }

            // 3. 执行Lua脚本(原子操作)
            String stockKey = STOCK_KEY_PREFIX + productId;
            String userKey = USER_KEY_PREFIX + productId;
            Long result = redisTemplate.execute(
                    seckillLuaScript,
                    Collections.singletonList(stockKey),  // KEYS[1]
                    userKey,                               // KEYS[2]
                    userId                                 // ARGV[1]
            );

            // 4. 处理Lua执行结果
            if (result == null) {
                throw new SeckillException(SeckillException.SeckillError.SYSTEM_ERROR.getMessage());
            }
            switch (result.intValue()) {
                case 1:
                    log.info("用户[{}]秒杀商品[{}]:成功", userId, productId);
                    // 此处可扩展:生成订单、发送消息等(实际业务需落库,可异步)
                    return true;
                case 0:
                    log.warn("用户[{}]秒杀商品[{}]:库存不足", userId, productId);
                    throw new SeckillException(SeckillException.SeckillError.STOCK_INSUFFICIENT.getMessage());
                case 2:
                    log.warn("用户[{}]秒杀商品[{}]:重复下单", userId, productId);
                    throw new SeckillException(SeckillException.SeckillError.REPEAT_SECKILL.getMessage());
                default:
                    throw new SeckillException(SeckillException.SeckillError.SYSTEM_ERROR.getMessage());
            }
        } catch (SeckillException e) {
            throw e;
        } catch (Exception e) {
            log.error("用户[{}]秒杀商品[{}]异常", userId, productId, e);
            throw new SeckillException(SeckillException.SeckillError.SYSTEM_ERROR.getMessage(), e);
        } finally {
            // 保证锁释放(仅当获取锁成功时)
            if (lockAcquired && lock.isHeldByCurrentThread()) {
                lock.unlock();
                log.debug("释放商品[{}]的分片锁[{}]", productId, lockKey);
            }
        }
    }

    /**
     * 查询商品剩余库存
     * @param productId 商品ID
     * @return 剩余库存
     */
    public Integer getStock(String productId) {
        String stockKey = STOCK_KEY_PREFIX + productId;
        Object stockObj = redisTemplate.opsForValue().get(stockKey);
        return stockObj == null ? 0 : Integer.parseInt(stockObj.toString());
    }

    /**
     * 重置商品库存(测试用)
     * @param productId 商品ID
     * @param stock     库存数
     */
    public void resetStock(String productId, Integer stock) {
        String stockKey = STOCK_KEY_PREFIX + productId;
        String userKey = USER_KEY_PREFIX + productId;
        redisTemplate.opsForValue().set(stockKey, stock);
        redisTemplate.delete(userKey); // 清空已下单用户
        log.info("重置商品[{}]库存为:{}件", productId, stock);
    }
}

6. 控制层(测试接口)

typescript 复制代码
package com.example.seckillredisson.controller;

import com.example.seckillredisson.exception.SeckillException;
import com.example.seckillredisson.service.SeckillService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.Map;

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

    private final SeckillService seckillService;

    /**
     * 秒杀接口
     * @param productId 商品ID
     * @param userId    用户ID
     * @return 秒杀结果
     */
    @PostMapping("/{productId}/{userId}")
    public ResponseEntity<Map<String, Object>> seckill(
            @PathVariable String productId,
            @PathVariable String userId) {
        Map<String, Object> result = new HashMap<>();
        try {
            boolean success = seckillService.seckill(productId, userId);
            result.put("code", 200);
            result.put("message", success ? "秒杀成功" : "秒杀失败");
            result.put("data", success);
            return ResponseEntity.ok(result);
        } catch (SeckillException e) {
            result.put("code", 500);
            result.put("message", e.getMessage());
            result.put("data", false);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(result);
        }
    }

    /**
     * 查询商品库存
     * @param productId 商品ID
     * @return 剩余库存
     */
    @GetMapping("/stock/{productId}")
    public ResponseEntity<Map<String, Object>> getStock(@PathVariable String productId) {
        Map<String, Object> result = new HashMap<>();
        Integer stock = seckillService.getStock(productId);
        result.put("code", 200);
        result.put("message", "查询成功");
        result.put("data", stock);
        return ResponseEntity.ok(result);
    }

    /**
     * 重置商品库存(测试用)
     * @param productId 商品ID
     * @param stock     库存数
     * @return 重置结果
     */
    @PostMapping("/reset/{productId}/{stock}")
    public ResponseEntity<Map<String, Object>> resetStock(
            @PathVariable String productId,
            @PathVariable Integer stock) {
        Map<String, Object> result = new HashMap<>();
        seckillService.resetStock(productId, stock);
        result.put("code", 200);
        result.put("message", "重置库存成功");
        result.put("data", stock);
        return ResponseEntity.ok(result);
    }
}

7. 启动类

typescript 复制代码
package com.example.seckillredisson;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SeckillRedissonApplication {

    public static void main(String[] args) {
        SpringApplication.run(SeckillRedissonApplication.class, args);
    }

}

三、测试说明

1. 环境准备

  • 本地启动Redis(默认端口6379,无密码)
  • 启动Spring Boot应用

2. 接口测试(Postman/Curl)

(1) 查询商品库存
css 复制代码
curl http://localhost:8080/seckill/stock/1001
# 响应示例:{"code":200,"message":"查询成功","data":100}
(2) 秒杀商品
shell 复制代码
curl -X POST http://localhost:8080/seckill/1001/user001
# 成功响应:{"code":200,"message":"秒杀成功","data":true}
# 重复下单响应:{"code":500,"message":"请勿重复秒杀","data":false}
# 库存不足响应:{"code":500,"message":"库存不足","data":false}
(3) 重置库存
css 复制代码
curl -X POST http://localhost:8080/seckill/reset/1001/100
# 响应示例:{"code":200,"message":"重置库存成功","data":100}

四、总结

核心关键点回顾

  1. 分片锁优化:按商品ID哈希分片,避免所有秒杀请求竞争同一把锁,提升并发能力;Redisson锁保证分布式环境下的锁安全性,自动过期+finally解锁避免死锁。
  2. 原子性保障:Lua脚本将「库存校验、扣减、用户记录」封装为原子操作,彻底解决超卖和重复下单问题。
  3. 通用组件设计 :通过配置化库存、标准化接口,支持任意数量商品的秒杀,只需修改application.yml中的seckill.init-stock即可扩展商品。

扩展建议

  • 高并发优化:接口层增加Guava RateLimiter限流,避免请求压垮后端;
  • 订单落库:秒杀成功后异步将订单写入数据库(如RabbitMQ),提升响应速度;
  • 集群适配:Redisson配置改为集群模式(config.useClusterServers()),适配Redis集群;
  • 监控告警:增加Redis库存、锁竞争、秒杀成功率的监控,异常时及时告警。
相关推荐
万象.几秒前
redis通用命令与数据结构
数据结构·数据库·redis
IT_陈寒10 分钟前
Vite 5大实战优化技巧:让你的开发效率提升200%|2025前端工程化指南
前端·人工智能·后端
Victor35627 分钟前
Hibernate(20)Hibernate如何处理继承关系?
后端
Victor35628 分钟前
Hibernate(19)什么是Hibernate的急加载?
后端
予枫的编程笔记29 分钟前
【Java 进阶3】Kafka从入门到实战:全面解析分布式消息队列的核心与应用
java·分布式·kafka
chenyuhao20242 小时前
Linux网络编程:传输层协议UDP
linux·服务器·网络·后端·udp
嘟嘟MD9 小时前
程序员副业 | 2025年12月复盘
后端·创业
..过云雨11 小时前
17-2.【Linux系统编程】线程同步详解 - 条件变量的理解及应用
linux·c++·人工智能·后端
南山乐只11 小时前
【Spring AI 开发指南】ChatClient 基础、原理与实战案例
人工智能·后端·spring ai
Miqiuha11 小时前
生成唯一id
分布式