分享万人级秒杀系统,如何设计高并发、防超卖、低成本落地方案
目录
二、核心问题解决方案(完整Java代码,防超卖+高并发+低成本)
1. 超卖问题(秒杀核心痛点,三种Java方案兜底,兼顾性能与成本)
解决方案一:Redis原子操作(首选,高性能+低成本,开源Redis即可支撑)
2.2 分层过滤(逐级拦截无效请求,最小化资源消耗,低成本核心)
3.1 多级缓存策略(本地缓存+Redis,降低分布式缓存成本,提升性能)
4.1 完整秒杀流程(全链路Java实现,层层优化,成本可控)
4.2 库存同步与对账方案(低成本兜底,保证数据一致性,避免损失)
1. 限流降级策略(开源组件,零额外成本,保护系统不被压垮)
2. 熔断降级(Resilience4j,开源免费,低成本,避免服务级联崩溃)
1. 关键监控指标(全链路监控,掌握系统状态,降低故障排查成本)
2. 监控实现(基于Micrometer+Prometheus+Grafana,低成本落地)
1. 弹性扩展策略(按需扩容,兼顾性能与成本,核心原则:「峰谷适配」)
✨ 作为Java后端高频面试天花板、电商核心场景的秒杀系统,不仅要搞定万人同时抢购的高并发,还要解决超卖、性能瓶颈,更要兼顾企业级低成本落地!今天这份全攻略,从架构设计到完整Java代码,从性能优化到成本控制,手把手教你搭建可落地、可扩展、低成本的秒杀系统,收藏这篇就够了!
一、系统架构设计(分层削峰,低成本起步,高扩展迭代)
1. 分层架构(层层拦截,逐级减负,最小化资源消耗)
客户端层 → 接入层 → 业务服务层 → 数据层 ↓ ↓ ↓ ↓ 前端防刷 缓存限流 队列异步 分库分表(按需启用)
设计亮点:低成本优先,非核心环节不投入昂贵资源,比如接入层优先用Nginx(开源免费),而非商业网关;缓存优先用Redis Cluster(开源),按需扩容节点,避免初期过度部署。
2. 具体组件(各司其职,开源为主,成本可控)
| 层级 | 组件选择 | 核心作用 | 成本优化点 |
|---|---|---|---|
| 客户端 | 静态资源CDN(阿里云/腾讯云按量付费)、前端原生校验 | 减轻后端压力,拦截无效请求 | 静态资源提前预热到边缘节点,按量付费避免闲置成本 |
| 接入层 | Nginx+Lua/OpenResty(开源)、Sentinel(开源) | 第一层限流、静态缓存、请求转发、恶意拦截 | 单台Nginx可支撑10万+QPS,初期无需集群,按需扩容 |
| 业务层 | 秒杀服务集群(Spring Boot/Spring Cloud)、RocketMQ(开源)、Redis Cluster(开源)、Caffeine(开源) | 核心业务处理、流量削峰、库存原子操作、热点缓存 | 服务无状态化,初期单节点部署,峰值时临时扩容;MQ单机模式起步,高并发时再集群 |
| 数据层 | MySQL主从(开源)、MyCat(开源,分库分表按需启用) | 数据持久化、读写分离、缓解单库压力 | 初期无需分库分表,主从架构满足需求;只读节点按需添加,降低主库压力同时控制成本 |
成本核心原则:按需部署,弹性伸缩,初期用最小资源满足业务需求,峰值时临时扩容,峰值过后缩容,避免闲置资源浪费。
二、核心问题解决方案(完整Java代码,防超卖+高并发+低成本)
1. 超卖问题(秒杀核心痛点,三种Java方案兜底,兼顾性能与成本)
超卖的本质是并发场景下库存操作的原子性无法保证,以下三种方案从高性能到兜底,既杜绝超卖,又最大化控制部署成本。
解决方案一:Redis原子操作(首选,高性能+低成本,开源Redis即可支撑)
利用Redis单线程特性,通过Lua脚本打包「查询库存+扣减库存+库存下限判断」操作,保证原子性,杜绝并发超卖。相比Redis分布式锁,Lua脚本无需额外维护锁资源,更轻量、更低成本(减少Redis交互次数,降低资源消耗)。
@Component
public class RedisStockService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 定义Lua脚本,保证库存扣减原子性
private static final String STOCK_DEDUCT_LUA_SCRIPT = """
local stockKey = KEYS[1]
local deductCount = tonumber(ARGV[1])
-- 获取当前库存
local currentStock = tonumber(redis.call('GET', stockKey))
if not currentStock or currentStock < deductCount then
-- 库存不足,返回0
return 0
end
-- 库存充足,扣减库存
redis.call('DECRBY', stockKey, deductCount)
-- 返回扣减后的库存
return tonumber(redis.call('GET', stockKey))
""";
/**
* 原子扣减Redis库存
* @param productId 商品ID
* @param deductCount 扣减数量(默认1)
* @return 扣减结果:true-成功,false-失败
*/
public boolean deductStock(String productId, int deductCount) {
if (deductCount <= 0) {
return false;
}
String stockKey = "seckill:stock:" + productId;
// 执行Lua脚本
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(STOCK_DEDUCT_LUA_SCRIPT);
redisScript.setResultType(Long.class);
// 传入参数:KEYS[1] = 库存key,ARGV[1] = 扣减数量
Long result = stringRedisTemplate.execute(
redisScript,
Collections.singletonList(stockKey),
String.valueOf(deductCount)
);
// result >= 0 说明扣减成功(库存充足),result == null 说明库存key不存在
return result != null && result >= 0;
}
/**
* 初始化商品Redis库存(缓存预热使用,低成本初始化)
*/
public boolean initStock(String productId, int totalStock) {
if (totalStock < 0) {
return false;
}
String stockKey = "seckill:stock:" + productId;
// setnx保证只有一个线程初始化库存,避免重复写入,降低Redis资源消耗
Boolean setIfAbsent = stringRedisTemplate.opsForValue().setIfAbsent(
stockKey,
String.valueOf(totalStock),
24,
TimeUnit.HOURS
);
// 若库存已存在,返回true(无需重复初始化)
return setIfAbsent != null && (setIfAbsent || stringRedisTemplate.hasKey(stockKey));
}
}
解决方案二:数据库乐观锁(最终一致性兜底,无额外部署成本)
无需额外引入中间件,基于现有MySQL即可实现,作为Redis方案的兜底,保证数据最终一致性,零额外部署成本,适合低并发场景或小型电商秒杀。
@Service
@Transactional(rollbackFor = Exception.class)
public class DbStockService {
@Autowired
private SeckillProductMapper seckillProductMapper;
/**
* 乐观锁扣减数据库库存
* @param productId 商品ID
* @param deductCount 扣减数量
* @return 扣减结果:true-成功,false-失败
*/
public boolean deductStockByOptimisticLock(String productId, int deductCount) {
if (deductCount <= 0) {
return false;
}
// 1. 查询商品当前信息(获取版本号和库存)
SeckillProduct product = seckillProductMapper.selectById(productId);
if (product == null || product.getStock() < deductCount || product.getStatus() != 1) {
return false;
}
// 2. 乐观锁更新库存(版本号匹配才更新)
int affectedRows = seckillProductMapper.updateStockByOptimisticLock(
productId,
deductCount,
product.getVersion(),
product.getStock()
);
// 3. 受影响行数>0说明更新成功
return affectedRows > 0;
}
}
对应的Mapper XML(MyBatis):
<update id="updateStockByOptimisticLock">
UPDATE t_seckill_product
SET stock = stock - #{deductCount},
version = version + 1,
update_time = NOW()
WHERE id = #{productId}
AND stock = #{currentStock}
AND version = #{version}
AND status = 1
</update>
解决方案三:预扣库存(高性能+异步同步,兼顾成本与一致性)
先在Redis中预扣库存(高性能),再通过消息队列异步同步到数据库(降低数据库瞬时压力),无需让用户等待数据库操作,既提升用户体验,又减少数据库资源消耗(低成本),是高并发秒杀的主流方案。
@Component
public class PreDeductStockService {
@Autowired
private RedisStockService redisStockService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 秒杀库存同步MQ主题(统一主题,降低MQ集群部署成本)
private static final String SECKILL_STOCK_SYNC_TOPIC = "seckill-stock-sync-topic";
/**
* 预扣Redis库存,异步同步到数据库
* @param productId 商品ID
* @param userId 用户ID
* @return 扣减结果:true-成功,false-失败
*/
public SeckillResult preDeductStock(String productId, String userId) {
// 1. 预扣Redis库存(扣减1件,默认秒杀每人限购1件)
boolean deductSuccess = redisStockService.deductStock(productId, 1);
if (!deductSuccess) {
// 库存不足,标记商品售罄(JVM本地缓存,避免重复访问Redis,降低成本)
markProductSoldOut(productId);
return SeckillResult.fail("商品已售罄,秒杀失败");
}
// 2. 生成秒杀订单前置信息(雪花算法生成唯一订单号,无额外中间件,低成本)
String orderNo = SnowflakeUtil.generateOrderNo();
SeckillStockSyncMessage syncMessage = SeckillStockSyncMessage.builder()
.orderNo(orderNo)
.productId(productId)
.userId(userId)
.deductCount(1)
.createTime(new Date())
.build();
// 3. 发送MQ消息,异步同步到数据库(OneWay模式,无需等待响应,提升性能,降低MQ资源消耗)
try {
rocketMQTemplate.sendOneWay(
SECKILL_STOCK_SYNC_TOPIC,
MessageBuilder.withPayload(JSON.toJSONString(syncMessage)).build()
);
} catch (Exception e) {
// MQ发送失败,回滚Redis库存(避免数据不一致)
redisStockService.deductStock(productId, -1);
return SeckillResult.fail("系统繁忙,请稍后再试");
}
// 4. 返回排队中结果,前端轮询订单状态
return SeckillResult.processing(orderNo, "已进入排队,正在为您生成订单");
}
/**
* JVM本地缓存标记商品售罄(无需访问Redis,降低Redis压力和成本)
*/
private final Map<String, Boolean> soldOutMarkMap = new ConcurrentHashMap<>();
private void markProductSoldOut(String productId) {
soldOutMarkMap.put(productId, true);
}
/**
* 检查商品是否已售罄(优先查本地缓存,低成本高性能)
*/
public boolean isProductSoldOut(String productId) {
return soldOutMarkMap.getOrDefault(productId, false);
}
}
配套MQ消费者(异步同步数据库,削峰填谷,降低数据库压力):
@Component
@RocketMQMessageListener(
topic = "seckill-stock-sync-topic",
consumerGroup = "seckill-stock-sync-consumer-group",
consumeMode = ConsumeMode.ORDERLY, // 按商品ID有序消费,避免同一商品库存更新冲突
messageModel = MessageModel.CLUSTERING
)
public class SeckillStockSyncConsumer implements RocketMQListener<String> {
@Autowired
private DbStockService dbStockService;
@Autowired
private SeckillOrderMapper seckillOrderMapper;
@Slf4j
@Override
public void onMessage(String message) {
try {
// 1. 解析消息
SeckillStockSyncMessage syncMessage = JSON.parseObject(message, SeckillStockSyncMessage.class);
// 2. 乐观锁扣减数据库库存
boolean dbDeductSuccess = dbStockService.deductStockByOptimisticLock(
syncMessage.getProductId(),
syncMessage.getDeductCount()
);
if (!dbDeductSuccess) {
log.error("订单{}同步数据库库存失败,库存不足", syncMessage.getOrderNo());
// 可记录死信队列,后续人工对账,避免数据丢失
return;
}
// 3. 生成正式订单(状态为未支付)
SeckillOrder order = SeckillOrder.builder()
.orderNo(syncMessage.getOrderNo())
.productId(syncMessage.getProductId())
.userId(syncMessage.getUserId())
.payStatus(0) // 0-未支付,1-已支付
.createTime(syncMessage.getCreateTime())
.build();
seckillOrderMapper.insert(order);
log.info("订单{}生成成功,库存同步完成", syncMessage.getOrderNo());
} catch (Exception e) {
log.error("秒杀库存同步失败,消息内容:{}", message, e);
// 抛出异常,让MQ重试(默认重试3次,避免单次异常导致数据不一致)
throw new RuntimeException(e);
}
}
}
2. 高并发请求处理(低成本削峰,避免资源浪费)
2.1 流量削峰(基于开源MQ,零额外成本,高效缓冲)
秒杀的核心痛点是「瞬间海量并发」,直接冲击后端服务会导致资源耗尽、服务崩溃。利用开源RocketMQ做流量缓冲,将「瞬间爆发流量」转化为「平稳串行流量」,后端服务按需消费,既保证服务稳定,又避免资源闲置浪费(低成本核心)。
成本优化点:初期采用RocketMQ单机模式,支撑10万+消息堆积,无需集群部署;峰值时临时扩容Broker节点,峰值过后缩容,降低运维和服务器成本。
完整Java代码已在「预扣库存」方案中体现,补充核心设计思路:
-
用户请求到达后,仅做简单校验,立即异步入队,不阻塞用户请求(提升用户体验)
-
后端消费者按自身处理能力消费消息(可配置消费线程数),避免被压垮
-
采用有序消费(按商品ID分片),避免同一商品库存更新冲突,无需额外分布式锁(降低成本)
2.2 分层过滤(逐级拦截无效请求,最小化资源消耗,低成本核心)
从入口到核心业务,层层过滤无效请求,将百万级请求压缩至后端可承受范围,减少不必要的资源占用,降低服务器、Redis、数据库的负载成本。
@Component
public class SeckillRequestFilterService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private BloomFilter<String> seckillProductBloomFilter;
// 1. 第一层:用户请求频率限制(Redis计数器,开源免费,低成本)
private static final String USER_REQUEST_LIMIT_KEY = "seckill:limit:user:";
private static final int USER_REQUEST_LIMIT = 10; // 每分钟最多10次请求
private static final int LIMIT_EXPIRE_SECONDS = 60;
public boolean checkUserRequestLimit(String userId) {
String limitKey = USER_REQUEST_LIMIT_KEY + userId;
// Redis自增计数器,原子操作,无需额外锁
Long increment = stringRedisTemplate.opsForValue().increment(limitKey);
if (increment == 1) {
// 第一次请求,设置过期时间
stringRedisTemplate.expire(limitKey, LIMIT_EXPIRE_SECONDS, TimeUnit.SECONDS);
}
// 超过限制,返回false
return increment != null && increment <= USER_REQUEST_LIMIT;
}
// 2. 第二层:布隆过滤器快速判断商品是否存在(Caffeine实现,本地缓存,零部署成本)
public boolean checkProductExist(String productId) {
// 布隆过滤器快速判断,误判率可控制,无需访问数据库/Redis,低成本高性能
return seckillProductBloomFilter.contains(productId);
}
// 3. 第三层:商品售罄标记判断(JVM本地缓存,零成本)
@Autowired
private PreDeductStockService preDeductStockService;
public boolean checkProductSoldOut(String productId) {
return !preDeductStockService.isProductSoldOut(productId);
}
// 4. 第四层:商品秒杀状态校验(Redis缓存,避免访问数据库,降低成本)
private static final String PRODUCT_STATUS_KEY = "seckill:product:status:";
public boolean checkProductStatus(String productId) {
String statusKey = PRODUCT_STATUS_KEY + productId;
String status = stringRedisTemplate.opsForValue().get(statusKey);
// 1-秒杀中,0-未开始,2-已结束
return "1".equals(status);
}
/**
* 全链路请求过滤(逐级拦截,返回第一个不通过的结果)
*/
public SeckillResult filterRequest(String userId, String productId) {
// 1. 用户请求频率限制
if (!checkUserRequestLimit(userId)) {
return SeckillResult.fail("请求过于频繁,请1分钟后再试");
}
// 2. 商品是否存在
if (!checkProductExist(productId)) {
return SeckillResult.fail("秒杀商品不存在");
}
// 3. 商品是否已售罄
if (!checkProductSoldOut(productId)) {
return SeckillResult.fail("商品已售罄,秒杀失败");
}
// 4. 商品秒杀是否正在进行
if (!checkProductStatus(productId)) {
return SeckillResult.fail("商品秒杀未开始或已结束");
}
// 过滤通过
return SeckillResult.success("请求合法,进入秒杀流程");
}
}
分层过滤效果(资源消耗逐级递减,成本可控): 所有请求 → 用户频率过滤 → 商品存在过滤 → 售罄状态过滤 → 秒杀状态过滤 → 实际下单 ↓ ↓ ↓ ↓ ↓ ↓ 100万 80万 50万 10万 5万 1万
成本优化点:优先使用本地缓存(Caffeine、JVM Map)和开源中间件(Redis、布隆过滤器),无需商业组件,减少服务器部署数量,降低硬件和运维成本。
3. 系统性能优化(高性能+低成本,鱼和熊掌兼得)
3.1 多级缓存策略(本地缓存+Redis,降低分布式缓存成本,提升性能)
采用「JVM本地缓存(Caffeine)→ Redis集群 → 数据库」的多级缓存架构,优先使用本地缓存,减少Redis访问次数,降低Redis集群压力和部署成本,同时兼顾性能与数据一致性。
@Component
public class SeckillMultiLevelCacheService {
// 一级缓存:JVM本地缓存(Caffeine,开源免费,高性能,零部署成本)
private final Cache<String, SeckillProduct> localCache = Caffeine.newBuilder()
.maximumSize(1000) // 缓存1000个热点商品,足够支撑大部分秒杀场景
.expireAfterWrite(5, TimeUnit.MINUTES) // 5分钟过期,保证数据新鲜度
.build();
// 二级缓存:Redis集群(开源,按需扩容)
@Autowired
private StringRedisTemplate stringRedisTemplate;
private static final String PRODUCT_DETAIL_KEY = "seckill:product:detail:";
// 三级缓存:数据库(最终一致性兜底)
@Autowired
private SeckillProductMapper seckillProductMapper;
/**
* 获取秒杀商品详情(多级缓存查询,优先本地缓存,低成本高性能)
*/
public SeckillProduct getSeckillProductDetail(String productId) {
// 1. 查询一级缓存:JVM本地缓存(无网络开销,高性能,零成本)
SeckillProduct product = localCache.getIfPresent(productId);
if (product != null) {
return product;
}
// 2. 查询二级缓存:Redis(分布式缓存,支撑高并发,低成本)
String productJson = stringRedisTemplate.opsForValue().get(PRODUCT_DETAIL_KEY + productId);
if (productJson != null && !productJson.isEmpty()) {
product = JSON.parseObject(productJson, SeckillProduct.class);
// 写入本地缓存,提升下次查询性能
localCache.put(productId, product);
return product;
}
// 3. 查询三级缓存:数据库(最终兜底,仅当缓存未命中时访问,降低数据库压力)
product = seckillProductMapper.selectById(productId);
if (product == null) {
return null;
}
// 4. 写入二级缓存和一级缓存,预热下次查询
stringRedisTemplate.opsForValue().set(
PRODUCT_DETAIL_KEY + productId,
JSON.toJSONString(product),
1,
TimeUnit.HOURS
);
localCache.put(productId, product);
return product;
}
}
成本优化点:本地缓存承载大部分热点商品查询,减少Redis的网络开销和访问压力,使得Redis集群无需大规模部署,初期单节点即可支撑高并发,降低硬件成本。
3.2 读多写少优化(缓存预热+读写分离,降低数据库成本)
秒杀场景属于典型的「读多写少」,活动开始前提前将热点商品数据加载到缓存,避免活动期间大量请求穿透到数据库;同时采用MySQL主从架构,读写分离,降低主库压力,减少数据库扩容成本。
@Service
public class SeckillCacheWarmUpService {
@Autowired
private SeckillProductMapper seckillProductMapper;
@Autowired
private RedisStockService redisStockService;
@Autowired
private SeckillMultiLevelCacheService multiLevelCacheService;
@Autowired
private BloomFilter<String> seckillProductBloomFilter;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 缓存预热(项目启动后执行,或秒杀活动开始前30分钟定时执行,零人工成本)
@PostConstruct
@Scheduled(cron = "0 30 * * * ?") // 每小时30分执行一次,保证缓存新鲜度
public void warmUpSeckillCache() {
log.info("开始执行秒杀商品缓存预热");
// 1. 查询即将开始或正在进行的秒杀商品(从MySQL从库查询,降低主库压力,低成本)
List<SeckillProduct> seckillProducts = seckillProductMapper.selectSeckillProducts();
if (seckillProducts.isEmpty()) {
log.info("暂无秒杀商品需要缓存预热");
return;
}
// 2. 批量预热缓存(减少单次Redis/数据库交互,降低资源消耗)
for (SeckillProduct product : seckillProducts) {
String productId = product.getId();
// 2.1 预热Redis库存
redisStockService.initStock(productId, product.getStock());
// 2.2 预热商品详情缓存(自动写入Redis和本地缓存)
multiLevelCacheService.getSeckillProductDetail(productId);
// 2.3 预热布隆过滤器(商品ID)
seckillProductBloomFilter.add(productId);
// 2.4 预热商品秒杀状态
stringRedisTemplate.opsForValue().set(
"seckill:product:status:" + productId,
String.valueOf(product.getStatus()),
24,
TimeUnit.HOURS
);
}
log.info("秒杀商品缓存预热完成,共预热{}个商品", seckillProducts.size());
}
}
成本优化点:
从MySQL从库查询商品数据,避免冲击主库,主库仅负责写入操作,无需高频扩容
批量预热缓存,减少Redis和数据库的交互次数,降低网络开销和资源消耗
定时自动预热,无需人工干预,降低运维成本
4. 详细实现方案(完整Java流程,低成本落地)
4.1 完整秒杀流程(全链路Java实现,层层优化,成本可控)
@Service
public class SeckillCoreService {
@Autowired
private SeckillRequestFilterService requestFilterService;
@Autowired
private PreDeductStockService preDeductStockService;
@Autowired
private SeckillMultiLevelCacheService multiLevelCacheService;
@Slf4j
/**
* 完整秒杀处理流程
* @param userId 用户ID
* @param productId 商品ID
* @return 秒杀结果
*/
public SeckillResult processSeckill(String userId, String productId) {
long startTime = System.currentTimeMillis();
try {
// 第一步:全链路请求过滤(逐级拦截无效请求,降低后续资源消耗)
SeckillResult filterResult = requestFilterService.filterRequest(userId, productId);
if (!filterResult.isSuccess()) {
return filterResult;
}
// 第二步:获取商品详情(多级缓存,低成本高性能)
SeckillProduct product = multiLevelCacheService.getSeckillProductDetail(productId);
if (product == null) {
return SeckillResult.fail("秒杀商品不存在");
}
// 第三步:预扣Redis库存,异步同步数据库(核心步骤,防超卖+低成本)
return preDeductStockService.preDeductStock(productId, userId);
} catch (Exception e) {
log.error("秒杀处理异常,userId={}, productId={}", userId, productId, e);
return SeckillResult.fail("系统繁忙,请稍后再试");
} finally {
long costTime = System.currentTimeMillis() - startTime;
// 记录处理耗时(监控用,便于后续优化成本和性能)
log.info("秒杀请求处理完成,userId={}, productId={}, 耗时={}ms", userId, productId, costTime);
}
}
}
配套控制层(轻量实现,避免复杂逻辑,降低资源消耗):
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@Autowired
private SeckillCoreService seckillCoreService;
@Autowired
private SeckillOrderMapper seckillOrderMapper;
// 秒杀请求接口(POST方式,防止GET请求缓存,轻量实现)
@PostMapping("/process")
public SeckillResult processSeckill(
@RequestParam String userId,
@RequestParam String productId) {
// 简单参数校验(避免无效请求进入核心流程)
if (userId == null || userId.isEmpty() || productId == null || productId.isEmpty()) {
return SeckillResult.fail("参数不能为空");
}
return seckillCoreService.processSeckill(userId, productId);
}
// 订单状态查询接口(前端轮询,轻量查询,优先Redis缓存)
@GetMapping("/order/query")
public SeckillResult queryOrderStatus(@RequestParam String orderNo) {
if (orderNo == null || orderNo.isEmpty()) {
return SeckillResult.fail("订单号不能为空");
}
// 优先查询Redis缓存(订单状态,降低数据库压力)
String orderStatus = stringRedisTemplate.opsForValue().get("seckill:order:status:" + orderNo);
if (orderStatus != null) {
return "1".equals(orderStatus) ? SeckillResult.success("秒杀成功,订单已生成,请尽快支付")
: SeckillResult.processing(orderNo, "正在排队中,请稍后再查");
}
// 查询数据库
SeckillOrder order = seckillOrderMapper.selectByOrderNo(orderNo);
if (order == null) {
return SeckillResult.fail("订单不存在或秒杀失败");
}
// 写入Redis缓存,提升下次查询性能
stringRedisTemplate.opsForValue().set(
"seckill:order:status:" + orderNo,
String.valueOf(order.getPayStatus()),
1,
TimeUnit.HOURS
);
return order.getPayStatus() == 1 ? SeckillResult.success("秒杀成功,订单已生成,请尽快支付")
: SeckillResult.processing(orderNo, "订单已生成,请尽快支付");
}
}
4.2 库存同步与对账方案(低成本兜底,保证数据一致性,避免损失)
由于采用异步处理,可能出现Redis库存与数据库库存不一致的情况(如MQ消息丢失、消费者异常),通过定时对账任务兜底,无需额外中间件,基于现有Redis和数据库实现,零额外部署成本,保证数据一致性,避免企业经济损失。
@Component
@Slf4j
public class SeckillStockReconciliationService {
@Autowired
private SeckillProductMapper seckillProductMapper;
@Autowired
private RedisStockService redisStockService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 每5分钟执行一次库存对账(低频率,降低数据库和Redis压力,低成本)
@Scheduled(cron = "0 */5 * * * ?")
public void stockReconciliation() {
log.info("开始执行秒杀库存对账任务");
// 1. 查询所有秒杀商品
List<SeckillProduct> seckillProducts = seckillProductMapper.selectSeckillProducts();
if (seckillProducts.isEmpty()) {
log.info("暂无秒杀商品需要对账");
return;
}
// 2. 逐一对账Redis库存与数据库库存
for (SeckillProduct product : seckillProducts) {
String productId = product.getId();
// 2.1 获取Redis库存
String redisStockStr = stringRedisTemplate.opsForValue().get("seckill:stock:" + productId);
Integer redisStock = redisStockStr == null ? 0 : Integer.parseInt(redisStockStr);
// 2.2 获取数据库库存
Integer dbStock = product.getStock();
// 2.3 对比库存(允许少量偏差,避免频繁修复)
if (Math.abs(redisStock - dbStock) > 0) {
log.warn("库存不一致,productId={}, Redis库存={}, 数据库库存={}",
productId, redisStock, dbStock);
// 3. 库存修复(以数据库库存为准,保证数据最终一致性)
fixStockInconsistency(productId, dbStock);
}
}
log.info("秒杀库存对账任务执行完成");
}
/**
* 修复库存不一致(低成本,无需人工干预)
*/
private void fixStockInconsistency(String productId, Integer dbStock) {
// 3.1 更新Redis库存,与数据库保持一致
stringRedisTemplate.opsForValue().set("seckill:stock:" + productId, String.valueOf(dbStock));
// 3.2 重置售罄标记(若数据库库存>0,清除售罄标记)
if (dbStock > 0) {
preDeductStockService.soldOutMarkMap.remove(productId);
} else {
preDeductStockService.markProductSoldOut(productId);
}
// 3.3 记录对账日志,便于后续追溯
log.info("库存修复完成,productId={}, 修复后库存={}", productId, dbStock);
}
}
三、高可用保障(低成本方案,避免服务崩溃,减少损失)
1. 限流降级策略(开源组件,零额外成本,保护系统不被压垮)
采用「多维度限流+降级兜底」,基于开源Sentinel和Redis实现,无需商业组件,低成本高可用,精准控制流量,避免单一维度限流的局限性,同时在服务异常时快速返回兜底结果,避免资源耗尽。
@RestController
@RequestMapping("/api/seckill")
public class SeckillController {
@Autowired
private SeckillCoreService seckillCoreService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
// 基于Sentinel限流(开源免费,低成本,支持多维度限流)
@GetMapping("/process")
@SentinelResource(value = "seckillProcess", fallback = "seckillFallback")
public SeckillResult processSeckill(
@RequestParam String userId,
@RequestParam String productId) {
if (userId == null || userId.isEmpty() || productId == null || productId.isEmpty()) {
return SeckillResult.fail("参数不能为空");
}
return seckillCoreService.processSeckill(userId, productId);
}
// 限流降级兜底方法(快速返回,避免用户等待,降低服务压力)
public SeckillResult seckillFallback(String userId, String productId, Throwable e) {
log.error("秒杀请求被限流或降级,userId={}, productId={}", userId, productId, e);
// 兜底结果:返回系统繁忙,引导用户稍后重试,无需访问核心服务,低成本
return SeckillResult.fail("当前秒杀人数过多,请稍后再试");
}
// 补充:Redis分布式限流(无中间件依赖,低成本,适合小型系统)
private static final String SECKILL_TOTAL_LIMIT_KEY = "seckill:limit:total:";
private static final int TOTAL_REQUEST_LIMIT = 50000; // 总QPS限制5万
public boolean checkTotalRequestLimit() {
String limitKey = SECKILL_TOTAL_LIMIT_KEY + LocalDate.now().toString();
Long increment = stringRedisTemplate.opsForValue().increment(limitKey);
if (increment == 1) {
stringRedisTemplate.expire(limitKey, 24, TimeUnit.HOURS);
}
return increment != null && increment <= TOTAL_REQUEST_LIMIT;
}
}
限流规则(多维度,低成本,覆盖核心场景):
-
用户维度:每个用户10次/分钟(防止单个用户恶意刷请求,Redis计数器实现)
-
IP维度:每个IP 1000次/分钟(防止同一网段恶意攻击,Sentinel实现)
-
商品维度:每个商品10000次/分钟(匹配商品库存上限,Sentinel实现)
-
总QPS:系统最大承受50000 QPS(根据服务器配置调整,Redis/Sentinel实现)
成本优化点:优先使用Redis计数器实现限流(无额外中间件),大型系统再引入Sentinel,避免初期过度部署,降低运维和服务器成本。
2. 熔断降级(Resilience4j,开源免费,低成本,避免服务级联崩溃)
使用Resilience4j替代Hystrix(轻量、开源、低成本),实现服务熔断降级,当后端服务(如Redis、数据库)出现异常或响应超时,快速返回兜底结果,防止服务被拖垮,零额外部署成本,易于集成。
@Service
public class SeckillCircuitBreakerService {
@Autowired
private RedisStockService redisStockService;
// 配置熔断规则(5秒内超过10次请求失败,熔断5秒,低成本,轻量)
private final CircuitBreaker circuitBreaker = CircuitBreaker.of("seckillStockService",
CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率阈值50%
.waitDurationInOpenState(Duration.ofSeconds(5)) // 熔断状态持续5秒
.slidingWindowSize(10) // 滑动窗口大小10
.build()
);
/**
* 带熔断的Redis库存扣减
*/
public boolean deductStockWithCircuitBreaker(String productId, int deductCount) {
// 执行熔断包装的方法
return Try.ofSupplier(CircuitBreaker.decorateSupplier(circuitBreaker,
() -> redisStockService.deductStock(productId, deductCount)))
.recover(ex -> {
log.error("库存扣减服务熔断,productId={}", productId, ex);
// 熔断兜底:返回false,标记商品售罄,避免重复请求
preDeductStockService.markProductSoldOut(productId);
return false;
})
.get();
}
}
成本优化点:Resilience4j是轻量级组件,无需独立部署,直接集成在Spring Boot项目中,零额外服务器成本,运维简单。
四、监控与告警(开源组件,低成本,及时发现问题)
1. 关键监控指标(全链路监控,掌握系统状态,降低故障排查成本)
分为三个层面,覆盖系统、应用、业务,无需商业监控工具,基于开源组件实现,低成本:
-
系统层面:QPS、响应时间(RT)、错误率、CPU/内存/磁盘使用率、网络吞吐量(Prometheus+Grafana,开源免费)
-
应用层面:库存扣减成功率、MQ消息堆积量、缓存命中率、数据库读写耗时(Micrometer,开源免费)
-
业务层面:抢购成功率、用户排队时长、订单生成率、支付转化率(自定义日志+ELK,开源免费)
2. 监控实现(基于Micrometer+Prometheus+Grafana,低成本落地)
通过Micrometer记录关键指标,暴露给Prometheus采集,最终在Grafana中可视化展示,异常时触发告警(邮件/钉钉),零商业成本,易于扩展。
@Component
public class SeckillMonitorService {
private final MeterRegistry meterRegistry;
public SeckillMonitorService(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}
/**
* 记录秒杀请求关键指标(低成本,无额外资源消耗)
*/
public void recordSeckillMetric(String productId, boolean success, long costTime) {
// 1. 总请求数计数
meterRegistry.counter("seckill.requests.total", "productId", productId).increment();
// 2. 成功/失败请求数计数
String result = success ? "success" : "fail";
meterRegistry.counter("seckill.requests.result", "productId", productId, "result", result).increment();
// 3. 处理耗时统计(直方图,便于分析响应时间分布)
meterRegistry.timer("seckill.process.time", "productId", productId).record(costTime, TimeUnit.MILLISECONDS);
// 4. 库存剩余量监控(仪表盘,便于实时查看)
Integer currentStock = getCurrentRedisStock(productId);
meterRegistry.gauge("seckill.stock.remaining", "productId", productId, currentStock);
// 5. 缓存命中率监控(降低Redis和数据库压力,成本优化关键指标)
recordCacheHitRate();
}
/**
* 记录缓存命中率(低成本,提升系统性能,降低资源消耗)
*/
private static long cacheHitCount = 0;
private static long cacheMissCount = 0;
public void recordCacheHit(boolean hit) {
if (hit) {
cacheHitCount++;
} else {
cacheMissCount++;
}
}
private void recordCacheHitRate() {
long total = cacheHitCount + cacheMissCount;
if (total == 0) {
return;
}
double hitRate = (double) cacheHitCount / total;
meterRegistry.gauge("seckill.cache.hit.rate", hitRate);
}
/**
* 获取当前Redis库存(轻量查询,低成本)
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;
private Integer getCurrentRedisStock(String productId) {
String stockKey = "seckill:stock:" + productId;
String stockStr = stringRedisTemplate.opsForValue().get(stockKey);
return stockStr == null ? 0 : Integer.parseInt(stockStr);
}
}
成本优化点:监控组件均为开源免费,无需商业授权;指标采集轻量级,不会对系统性能造成明显影响,降低服务器资源消耗。
五、部署与扩展(弹性伸缩,低成本扩容,避免资源闲置)
1. 弹性扩展策略(按需扩容,兼顾性能与成本,核心原则:「峰谷适配」)
-
水平扩展:秒杀服务无状态化设计,基于Docker/K8s实现快速横向扩容,峰值时临时增加节点,峰值过后缩容,避免资源闲置,降低服务器成本
-
自动伸缩:基于K8s HPA(Horizontal Pod Autoscaler),根据CPU使用率(>80%)或QPS(>40000)自动扩容,无需人工干预,降低运维成本
-
异地多活:核心服务(秒杀服务、Redis、数据库)按需部署在多个机房,初期可单机房部署,业务增长后再扩展,避免初期过度投入,降低硬件和运维成本
成本核心原则:资源复用,按需分配,避免为了峰值流量而长期部署大量闲置资源。
2. 压测方案(提前验证,规避线上风险,降低故障损失成本)
压测是秒杀系统上线前的必要环节,基于开源JMeter实现,零商业成本,提前发现性能瓶颈和资源不足,避免线上故障导致的经济损失。
压测场景(覆盖核心场景,低成本,高效验证)
-
库存预热场景:10万用户同时抢1万商品,验证库存扣减准确性与系统响应能力
-
持续高压场景:5万QPS持续5分钟,验证系统稳定性与资源瓶颈
-
峰值冲击场景:瞬间20万QPS,验证流量削峰与熔断降级效果
-
降级恢复场景:手动熔断Redis服务,验证降级兜底效果与服务恢复能力
压测目标(兼顾性能与成本,合理设定,避免过度优化)
-
秒杀请求成功率:>99.9%
-
平均响应时间:<100ms
-
系统错误率:<0.1%
-
无超卖、无少卖问题
-
熔断降级生效及时,服务恢复正常无数据不一致
压测工具与成本优化
-
压测工具:JMeter(开源免费,支持分布式压测,低成本)
-
压测环境:使用云服务器按量付费,压测完成后释放资源,避免长期占用,降低服务器成本
-
压测数据:使用模拟数据,无需真实数据,降低数据准备成本和隐私泄露风险
六、安全考虑(低成本方案,避免恶意攻击,减少经济损失)
-
防刷机制(开源组件,零额外成本):
-
计算型验证码(Google reCAPTCHA,开源免费),峰值时可降级为按钮置灰,降低后端压力,低成本
-
设备指纹校验(前端收集设备信息,后端哈希存储,Redis实现,开源免费),防止同一设备多账号刷请求
-
行为分析(自定义日志+ELK,开源免费),拦截异常行为(如短时间内大量请求不同商品)
-
布隆过滤器(Caffeine实现,本地缓存),拦截无效商品请求,降低Redis和数据库压力,低成本
-
数据安全(原生方案,零额外成本):
-
用户敏感信息加密存储(Spring Security加密,开源免费),如手机号、地址,采用AES加密,防止数据泄露,降低法律风险和经济损失
-
订单操作日志全记录(MyBatis拦截器,开源免费),便于追溯和对账,降低故障排查成本
-
接口防篡改校验(签名验证,原生Java实现),请求参数+时间戳+密钥生成签名,防止恶意篡改请求,低成本
-
权限控制(Spring Security,开源免费,低成本):
-
登录态校验(JWT,无状态,开源免费),避免匿名用户参与秒杀
-
用户秒杀资格校验(Redis缓存,开源免费),避免重复参与秒杀
-
接口访问权限控制(Spring Security,开源免费),防止恶意调用内部接口
成本优化点:所有安全方案均基于开源组件和原生Java实现,无需商业安全工具,零额外部署成本,同时有效避免恶意攻击,减少经济损失。
总结要点(高性能+低成本,落地核心)
-
架构核心:分层过滤 + 异步处理 + 最终一致性,层层削峰,避免后端承压,最小化资源消耗,降低部署成本
-
库存核心:Redis原子操作 + 消息队列 + 数据库乐观锁,三重保障,杜绝超卖,无需商业中间件,低成本落地
-
性能核心:多级缓存 + 缓存预热 + 读写分离,提升响应速度,降低数据库压力,优先使用本地缓存,减少分布式缓存成本
-
成本核心:按需部署 + 弹性伸缩 + 开源组件,避免资源闲置,初期最小资源起步,业务增长后再扩容,兼顾性能与成本
-
稳定核心:熔断降级 + 多维度限流 + 定时对账,快速失败,兜底数据一致性,降低故障排查成本和经济损失
面试回答精简版(突出低成本+高可用,贴合企业实际需求)
在设计万人级秒杀系统时,我的核心思路是「前端拦截、中间削峰、后端兜底、成本可控」,既保证高并发、防超卖,又兼顾企业低成本落地,具体分为四步:
-
架构分层,层层削峰,低成本起步:首先做静态资源CDN分离(按量付费,降低闲置成本),减轻后端压力;然后通过Nginx(开源免费)做入口限流,拦截恶意请求;最后将秒杀请求入开源RocketMQ(单机起步,峰值扩容),异步处理,把瞬间高并发转化为平稳流量,避免后端服务被压垮。同时,秒杀服务独立部署且无状态化,支持K8s弹性伸缩(峰值扩容,谷值缩容),控制服务器成本。
-
核心防超卖,三重保障,零额外成本:采用「Redis原子扣减」作为核心方案(开源Redis,Lua脚本保证原子性,杜绝超卖);再用「数据库乐观锁」作为兜底(基于现有MySQL,零额外部署成本);同时引入「预扣库存+异步同步」,活动前缓存预热(多级缓存,优先本地缓存,降低Redis成本),活动中异步更新数据库,兼顾性能与数据一致性。
-
高可用兜底,开源组件,低成本落地:一方面通过多维度限流(Redis计数器+开源Sentinel)、熔断降级(开源Resilience4j),防止服务级联崩溃,快速返回兜底结果,降低故障损失;另一方面通过定时对账任务(基于现有Redis和数据库,零额外成本),修复Redis与数据库的库存偏差,保证最终一致性。
-
全链路优化,成本可控,贴合企业实际:优先使用本地缓存(Caffeine)和开源组件,避免商业中间件;监控采用Prometheus+Grafana(开源免费),及时发现问题;部署采用弹性伸缩,按需分配资源,避免闲置浪费。同时,做好防刷、数据安全等细节,兼顾系统性能、用户体验与企业落地成本,最终实现高并发、高可用、低成本、无超卖的秒杀系统。
💡 请期待更多低成本优化技巧(热点Key分散、Redis单机转集群平滑迁移、K8s弹性伸缩配置),助力企业快速落地秒杀系统,降低研发与运维成本!
更多推荐

所有评论(0)