蜜雪冰城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库存、锁竞争、秒杀成功率的监控,异常时及时告警。
相关推荐
雨中飘荡的记忆41 分钟前
Redis_实战指南
数据库·redis·缓存
WZTTMoon1 小时前
Spring Boot 4.0 迁移核心注意点总结
java·spring boot·后端
寻kiki1 小时前
scala 函数类?
后端
疯狂的程序猴1 小时前
iOS App 混淆的真实世界指南,从构建到成品 IPA 的安全链路重塑
后端
bcbnb1 小时前
iOS 性能测试的工程化方法,构建从底层诊断到真机监控的多工具测试体系
后端
开心就好20251 小时前
iOS 上架 TestFlight 的真实流程复盘 从构建、上传到审核的团队协作方式
后端
小周在成长1 小时前
Java 泛型支持的类型
后端
aiopencode1 小时前
Charles 抓不到包怎么办?HTTPS 抓包失败、TCP 数据流异常与底层补抓方案全解析
后端
稚辉君.MCA_P8_Java2 小时前
Gemini永久会员 C++返回最长有效子串长度
开发语言·数据结构·c++·后端·算法