一、系统架构设计
1. 分层架构
markdown
客户端层 → 接入层 → 业务服务层 → 数据层
↓ ↓ ↓ ↓
限流 缓存 队列 数据库
2. 具体组件
- 客户端:静态资源CDN、倒计时校准、防重复提交
- 接入层:Nginx+Lua/OpenResty,做第一层限流和缓存
- 业务层:
-
- 秒杀服务集群(无状态)
- 消息队列(Kafka/RocketMQ)
- 缓存集群(Redis Cluster)
- 数据层:
-
- 主从数据库(读写分离)
- 分库分表(按商品/时间)
二、核心问题解决方案
1. 超卖问题
解决方案一:Redis原子操作
ini
# 使用Redis的DECR原子操作扣减库存
def deduct_stock(product_id, user_id):
stock_key = f"stock:{product_id}"
# Lua脚本保证原子性
lua_script = """
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock > 0 then
redis.call('DECR', KEYS[1])
return 1
end
return 0
"""
result = redis.eval(lua_script, 1, stock_key)
return result == 1
解决方案二:数据库乐观锁
ini
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = ? AND stock > 0 AND version = ?
解决方案三:预扣库存
arduino
// 先预扣Redis库存,再异步同步到DB
public boolean preDeductStock(String productId, int count) {
String key = "seckill:stock:" + productId;
Long remaining = redisTemplate.opsForValue().decrement(key, count);
if (remaining >= 0) {
// 发送MQ消息异步扣减数据库
sendStockDeductMessage(productId, count);
return true;
} else {
// 库存不足,回滚
redisTemplate.opsForValue().increment(key, count);
return false;
}
}
2. 高并发请求处理
2.1 流量削峰
kotlin
// 使用消息队列缓冲请求
@Component
public class SeckillService {
@Autowired
private RocketMQTemplate mqTemplate;
public SeckillResult seckill(SeckillRequest request) {
// 1. 校验用户和商品状态
if (!validate(request)) {
return SeckillResult.fail("校验失败");
}
// 2. 生成唯一请求ID
String requestId = generateRequestId(request);
// 3. 请求入队,立即返回
mqTemplate.sendOneWay("seckill-topic",
MessageBuilder.withPayload(request).build());
// 4. 返回排队中状态,前端轮询结果
return SeckillResult.processing(requestId);
}
}
2.2 分层过滤
所有请求 → 合法性校验 → 库存校验 → 频率控制 → 实际下单
↓ ↓ ↓ ↓ ↓
100万 50万 10万 5万 1万
3. 系统性能优化
3.1 缓存策略
makefile
# 多级缓存配置
缓存层级:
一级: JVM本地缓存 (Caffeine) - 热点商品
二级: Redis集群 - 库存信息
三级: 数据库 - 最终一致性
3.2 读多写少优化
scss
// 商品信息缓存预热
@Service
public class CacheWarmUpService {
@PostConstruct
public void warmUpSeckillProducts() {
List<Product> hotProducts = loadHotProducts();
for (Product product : hotProducts) {
// 库存信息
redisTemplate.opsForValue().set(
"stock:" + product.getId(),
product.getStock()
);
// 商品详情
redisTemplate.opsForValue().set(
"product:" + product.getId(),
JSON.toJSONString(product)
);
// 使用布隆过滤器存储可售商品ID
bloomFilter.add(product.getId());
}
}
}
4. 详细实现方案
4.1 秒杀流程
ruby
class SeckillSystem:
def process_seckill(self, user_id, product_id):
# 1. 恶意请求拦截
if not self.check_risk(user_id):
return {"code": 403, "msg": "访问过于频繁"}
# 2. 布隆过滤器快速判断
if not bloom_filter.contains(product_id):
return {"code": 404, "msg": "商品不存在"}
# 3. 内存标记(已售罄的商品直接返回)
if sold_out_flags.get(product_id):
return {"code": 400, "msg": "已售罄"}
# 4. Redis原子扣减库存
if not self.deduct_stock_in_redis(product_id):
sold_out_flags[product_id] = True
return {"code": 400, "msg": "库存不足"}
# 5. 生成订单ID(雪花算法)
order_id = snowflake.generate()
# 6. 订单信息入队
mq.send({
"order_id": order_id,
"user_id": user_id,
"product_id": product_id,
"time": time.time()
})
# 7. 返回排队中
return {
"code": 200,
"msg": "排队中",
"order_id": order_id,
"queue_position": get_queue_position(order_id)
}
4.2 库存同步方案
scss
@Component
@Slf4j
public class StockSyncService {
// 数据库最终扣减
@Transactional
public void syncStockToDB(String productId, int count) {
try {
// 数据库扣减(带重试机制)
boolean success = productDAO.deductStock(productId, count);
if (success) {
// 更新Redis中的最终库存状态
redisTemplate.opsForValue().set(
"stock_final:" + productId,
getDBStock(productId)
);
// 删除售罄标记
soldOutCache.remove(productId);
}
} catch (Exception e) {
log.error("库存同步失败", e);
// 记录异常,人工介入处理
alertService.sendAlert(e);
}
}
// 库存对账任务
@Scheduled(cron = "0 */5 * * * ?")
public void stockReconciliation() {
List<Product> products = productDAO.getAllSeckillProducts();
for (Product product : products) {
Integer redisStock = getRedisStock(product.getId());
Integer dbStock = product.getStock();
if (!Objects.equals(redisStock, dbStock)) {
log.warn("库存不一致: productId={}, redis={}, db={}",
product.getId(), redisStock, dbStock);
// 自动修复或报警
fixStockInconsistency(product.getId(), dbStock);
}
}
}
}
三、高可用保障
1. 限流降级策略
makefile
# 多维度限流配置
限流规则:
用户维度: 每个用户10次/分钟
IP维度: 每个IP 1000次/分钟
商品维度: 每个商品 10000次/分钟
总QPS: 系统最大承受50000 QPS
2. 熔断降级
less
@RestController
@Slf4j
public class SeckillController {
@GetMapping("/seckill/{productId}")
@HystrixCommand(
fallbackMethod = "seckillFallback",
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20")
}
)
public Response seckill(@PathVariable String productId,
@RequestParam String userId) {
return seckillService.process(userId, productId);
}
// 降级方法
public Response seckillFallback(String productId, String userId) {
return Response.error("系统繁忙,请稍后重试");
}
}
四、监控与告警
1. 关键监控指标
- 系统层面:QPS、RT、错误率、CPU/内存使用率
- 应用层面:库存扣减成功率、消息堆积量
- 业务层面:抢购成功率、用户排队时长
2. 监控实现
arduino
@Component
public class SeckillMonitor {
private final MeterRegistry meterRegistry;
// 记录关键指标
public void recordSeckill(String productId, boolean success, long cost) {
// QPS监控
meterRegistry.counter("seckill.requests.total").increment();
if (success) {
meterRegistry.counter("seckill.success.total").increment();
} else {
meterRegistry.counter("seckill.fail.total").increment();
}
// 耗时分布
meterRegistry.timer("seckill.process.time")
.record(cost, TimeUnit.MILLISECONDS);
// 库存变化
meterRegistry.gauge("seckill.stock." + productId,
getCurrentStock(productId));
}
}
五、部署与扩展
1. 弹性扩展策略
- 水平扩展:无状态服务可快速扩容
- 自动伸缩:基于CPU使用率或QPS自动扩缩容
- 异地多活:重要业务支持多机房部署
2. 压测方案
makefile
压测场景:
场景1: 库存预热,10万用户同时抢1万商品
场景2: 持续高压,5万QPS持续5分钟
场景3: 峰值冲击,瞬间20万QPS
压测目标:
成功率: >99.9%
平均RT: <100ms
错误率: <0.1%
六、安全考虑
- 防刷机制:
-
- 验证码(峰值时降级)
- 设备指纹
- 行为分析
- 数据安全:
-
- 关键数据加密
- 操作日志记录
- 防篡改校验
总结要点
- 架构核心:分层过滤 + 异步处理 + 最终一致
- 库存核心:Redis原子操作 + 消息队列 + 数据库乐观锁
- 性能核心:缓存预热 + 流量削峰 + 读写分离
- 稳定核心:熔断降级 + 限流隔离 + 快速失败
面试回答
首先,架构设计上要动静分离、分层削峰。我会把系统分为:
- 静态资源分离:商品图片、描述页等提前推送到CDN,请求直接走边缘节点,不给后端压力。
- 网关层限流:在入口用Nginx或网关(如Sentinel)做恶意请求拦截和总流量限制,比如对同一UID限速,超过阈值直接返回"请求频繁"。
- 业务逻辑后置,请求队列化 :秒杀的核心------"下单扣库存"这个最重要的逻辑,绝不放在前台实时处理。用户点击"抢购"后,前端直接返回"排队中",请求进入一个消息队列 (比如RabbitMQ、Kafka或RocketMQ)。这样一来,海量并发就被平滑成顺序处理的流量,后端服务按照自己的能力从队列里慢慢消费,实现削峰填谷。
- 服务独立部署:把秒杀相关的功能(验资格、扣库存)单独做成一个微服务,避免影响商城其他正常功能(如浏览、普通下单)。
其次,针对如何解决超卖、库存扣减和高并发请求这三个核心问题,我的解决方案是:
- 解决超卖和库存扣减:这是秒杀的核心。我的方案是:
-
- 预扣库存 :活动开始前,把商品的库存从主库加载到Redis中。Redis是单线程内存操作,可以保证原子性。
- 原子化操作 :在Redis里,使用
DECR或LUA脚本来扣减库存。DECR命令会直接返回扣减后的值,如果返回值小于0,就说明库存没了,后续流程直接返回售罄。LUA脚本可以打包多个操作(检查库存、扣减),确保整个过程原子性,彻底杜绝超卖。 - 最终同步:后台服务从队列消费,成功扣减Redis库存后,生成一个订单ID(但状态是"未支付"),再异步去更新数据库的库存。这里数据库的库存更多是用于后续对账和长尾查询。
- 应对高并发请求:
-
- 限流:除了网关层的总限流,在秒杀服务本身也要做限流,比如用信号量或令牌桶控制处理线程数,只服务自己能承受的流量,多的直接拒绝,快速失败。
- 无状态化与扩容:秒杀服务做成无状态的,方便用K8s或云服务快速横向扩容,扛过峰值后再缩容,控制成本。
- 热点数据隔离 :对于"爆款"商品,它的库存Key在Redis里是热点Key。可以做两件事:一是提前对它进行Key散列,把压力分散到多个Redis节点;二是使用Redis集群模式,并开启读写分离。
最后,还有一些关键的细节和兜底策略:
- 防刷与验证:前端加入计算型验证码或答题,防止机器人;下单前必须校验用户资格(是否登录、地址完善等)。
- 异步下单与结果轮询:用户提交后,服务端返回一个"排队ID",前端用这个ID轮询后端,查询最终结果(成功、失败或等待)。用户体验上是"排队等待",而不是一直卡住或报错。
- 数据一致性对账:因为用了Redis和消息队列,可能出现极端情况下的数据不一致(比如Redis扣成功,但下游服务挂了,订单没生成)。需要有一个定时对账任务,核对Redis、数据库库存和订单状态,进行修复。
- 降级与熔断:如果Redis或数据库访问慢,要有熔断机制,防止服务被拖垮。比如可以快速降级到"返回售罄"的静态页面。
总结一下 ,我的设计思路是:前端限流拦截,请求队列削峰;Redis原子扣减防超卖;服务无状态化应对高并发;再通过异步、对账等手段保证最终一致性和用户体验。