引言
在电商领域,商品秒杀是一种常见的促销活动,它要求系统能够在短时间内处理大量的并发请求。本文将深入探讨高并发的概念、秒杀系统面临的挑战以及相应的解决方案,并提供基于Redis和MySQL的实现示例。
目录
并发与高并发基础
什么是并发
并发是指在同一时间段内,多个任务同时执行的能力。在计算机系统中,这通常通过时间片轮转或多核处理来实现。
举个生活中的例子来理解并发和并行的区别:
- 单线程处理:你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这说明你不支持并发也不支持并行。
- 并发处理:你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
- 并行处理:你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
什么是高并发
高并发是指在同一时间内,有大量的用户请求同时访问系统,系统需要能够处理这些请求并返回结果。在电商秒杀、抢购、直播带货等场景中,高并发尤为常见。
高并发的特点
- 请求量大:短时间内涌入大量请求
- 请求集中:请求在时间和对象上高度集中
- 峰值明显:流量呈现明显的峰谷特征
- 持续时间短:高峰期通常持续时间较短
- 用户体验敏感:响应速度直接影响用户体验和转化率
秒杀系统架构设计
系统架构图
一个典型的秒杀系统架构通常包括以下几个层次:
用户 -> CDN -> 负载均衡 -> 应用服务器集群 -> 缓存层 -> 数据库层
↓
消息队列
↓
异步处理服务
核心流程
- 秒杀前:商品数据预热到缓存,准备好限流策略
- 秒杀中:
- 前端限流和防重复提交
- 验证用户资格
- 检查库存(缓存层)
- 扣减库存(缓存层原子操作)
- 生成订单(可异步处理)
- 支付确认
- 秒杀后:数据一致性处理,清理缓存等
关键技术点
- 动静分离
- 热点数据缓存
- 削峰填谷
- 限流熔断
- 异步处理
- 分布式锁
- 幂等性设计
高并发的挑战
高并发系统面临的主要挑战包括:
- 系统的性能瓶颈:CPU、内存、网络IO等资源限制
- 数据库的压力:连接数限制、锁竞争、磁盘IO瓶颈
- 缓存的失效:缓存穿透、缓存击穿、缓存雪崩
- 网络的延迟:网络带宽限制、网络拥塞
- 数据一致性:分布式系统中的数据一致性难题
- 系统稳定性:单点故障、级联失败风险
高并发的解决方案
前端优化
- 页面静态化:将秒杀页面静态化并部署到CDN
- 资源压缩:压缩CSS、JavaScript文件减少传输量
- 请求合并:合并多个HTTP请求减少连接开销
- 客户端限流:前端按钮控制、倒计时等防止重复提交
- 页面预加载:提前加载页面资源
系统架构优化
- 系统拆分:按业务领域或功能拆分为微服务
- 集群部署:应用服务器水平扩展
- 负载均衡:使用Nginx、LVS等实现负载均衡
- 异步处理:使用消息队列实现异步处理
- 服务降级:非核心功能在高峰期自动降级
数据库优化
- 分库分表:水平拆分和垂直拆分减轻单库压力
- 读写分离:主库写入,从库读取
- 索引优化:合理设计索引提高查询效率
- SQL优化:优化SQL语句,避免全表扫描
- 连接池:复用数据库连接减少建立连接的开销
缓存策略
- 多级缓存:浏览器缓存、CDN缓存、应用缓存、分布式缓存
- 热点数据缓存:将秒杀商品信息、库存等热点数据放入缓存
- 缓存预热:活动开始前提前将数据加载到缓存
- 缓存更新策略:设置合理的缓存过期策略,采用更新缓存和删除缓存策略
- 防止缓存问题:解决缓存穿透、缓存击穿、缓存雪崩问题
限流与熔断
- 接口限流:令牌桶、漏桶算法限制接口访问频率
- 分布式限流:使用Redis、Sentinel等实现分布式限流
- 熔断机制:当服务不可用时快速失败而不是持续等待
- 降级策略:返回兜底数据而不是错误
- 黑白名单:针对特定IP或用户实施不同的限流策略
秒杀系统实现示例
基于Redis的实现
List 队列实现
<?php
namespace app\controller;
use support\Request;
use app\model\Goods;
use app\model\Order;
use support\Db;
use support\Redis;
class ScoreKill
{
/**
* 初始化商品库存
* @param int $goodsId 商品ID
* @return void
*/
public function setGoods($goodsId = 1)
{
// 获取商品信息
$goods = Goods::find($goodsId);
if (!$goods) {
return json([
'code' => 400,
'message' => '商品不存在'
]);
}
$stock = $goods->stock;
// 清空之前的库存队列
Redis::del("goodsId:{$goodsId}");
// 将商品放入Redis List队列中,每个元素代表一个库存单位
for ($i=1; $i <= $stock; $i++) {
Redis::lpush("goodsId:{$goodsId}", json_encode($goods));
}
return json([
'code' => 200,
'message' => '库存初始化成功',
'data' => [
'goods_id' => $goodsId,
'stock' => $stock
]
]);
}
/**
* 秒杀商品 - 基于Redis List队列实现
* @param Request $request
* @return \support\Response
*/
public function killGoods(Request $request)
{
$goodsId = 1;
$userId = $request->input('user_id', rand(1, 999));
// 检查库存
$stock = Redis::lLen("goodsId:{$goodsId}");
if ($stock <= 0) {
return json([
'code' => 400,
'message' => '商品库存不足'
]);
}
// 检查用户是否购买过
$purchased = Redis::get("user_purchased:{$userId}:{$goodsId}");
if ($purchased) {
return json([
'code' => 400,
'message' => '用户已购买过该商品'
]);
}
// 从队列中取出商品(原子操作,保证了并发安全)
$goods = Redis::rPop("goodsId:{$goodsId}");
if (!$goods) {
return json([
'code' => 400,
'message' => '商品库存不足'
]);
}
$goods = json_decode($goods, true);
// 标记用户已购买
Redis::set("user_purchased:{$userId}:{$goodsId}", 1, ['EX' => 86400]); // 24小时过期
try {
// 创建订单(实际项目中应考虑异步处理)
$order = new Order();
$order->user_id = $userId;
$order->goods_id = $goodsId;
$order->save();
return json([
'code' => 200,
'message' => '秒杀成功',
'data' => [
'order_id' => $order->id
]
]);
} catch (\Exception $e) {
// 发生异常时,将商品放回队列
Redis::lpush("goodsId:{$goodsId}", json_encode($goods));
// 移除购买标记
Redis::del("user_purchased:{$userId}:{$goodsId}");
return json([
'code' => 500,
'message' => '系统异常:' . $e->getMessage()
]);
}
}
}
Watch 乐观锁实现
<?php
namespace app\controller;
use support\Request;
use app\model\Goods;
use app\model\Order;
use support\Db;
use support\Redis;
class ScoreKill
{
/**
* 秒杀商品 - 基于Redis Watch乐观锁实现
* @param Request $request
* @return \support\Response
*/
public function killGoods(Request $request)
{
$goodsId = 1;
$userId = $request->input('user_id', rand(1, 999));
$stockKey = "goods_stock:{$goodsId}";
$purchasedKey = "user_purchased:{$userId}:{$goodsId}";
// 检查用户是否已购买(防止重复购买)
$hasPurchased = Redis::get($purchasedKey);
if ($hasPurchased) {
return json([
'code' => 400,
'message' => '用户已购买过该商品'
]);
}
// 初始化库存(只在第一次执行时初始化,实际项目中应在商品上架时初始化)
$exists = Redis::exists($stockKey);
if (!$exists) {
// 从数据库获取商品信息
$goods = Goods::find($goodsId);
if (!$goods) {
return json([
'code' => 400,
'message' => '商品不存在'
]);
}
Redis::set($stockKey, $goods->stock);
}
// 尝试最多3次执行秒杀操作
$maxRetries = 3;
for ($i = 0; $i < $maxRetries; $i++) {
// 监视库存键
Redis::watch($stockKey);
// 获取当前库存
$stock = Redis::get($stockKey);
if ($stock <= 0) {
Redis::unwatch(); // 解除监视
return json([
'code' => 400,
'message' => '商品库存不足'
]);
}
// 开启事务
Redis::multi();
// 减少库存
Redis::decr($stockKey);
// 记录用户购买记录
Redis::set($purchasedKey, 1, ['EX' => 86400]); // 24小时过期
// 执行事务
$result = Redis::exec();
// 判断事务是否执行成功
if ($result) {
try {
// 创建订单(实际项目中可考虑异步处理)
$order = new Order();
$order->user_id = $userId;
$order->goods_id = $goodsId;
$order->save();
return json([
'code' => 200,
'message' => '秒杀成功',
'data' => [
'order_id' => $order->id,
'retry_count' => $i
]
]);
} catch (\Exception $e) {
// 发生异常时,恢复库存
Redis::incr($stockKey);
// 移除购买标记
Redis::del($purchasedKey);
return json([
'code' => 500,
'message' => '系统异常:' . $e->getMessage()
]);
}
}
// 如果执行失败,说明有并发修改,继续重试
// Redis Watch会自动取消监视,不需要显式调用unwatch
}
// 多次重试后仍然失败
return json([
'code' => 429, // Too Many Requests
'message' => '秒杀人数过多,请稍后重试'
]);
}
}
基于MySQL的实现
乐观锁实现
<?php
namespace app\controller;
use support\Request;
use app\model\Goods;
use app\model\Order;
use support\Db;
class ScoreKill
{
/**
* 秒杀商品 - 基于MySQL乐观锁实现
* @param Request $request
* @return \support\Response
*/
public function killGoods(Request $request)
{
$goodsId = 1;
$userId = $request->input('user_id', rand(1, 999));
// 检查商品是否存在
Db::beginTransaction();
try {
// 查询商品信息
$goods = Goods::find($goodsId);
if (!$goods) {
Db::rollBack();
return json([
'code' => 400,
'message' => '商品不存在'
]);
}
// 检查库存
if ($goods->stock <= 0) {
Db::rollBack();
return json([
'code' => 400,
'message' => '商品库存不足'
]);
}
// 检查用户是否购买过
$order = Order::where('user_id', $userId)
->where('goods_id', $goodsId)
->first();
if ($order) {
Db::rollBack();
return json([
'code' => 400,
'message' => '用户已购买过该商品'
]);
}
// 使用乐观锁更新库存
// 通过version字段实现乐观锁,只有当version匹配时才更新成功
$result = Goods::where([
'id' => $goodsId,
'version' => $goods->version
])->update([
'stock' => $goods->stock - 1,
'version' => $goods->version + 1
]);
if ($result) {
// 创建订单
$order = new Order();
$order->user_id = $userId;
$order->goods_id = $goodsId;
$order->save();
Db::commit();
return json([
'code' => 200,
'message' => '秒杀成功',
'data' => [
'order_id' => $order->id
]
]);
} else {
// 更新失败,说明有并发更新,版本号已变化
Db::rollBack();
return json([
'code' => 409, // Conflict
'message' => '秒杀失败,请重试'
]);
}
} catch(\Exception $e) {
Db::rollBack();
return json([
'code' => 500,
'message' => '系统异常:' . $e->getMessage()
]);
}
}
}
排他锁实现
<?php
namespace app\controller;
use support\Request;
use app\model\Goods;
use app\model\Order;
use support\Db;
class ScoreKill
{
/**
* 秒杀商品 - 基于MySQL排他锁实现
* @param Request $request
* @return \support\Response
*/
public function killGoods(Request $request)
{
$goodsId = 1;
$userId = $request->input('user_id', rand(1, 999));
// 开启事务
Db::beginTransaction();
try {
// 使用FOR UPDATE获取排他锁
$goods = Goods::lockForUpdate()->find($goodsId);
if (!$goods) {
Db::rollBack();
return json([
'code' => 400,
'message' => '商品不存在'
]);
}
// 检查库存
if ($goods->stock <= 0) {
Db::rollBack();
return json([
'code' => 400,
'message' => '商品库存不足'
]);
}
// 检查用户是否购买过
$order = Order::where('user_id', $userId)
->where('goods_id', $goodsId)
->first();
if ($order) {
Db::rollBack();
return json([
'code' => 400,
'message' => '用户已购买过该商品'
]);
}
// 更新库存
$result = Goods::where('id', $goodsId)->update([
'stock' => $goods->stock - 1,
]);
if ($result) {
// 创建订单
$order = new Order();
$order->user_id = $userId;
$order->goods_id = $goodsId;
$order->save();
Db::commit();
return json([
'code' => 200,
'message' => '秒杀成功',
'data' => [
'order_id' => $order->id
]
]);
} else {
Db::rollBack();
return json([
'code' => 400,
'message' => '秒杀失败'
]);
}
} catch(\Exception $e) {
Db::rollBack();
return json([
'code' => 500,
'message' => '系统异常:' . $e->getMessage()
]);
}
}
}
性能测试与优化
在实现秒杀系统后,需要进行性能测试以验证系统的并发处理能力:
- 压力测试工具:使用Apache JMeter、Gatling等工具模拟大量并发请求
- 测试指标:
- QPS(每秒查询率)
- 响应时间
- 成功率
- 资源利用率(CPU、内存、网络等)
- 性能瓶颈分析:
- 使用监控工具定位瓶颈
- 分析慢查询日志
- 检查网络延迟
常见问题与解决方案
1. 超卖问题
问题:多个用户同时抢购,导致库存出现负数。
解决方案:
- 使用Redis的原子操作(如LPOP、DECR等)
- 数据库乐观锁或悲观锁
- 预扣库存 + 异步确认
2. 库存同步问题
问题:缓存中的库存与数据库中的库存不一致。
解决方案:
- 定时任务同步库存
- 异步更新数据库
- 最终一致性设计
3. 重复下单问题
问题:用户重复提交订单。
解决方案:
- 前端防重复提交
- 接口幂等性设计
- 唯一索引约束
4. 缓存雪崩问题
问题:大量缓存同时失效,导致请求直接访问数据库。
解决方案:
- 缓存失效时间随机化
- 热点数据永不过期
- 多级缓存架构
总结与最佳实践
构建高性能的秒杀系统需要综合考虑前端优化、系统架构、数据库设计、缓存策略等多个方面。以下是一些最佳实践:
-
系统设计:
- 秒杀系统独立部署
- 采用微服务架构
- 使用消息队列削峰填谷
-
数据层优化:
- 热点数据预加载到缓存
- 避免使用复杂SQL
- 合理使用锁机制
-
应用层优化:
- 接口限流
- 异步处理
- 服务降级
-
前端优化:
- 页面静态化
- CDN加速
- 客户端限流
-
监控与预警:
- 实时监控系统状态
- 建立完善的预警机制
- 制定应急预案
通过合理的架构设计和优化策略,可以构建出高性能、高可用的秒杀系统,满足大规模并发访问的需求。