09-项目实战

14 阅读6分钟

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 优化前后对比

指标优化前优化后提升
QPS50015,00030x
响应时间200ms50ms4x
并发连接1,000100,000100x

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 反问问题

  1. 贵公司的技术栈是什么?是否使用 Hyperf 或 Swoole?
  2. 贵公司的业务场景是否有高并发需求?
  3. 团队的技术氛围如何?是否有技术分享?
  4. 这个岗位的主要职责是什么?

5. 注意事项

5.1 简历数据要真实

  • ❌ 不要编造不存在的项目
  • ❌ 不要夸大性能数据
  • ✅ 基于真实项目,适当美化
  • ✅ 数据可以模糊处理(如"数万 QPS"而不是"15,234 QPS")

5.2 准备好代码细节

官可能会问:

  • "这段代码怎么写的?"
  • "为什么用这个方案?"
  • "有没有考虑过其他方案?"

提前准备好核心代码,能够流畅讲解。

5.3 不要背答案

官能看出来你是在背诵还是真正理解。

建议:

  • 用自己的话讲述
  • 结合实际场景
  • 能够展开和深入

祝你成功! 🎉