Hyperf 项目实战经验总结
1. 项目背景准备
1.1 项目介绍模板
基础信息:
项目名称:[项目名称]
项目类型:高并发 API 服务 / 微服务系统 / 实时通信系统
技术栈:Hyperf 3.1 + MySQL + Redis + RabbitMQ
团队规模:[人数]
项目周期:[时间]
我的角色:后端开发 / 核心开发 / 技术负责人
业务场景(选择一个):
- 电商平台(订单、支付、库存)
- 金融系统(交易、账户、风控)
- 社交平台(消息、动态、推荐)
- 物联网平台(设备管理、数据采集)
- 内容管理系统(文章、评论、搜索)
项目规模:
用户量:[数量]
日活:[数量]
峰值 QPS:[数量]
数据量:[数量]
1.2 技术架构图
┌─────────────────────────────────────────┐
│ Nginx 负载均衡 │
└──────────────┬──────────────────────────┘
│
┌───────┴───────┐
│ │
┌───▼───┐ ┌───▼───┐
│ Hyperf │ │ Hyperf │
│ 服务 1 │ │ 服务 2 │
└───┬───┘ └───┬───┘
│ │
├───────┬───────┤
│ │ │
┌───▼──┐ ┌──▼──┐ ┌─▼────┐
│ MySQL│ │Redis│ │ Queue│
└──────┘ └─────┘ └──────┘
2. 核心功能实现
2.1 用户系统
用户注册
<?php
namespace App\Service;
use App\Model\User;
use App\Event\UserRegistered;
use Hyperf\Di\Annotation\Inject;
use Psr\EventDispatcher\EventDispatcherInterface;
class UserService
{
#[Inject]
private EventDispatcherInterface $eventDispatcher;
public function register(array $data)
{
// 1. 验证数据(通过 Request 验证器完成)
// 2. 检查邮箱是否已注册
if (User::where('email', $data['email'])->exists()) {
throw new \Exception('邮箱已被注册');
}
// 3. 创建用户
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => password_hash($data['password'], PASSWORD_DEFAULT),
'status' => 1,
]);
// 4. 触发注册事件(发送欢迎邮件、赠送优惠券等)
$this->eventDispatcher->dispatch(new UserRegistered($user));
return $user;
}
}
用户登录(JWT)
<?php
namespace App\Service;
use App\Model\User;
use Hyperf\Di\Annotation\Inject;
use Phper666\JWTAuth\JWT;
class AuthService
{
#[Inject]
private JWT $jwt;
public function login(string $email, string $password)
{
// 1. 查找用户
$user = User::where('email', $email)->first();
if (!$user || !password_verify($password, $user->password)) {
throw new \Exception('邮箱或密码错误');
}
// 2. 检查用户状态
if ($user->status != 1) {
throw new \Exception('账户已被禁用');
}
// 3. 生成 JWT Token
$token = $this->jwt->getToken([
'user_id' => $user->id,
'email' => $user->email,
]);
// 4. 更新最后登录时间
$user->last_login_at = date('Y-m-d H:i:s');
$user->save();
return [
'token' => $token,
'user' => $user,
];
}
public function getUserByToken(string $token)
{
$payload = $this->jwt->checkToken($token);
return User::find($payload['user_id']);
}
}
2.2 订单系统
创建订单(带事务和库存扣减)
<?php
namespace App\Service;
use App\Model\Order;
use App\Model\Product;
use App\Job\OrderNotificationJob;
use Hyperf\DbConnection\Db;
use Hyperf\Di\Annotation\Inject;
use Hyperf\AsyncQueue\Driver\DriverFactory;
class OrderService
{
#[Inject]
private DriverFactory $driverFactory;
public function createOrder(int $userId, array $items)
{
return Db::transaction(function () use ($userId, $items) {
$totalAmount = 0;
$orderItems = [];
foreach ($items as $item) {
// 1. 锁定商品(for update)
$product = Product::where('id', $item['product_id'])
->lockForUpdate()
->first();
if (!$product) {
throw new \Exception('商品不存在');
}
// 2. 检查库存
if ($product->stock < $item['quantity']) {
throw new \Exception("{$product->name} 库存不足");
}
// 3. 扣减库存
$product->stock -= $item['quantity'];
$product->save();
// 4. 计算金额
$amount = $product->price * $item['quantity'];
$totalAmount += $amount;
$orderItems[] = [
'product_id' => $product->id,
'product_name' => $product->name,
'price' => $product->price,
'quantity' => $item['quantity'],
'amount' => $amount,
];
}
// 5. 创建订单
$order = Order::create([
'order_no' => $this->generateOrderNo(),
'user_id' => $userId,
'total_amount' => $totalAmount,
'status' => 'pending',
]);
// 6. 创建订单明细
foreach ($orderItems as $item) {
$item['order_id'] = $order->id;
Db::table('order_items')->insert($item);
}
// 7. 发送订单通知(异步队列)
$this->driverFactory->get('default')->push(
new OrderNotificationJob(['order_id' => $order->id])
);
return $order;
});
}
private function generateOrderNo(): string
{
return date('YmdHis') . mt_rand(1000, 9999);
}
}
订单详情(协程并发查询)
<?php
namespace App\Service;
use App\Model\Order;
use Hyperf\Utils\Coroutine;
class OrderService
{
public function getOrderDetail(int $orderId)
{
// 并发查询订单、用户、商品信息
[$order, $user, $items] = Coroutine::parallel([
fn() => Order::find($orderId),
fn() => Db::table('users')
->where('id', function ($query) use ($orderId) {
$query->select('user_id')
->from('orders')
->where('id', $orderId);
})
->first(),
fn() => Db::table('order_items')
->where('order_id', $orderId)
->get(),
]);
return [
'order' => $order,
'user' => $user,
'items' => $items,
];
}
}
2.3 缓存策略
多级缓存
<?php
namespace App\Service;
use App\Model\Product;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
use Psr\SimpleCache\CacheInterface;
class ProductService
{
#[Inject]
private Redis $redis;
#[Inject]
private CacheInterface $cache; // 本地缓存
public function getProduct(int $id)
{
// 1. 本地缓存(进程内存,最快)
$key = "product:{$id}";
$product = $this->cache->get($key);
if ($product) {
return $product;
}
// 2. Redis 缓存(网络开销)
$product = $this->redis->get($key);
if ($product) {
$product = json_decode($product, true);
$this->cache->set($key, $product, 60); // 本地缓存 1 分钟
return $product;
}
// 3. 数据库查询
$product = Product::find($id);
if ($product) {
// 保存到 Redis(1 小时)
$this->redis->setex($key, 3600, json_encode($product));
// 保存到本地缓存(1 分钟)
$this->cache->set($key, $product, 60);
}
return $product;
}
public function updateProduct(int $id, array $data)
{
$product = Product::find($id);
$product->update($data);
// 删除缓存
$key = "product:{$id}";
$this->redis->del($key);
$this->cache->delete($key);
return $product;
}
}
缓存穿透、击穿、雪崩的解决方案
<?php
namespace App\Service;
class CacheService
{
/**
* 防止缓存穿透(查询不存在的数据)
* 方案:缓存空值
*/
public function getWithNullCache(string $key, callable $callback)
{
$value = $this->redis->get($key);
// 缓存命中(包括空值)
if ($value !== false) {
return $value === 'null' ? null : json_decode($value, true);
}
// 查询数据库
$data = $callback();
if ($data === null) {
// 缓存空值(短时间)
$this->redis->setex($key, 60, 'null');
} else {
// 缓存数据
$this->redis->setex($key, 3600, json_encode($data));
}
return $data;
}
/**
* 防止缓存击穿(热点 key 过期)
* 方案:分布式锁 + 双重检查
*/
public function getWithLock(string $key, callable $callback, int $ttl = 3600)
{
$value = $this->redis->get($key);
if ($value !== false) {
return json_decode($value, true);
}
// 获取分布式锁
$lockKey = "lock:{$key}";
$lockValue = uniqid();
$locked = $this->redis->set($lockKey, $lockValue, ['NX', 'EX' => 10]);
if (!$locked) {
// 未获取到锁,等待后重试
usleep(50000); // 等待 50ms
return $this->getWithLock($key, $callback, $ttl);
}
try {
// 双重检查
$value = $this->redis->get($key);
if ($value !== false) {
return json_decode($value, true);
}
// 查询数据库
$data = $callback();
// 保存到缓存
$this->redis->setex($key, $ttl, json_encode($data));
return $data;
} finally {
// 释放锁(Lua 脚本保证原子性)
$script = <<<LUA
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end
LUA;
$this->redis->eval($script, [$lockKey, $lockValue], 1);
}
}
/**
* 防止缓存雪崩(大量 key 同时过期)
* 方案:过期时间加随机值
*/
public function setWithRandomTTL(string $key, $value, int $baseTTL = 3600)
{
$ttl = $baseTTL + mt_rand(0, 600); // 加 0-10 分钟的随机值
$this->redis->setex($key, $ttl, json_encode($value));
}
}
2.4 限流防刷
令牌桶限流
<?php
namespace App\Middleware;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class RateLimitMiddleware implements MiddlewareInterface
{
#[Inject]
private Redis $redis;
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$userId = $request->getAttribute('user_id');
$key = "rate_limit:{$userId}";
// 令牌桶算法(Lua 脚本)
$script = <<<LUA
local key = KEYS[1]
local capacity = tonumber(ARGV[1]) -- 桶容量
local rate = tonumber(ARGV[2]) -- 补充速率(令牌/秒)
local requested = tonumber(ARGV[3]) -- 请求令牌数
local now = tonumber(ARGV[4]) -- 当前时间
local bucket = redis.call('hmget', key, 'tokens', 'last_time')
local tokens = tonumber(bucket[1]) or capacity
local last_time = tonumber(bucket[2]) or now
-- 计算当前令牌数
local delta = math.max(0, now - last_time)
tokens = math.min(capacity, tokens + delta * rate)
-- 尝试消费令牌
if tokens >= requested then
tokens = tokens - requested
redis.call('hmset', key, 'tokens', tokens, 'last_time', now)
redis.call('expire', key, 3600)
return 1
else
return 0
end
LUA;
$capacity = 100; // 桶容量 100
$rate = 10; // 每秒补充 10 个令牌
$requested = 1; // 每次请求消耗 1 个令牌
$now = microtime(true);
$allowed = $this->redis->eval($script, [$key, $capacity, $rate, $requested, $now], 1);
if (!$allowed) {
return response()->json([
'code' => 429,
'message' => '请求过于频繁,请稍后再试',
], 429);
}
return $handler->handle($request);
}
}
3. 性能优化案例
3.1 优化前后对比
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| QPS | 500 | 15,000 | 30x |
| 响应时间 | 200ms | 50ms | 4x |
| 并发连接 | 1,000 | 100,000 | 100x |
3.2 优化措施
1. 数据库优化
问题:N+1 查询导致性能差。
优化前:
$users = User::all();
foreach ($users as $user) {
$user->orders; // 每次都查询数据库,100 个用户就是 100 次查询
}
优化后:
$users = User::with('orders')->get(); // 只查询 2 次(1 次用户 + 1 次订单)
2. 使用缓存
问题:每次都查询数据库。
优化:
#[Cacheable(prefix: 'product', ttl: 3600)]
public function getProduct(int $id)
{
return Product::find($id);
}
效果:
- 首次查询:100ms(数据库)
- 后续查询:2ms(Redis)
3. 协程并发
问题:串行执行多个查询。
优化前:
$user = Db::table('users')->find($id); // 10ms
$orders = Db::table('orders')->where(...)->get(); // 20ms
$profile = Redis::get(...); // 5ms
// 总耗时:35ms
优化后:
[$user, $orders, $profile] = Coroutine::parallel([
fn() => Db::table('users')->find($id),
fn() => Db::table('orders')->where(...)->get(),
fn() => Redis::get(...),
]);
// 总耗时:20ms(取最大值)
4. 连接池配置
// 优化前
'pool' => [
'min_connections' => 1,
'max_connections' => 10,
],
// 优化后
'pool' => [
'min_connections' => 10, // 预热连接
'max_connections' => 32, // 增加最大连接数
'wait_timeout' => 3.0, // 超时时间
],
5. 异步队列
问题:发送邮件阻塞主流程。
优化:
// 推送到队列,异步执行
$this->driver->push(new EmailJob([...]));
return '注册成功'; // 立即返回
4. 话术准备
4.1 项目介绍(30 秒)
我在 [公司] 负责开发一个高并发的电商 API 服务,
使用 Hyperf 框架构建,主要包括用户、订单、支付等模块。
项目峰值 QPS 达到 15,000,支持 10 万+并发连接。
我主要负责核心业务逻辑的开发、性能优化和系统稳定性保障,
通过使用协程、缓存、异步队列等技术,
将接口响应时间从 200ms 优化到 50ms,QPS 提升了 30 倍。
4.2 技术难点(2-3 分钟)
难点 1:高并发秒杀
问题:秒杀场景下,瞬时流量巨大,容易导致库存超卖。
解决方案:
1. 使用 Redis 预热库存
2. 使用 Lua 脚本原子扣减库存
3. 使用异步队列创建订单
4. 使用令牌桶限流
效果:支持 10 万+并发抢购,无超卖现象。
难点 2:协程数据隔离
问题:多个请求在同一个进程中并发执行,
使用全局变量会导致数据串了。
解决方案:
使用协程上下文(Context)存储请求相关数据,
确保每个协程的数据独立。
效果:解决了数据串的问题,系统稳定运行。
难点 3:缓存雪崩
问题:大量缓存同时过期,导致数据库压力骤增。
解决方案:
1. 缓存过期时间加随机值
2. 使用分布式锁防止缓存击穿
3. 缓存空值防止缓存穿透
效果:缓存命中率提升到 95%,数据库压力降低 80%。
4.3 反问问题
- 贵公司的技术栈是什么?是否使用 Hyperf 或 Swoole?
- 贵公司的业务场景是否有高并发需求?
- 团队的技术氛围如何?是否有技术分享?
- 这个岗位的主要职责是什么?
5. 注意事项
5.1 简历数据要真实
- ❌ 不要编造不存在的项目
- ❌ 不要夸大性能数据
- ✅ 基于真实项目,适当美化
- ✅ 数据可以模糊处理(如"数万 QPS"而不是"15,234 QPS")
5.2 准备好代码细节
官可能会问:
- "这段代码怎么写的?"
- "为什么用这个方案?"
- "有没有考虑过其他方案?"
提前准备好核心代码,能够流畅讲解。
5.3 不要背答案
官能看出来你是在背诵还是真正理解。
建议:
- 用自己的话讲述
- 结合实际场景
- 能够展开和深入
祝你成功! 🎉