需求复述
基于 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-redis、redisson-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}
四、总结
核心关键点回顾
- 分片锁优化:按商品ID哈希分片,避免所有秒杀请求竞争同一把锁,提升并发能力;Redisson锁保证分布式环境下的锁安全性,自动过期+finally解锁避免死锁。
- 原子性保障:Lua脚本将「库存校验、扣减、用户记录」封装为原子操作,彻底解决超卖和重复下单问题。
- 通用组件设计 :通过配置化库存、标准化接口,支持任意数量商品的秒杀,只需修改
application.yml中的seckill.init-stock即可扩展商品。
扩展建议
- 高并发优化:接口层增加Guava RateLimiter限流,避免请求压垮后端;
- 订单落库:秒杀成功后异步将订单写入数据库(如RabbitMQ),提升响应速度;
- 集群适配:Redisson配置改为集群模式(
config.useClusterServers()),适配Redis集群; - 监控告警:增加Redis库存、锁竞争、秒杀成功率的监控,异常时及时告警。