文章目录
- 一、前言
- 二、系统架构
- 三、技术栈
- 四、系统设计
- 4.1 商品设计
- 4.2 用户设计
- 4.3 抢单设计
- 4.5 并发控制
- 4.5 获取用户购买记录代码
- 4.7 扣减商品库存代码
- 4.8 获取商品锁代码
- 4.9 添加订单记录代码
- 总结
一、前言
本文已收录于PHP全栈系列专栏:PHP面试专区。
计划将全覆盖PHP开发领域所有的面试题,对标资深工程师/架构师序列
,欢迎大家提前关注锁定。
随着互联网的快速发展,电商行业也在不断地蓬勃发展。其中,电商秒杀活动已经成为了电商行业重要的促销方式之一。在电商秒杀活动中,用户可以以非常低的价格抢购到商品,从而带来了极高的转化率和客户满意度。但是,由于秒杀活动的特殊性,系统对性能和安全等方面的要求都很高。
本文将介绍一个基于PHP、Redis、MySQL的电商秒杀系统的实现方案,并给出核心关键代码。
二、系统架构
整个秒杀系统分为四层,分别是Web层、应用层、数据层和存储层。其中:
- Web层:负责接收用户的请求,处理请求参数和验证请求的合法性。
- 应用层:负责对用户请求进行逻辑处理,包括判断该用户是否有抢购资格、判断商品库存是否充足等。
- 数据层:负责操作数据库,通过读写分离提升数据库性能。
- 存储层:负责缓存商品信息和用户信息,通过Redis集群提升系统性能。
三、技术栈
本系统使用的技术栈如下:
- Nginx:Web服务器,负责接收用户请求并分发到应用层。
- PHP:服务端编程语言,用于实现应用层。本系统使用最新的PHP 8版本。
- Redis:内存缓存数据库,用于缓存商品和用户信息。
- MySQL:关系型数据库,用于存储商品和订单信息。
- Docker:容器化部署工具,用于统一环境和简化部署。
四、系统设计
4.1 商品设计
在秒杀系统中,商品是核心资源之一。本系统中,商品数据表结构如下:
CREATE TABLE `goods` (
`id` INT UNSIGNED AUTO_INCREMENT COMMENT '商品ID',
`name` VARCHAR(128) NOT NULL COMMENT '商品名称',
`price` DECIMAL(10,2) UNSIGNED NOT NULL DEFAULT '0.00' COMMENT '商品单价',
`total` INT UNSIGNED NOT NULL COMMENT '商品库存',
`sold` INT UNSIGNED NOT NULL DEFAULT '0' COMMENT '已售数量',
`start_time` TIMESTAMP NOT NULL COMMENT '秒杀开始时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
- id:商品ID,自增主键。
- name:商品名称。
- price:商品单价,精确到两位小数。
- total:商品总库存。
- sold:已经售出的数量。
- start_time:秒杀开始时间,建议为具体时间点,如
2023-07-01 12:00:00
。
4.2 用户设计
在秒杀系统中,用户需要进行身份验证和抢购资格判断。本系统中,用户数据表结构如下:
CREATE TABLE `user` (
`id` INT UNSIGNED AUTO_INCREMENT COMMENT '用户ID',
`name` VARCHAR(32) NOT NULL COMMENT '用户名',
`mobile` VARCHAR(11) NOT NULL COMMENT '手机号',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
- id:用户ID,自增主键。
- name:用户名。
- mobile:用户手机号,用于身份验证和资格判断。
需要注意的是,为了提高系统性能,建议使用Redis缓存用户信息,避免频繁地查询数据库。
4.3 抢单设计
在秒杀系统中,抢单是最核心的功能之一。抢单的处理流程如下:
- 用户发起请求,包含用户ID和商品ID。
- 系统首先从Redis缓存中获取该用户的购买记录,如果该用户已经购买过该商品,则直接返回“重复购买”错误。
- 如果该用户没有购买记录,则从Redis中获取该商品的库存数量。如果库存已经为0,则直接返回“商品已经抢光”错误。
- 如果该商品还有库存,则从Redis的队列中取出一个商品锁,如果队列已经空了,则直接返回“当前人数过多,请稍后再试”错误。
- 如果取到了商品锁,则进行事务操作,将商品库存减1,并在MySQL中添加订单记录。如果事务提交成功,则返回“抢购成功”,否则返回“系统繁忙,请稍后再试”错误。
商品锁是本系统的核心设计之一。为了保证系统的性能和可靠性,本系统采用Redis的队列实现商品锁。可以通过如下代码很容易地实现队列的获取和释放:
// 获取锁
$lock = $redis->blPop('goods_lock:' . $goodsId, 10);
if (!isset($lock[1])) {
throw new RuntimeException('当前人数过多,请稍后再试');
}
// 处理业务逻辑
// ...
// 释放锁
$redis->rPush('goods_lock:' . $goodsId, 1);
4.5 并发控制
在秒杀活动中,高并发是不可避免的问题。因此,在系统设计中需要考虑如何处理高并发请求。本系统采用如下措施来处理高并发:
- 使用Redis缓存,减轻数据库的压力。
- 前端限流,避免过多的请求直接打到系统中。例如,可以设置每个用户每秒只能发起一次请求。
- 后端限流,避免请求过多导致系统崩溃。例如,可以使用
semaphore
或redis
进行请求频率控制。 - 使用Redis的队列实现商品锁,避免并发造成的销量超额问题。
4.5 获取用户购买记录代码
// 缓存用户的购买记录
$key = 'user_buy:' . $userId . '_' . $goodsId;
$count = $redis->get($key);
if (false !== $count) {
if ($count >= 1) {
throw new RuntimeException('重复购买');
}
}
4.7 扣减商品库存代码
// 检查库存是否足够
$stockKey = 'goods_stock:' . $goodsId;
$total = $redis->get($stockKey);
if ($total <= 0) {
throw new RuntimeException('商品已经抢光');
}
// 将库存数减1
$newStock = $redis->decr($stockKey);
if ($newStock < 0) {
// 库存不足
$redis->incr($stockKey);
throw new RuntimeException('商品已经抢光');
}
4.8 获取商品锁代码
// 获取锁
$lock = $redis->blPop('goods_lock:' . $goodsId, 10);
if (!isset($lock[1])) {
throw new RuntimeException('当前人数过多,请稍后再试');
}
// 处理业务逻辑
// ...
// 释放锁
$redis->rPush('goods_lock:' . $goodsId, 1);
4.9 添加订单记录代码
// 添加订单记录
$data = [
'order_no' => $orderNo,
'user_id' => $userId,
'goods_id' => $goodsId,
'goods_name' => $goods['name'],
'goods_price' => $goods['price'],
'ctime' => time(),
];
$res = $this->db->table('order')->insert($data);
if (empty($res)) {
throw new RuntimeException('添加订单失败');
}
总结
本文介绍了一个基于PHP、Redis、MySQL的电商秒杀系统的实现方案,并给出了核心代码。
本文已收录于PHP全栈系列专栏:PHP面试专区。
计划将全覆盖PHP开发领域所有的面试题,对标资深工程师/架构师序列
,欢迎大家提前关注锁定。