今天主题是 Redis 缓存、连接池与事件机制。缓存是高性能系统的核心,而事件驱动则是解耦业务逻辑的利器。今天你将学会在 Hyperf 中优雅地操作 Redis,并利用事件机制实现登录日志异步记录,让系统更加灵活高效。


在这里插入图片描述

今日目标

  1. 配置 Redis 连接池,理解连接池在常驻内存下的重要性。
  2. 使用 hyperf/cachehyperf/redis 实现数据缓存,掌握注解 @Cacheable 等用法。
  3. 理解 PSR-14 事件规范,掌握 Hyperf 事件组件的使用。
  4. 编写用户登录接口,结合 Redis 缓存用户信息,并监听登录事件异步写入日志。
  5. 通过压测和 Redis 监控,验证缓存加速效果和事件处理的解耦能力。

一、环境准备(约 30 分钟)

继续在 hyperf-app 项目中工作,并确保 Redis 服务可用。我们的 Docker 环境还没有 Redis,需要添加。

1. 修改 docker-compose.yml 添加 Redis 服务

swoole-course/docker-compose.yml 中增加:

services:
  swoole:
    # ... 原有配置 ...
  redis:
    image: redis:7-alpine
    container_name: redis-lab
    ports:
      - "6379:6379"
    restart: unless-stopped

然后重启容器:

docker-compose up -d

进入 Swoole 容器后,用 ping 测试 Redis 连通性:

docker-compose exec swoole bash
redis-cli -h redis ping   # 应返回 PONG
2. 安装 Redis 与 Cache 组件
cd /var/www/hyperf-app
composer require hyperf/redis hyperf/cache

发布缓存配置:

php bin/hyperf.php vendor:publish hyperf/cache

这会在 config/autoload/ 下生成 cache.php

3. 配置 Redis 连接

编辑 config/autoload/redis.php(若没有则创建),配置连接池:

<?php
return [
    'default' => [
        'host' => env('REDIS_HOST', 'redis'), // 对应 docker-compose 服务名
        'port' => (int) env('REDIS_PORT', 6379),
        'auth' => env('REDIS_AUTH', null),
        'db'   => (int) env('REDIS_DB', 0),
        'pool' => [
            'min_connections' => 1,
            'max_connections' => 10,
            'connect_timeout' => 10.0,
            'wait_timeout' => 3.0,
            'heartbeat' => -1,
            'max_idle_time' => (float) env('REDIS_MAX_IDLE_TIME', 60),
        ],
    ],
];

缓存配置文件 config/autoload/cache.php 使用 Redis 驱动:

<?php
return [
    'default' => [
        'driver' => Hyperf\Cache\Driver\RedisDriver::class,
        'packer' => Hyperf\Utils\Packer\PhpSerializerPacker::class,
        'prefix' => 'c:',
    ],
];

注意:前缀可自定义,便于区分缓存键。

4. 验证 Redis 可用

创建测试路由,直接在控制器中注入 Hyperf\Redis\Redis

use Hyperf\Redis\Redis;
// ...
#[Inject]
protected Redis $redis;

public function testRedis() {
    $this->redis->set('test', 'hello redis');
    return ['data' => $this->redis->get('test')];
}

访问即可验证。成功后删除测试代码。


二、知识核心:缓存与事件机制(约 1.5 小时)

1. Redis 连接池原理

在传统 PHP-FPM 中,每次请求结束都会销毁 Redis 连接,高并发时连接开销巨大。Swoole 的常驻内存允许我们复用连接,但需要避免协程间共享连接导致数据混乱。Hyperf 的连接池保证:

  • 每个协程从池中借用连接,使用后归还。
  • 池大小根据 max_connections 控制,防止连接数爆炸。
  • 自动心跳和断线重连。

我们只需通过依赖注入获取 Redis 对象,即可安全使用,框架自动管理连接。

2. 注解驱动的缓存

hyperf/cache 提供了类似 Spring 的缓存注解:

  • @Cacheable(prefix="user", ttl=3600):先从缓存取,不存在则执行方法体并缓存结果。
  • @CachePut(prefix="user"):每次都执行方法,并更新缓存。
  • @CacheEvict(prefix="user", all=true):清除缓存。
    这些注解是通过 AOP 代理实现的,与周一学的内容完美契合。
3. PSR-14 事件机制

事件系统包含三个角色:

  • 事件:一个普通的 PHP 对象,携带需要传递的数据。
  • 监听器:响应事件的类,实现 Hyperf\Event\Contract\ListenerInterface
  • 事件调度器:负责触发事件,通知所有监听器。

我们在 LoginEvent 中存放用户信息,然后通过 EventDispatcher 分发,监听器可以异步记录日志、发邮件等,完全不侵入业务代码。Hyperf 的事件支持协程,监听器默认在触发事件的协程中执行,但我们也可以配置为通过队列异步执行(未来学队列时可升级)。


三、实战:登录缓存与事件记录(约 2.5 小时)

步骤 1:创建用户登录控制器

新建 app/Controller/AuthController.php

<?php
namespace App\Controller;

use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
use App\Event\LoginEvent;
use Psr\EventDispatcher\EventDispatcherInterface;

#[Controller(prefix: '/auth')]
class AuthController extends AbstractController
{
    #[Inject]
    protected Redis $redis;

    #[Inject]
    protected EventDispatcherInterface $eventDispatcher;

    #[RequestMapping(path: 'login', methods: 'post')]
    public function login()
    {
        $username = $this->request->input('username');
        $password = $this->request->input('password');

        // 模拟用户验证(实际应查库)
        if ($username !== 'admin' || $password !== '123456') {
            return ['code' => 401, 'message' => '用户名或密码错误'];
        }

        $userData = [
            'id'       => 1,
            'username' => 'admin',
            'email'    => 'admin@example.com',
            'last_login' => date('Y-m-d H:i:s'),
        ];

        // 将用户信息缓存到 Redis,有效期 3600 秒
        $this->redis->set('user:1', json_encode($userData), 3600);

        // 分发登录事件
        $this->eventDispatcher->dispatch(new LoginEvent($userData));

        return [
            'code'    => 200,
            'message' => '登录成功',
            'data'    => $userData,
        ];
    }
}
步骤 2:创建登录事件类

新建 app/Event/LoginEvent.php

<?php
namespace App\Event;

class LoginEvent
{
    public array $user;

    public function __construct(array $user)
    {
        $this->user = $user;
    }
}
步骤 3:创建事件监听器

新建 app/Listener/LoginLogListener.php

<?php
namespace App\Listener;

use App\Event\LoginEvent;
use Hyperf\Event\Contract\ListenerInterface;
use Psr\Log\LoggerInterface;
use Hyperf\Di\Annotation\Inject;

class LoginLogListener implements ListenerInterface
{
    #[Inject]
    protected LoggerInterface $logger;

    public function listen(): array
    {
        // 返回要监听的事件类数组
        return [
            LoginEvent::class,
        ];
    }

    public function process(object $event)
    {
        /** @var LoginEvent $event */
        $user = $event->user;
        // 记录登录日志(这里写入 log 文件,实际可写数据库)
        $this->logger->info(sprintf(
            "用户 %s (ID:%d) 于 %s 登录成功",
            $user['username'],
            $user['id'],
            date('Y-m-d H:i:s')
        ));
    }
}

注意:监听器的 process 方法默认在触发事件的同一协程中同步执行,这可能会略微增加请求耗时。我们可以通过配置队列异步执行,但今天暂不涉及,先体验机制。

步骤 4:使用缓存注解优化用户信息获取

为了展示 @Cacheable 的神奇,我们再创建一个获取用户信息的接口,从 Redis 缓存读取。

新建 app/Controller/UserController.php(如果已有则追加方法):

<?php
namespace App\Controller;

use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\RequestMapping;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Redis\Redis;
use Hyperf\Cache\Annotation\Cacheable;

#[Controller(prefix: '/user')]
class UserController extends AbstractController
{
    #[Inject]
    protected Redis $redis;

    #[RequestMapping(path: 'info/{id}', methods: 'get')]
    #[Cacheable(prefix: 'user', ttl: 600)]
    public function info(int $id)
    {
        // 此方法体只在缓存未命中时执行
        $userJson = $this->redis->get('user:' . $id);
        if (!$userJson) {
            return ['code' => 404, 'message' => '用户不存在'];
        }
        return json_decode($userJson, true);
    }
}

解读@Cacheable 注解会以 prefix:arguments 的格式生成缓存键(例如 user:1)。第一次请求 /user/info/1 会执行方法体,返回用户信息并缓存;后续请求直接返回缓存数据,不走方法体,也不查询 Redis!这相当于在应用层又加了一层缓存,减少了对 Redis 的网络调用,速度更快。

注意@Cacheable 的缓存驱动由 cache.php 配置决定,默认就是 Redis。值经过 packer 序列化,所以可以直接返回数组。

步骤 5:测试整个流程

重启服务(热重启或手动),使用 curl 测试登录:

curl -X POST http://localhost:9501/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"123456"}'

返回登录成功,同时查看 runtime/logs/hyperf.log,应能看到 LoginLogListener 记录的日志。

检查 Redis 中的缓存:

redis-cli -h redis get user:1
# 返回 JSON 字符串

测试缓存注解:

curl http://localhost:9501/user/info/1

第一次返回用户信息,第二次再次请求,你会发现响应速度更快,且控制台没有执行方法体的痕迹(如果我们在方法体内打 echo 的话)。可以使用 Redis 的 MONITOR 命令观察:

redis-cli -h redis monitor

只有第一次请求会有 GET 命令(如果方法体内没有手动 Redis 操作,注解缓存走的是 Cache 组件的内部存储,不经过 Redis 命令。实际上 @Cacheable 使用的是缓存驱动,Redis 驱动会 GET/SET,但注解生成的键带有 c: 前缀,因此你会看到 c:user:1 的读写)。这证明了两级缓存:应用级缓存注解直接返回,避免了 Redis 查询。


四、高级实战:事件异步化与缓存击穿防护(约 1 小时)

1. 将监听器配置为异步执行

要将监听器改为异步执行(协程并发),只需在监听器类上添加 #[Listener(priority: 1, async: true)] 注解(若用传统配置,则在 config/autoload/listeners.php 中注册)。我们使用注解方式:

use Hyperf\Event\Annotation\Listener;

#[Listener(async: true)]
class LoginLogListener implements ListenerInterface
{
    // ...
}

重启服务后,登录时,监听器的执行会放到新的协程中,不阻塞登录接口的响应。

2. 防止缓存穿透/击穿

@Cacheable 注解支持 lock 机制防止缓存击穿:

#[Cacheable(prefix: 'user', ttl: 600, lock: true)]

当多个请求同时触发缓存未命中,只有一个会执行方法体,其他等待锁释放后直接读取缓存。

3. 使用 @CacheEvict 清除缓存

修改登录接口,当重新登录时,清除旧的用户缓存,以便获取最新信息:

use Hyperf\Cache\Annotation\CacheEvict;

#[RequestMapping(path: 'login', methods: 'post')]
#[CacheEvict(prefix: 'user', value: '#{input("username")}')] // 清除 user:admin 缓存
public function login() { ... }

这里的 value 支持 SpEL 表达式,#{input("username")} 会获取请求中的 username 参数。


五、成果测试与总结(约 1 小时)

1. 测试清单
检验项方法通过标准
Redis 连接池高并发访问任意 Redis 操作接口无连接耗尽报错,pool::getConnection 正常
登录缓存用户信息POST 登录后 redis-cli get user:1返回 JSON 数据
缓存注解生效第一次请求 /user/info/1 后,再次请求第二次请求速度提升,且 Redis MONITOR 无 GET(或仅一次)
事件监听同步/异步登录后观察 hyperf.log 日志同步时日志在响应前打印,异步时延迟极短
事件监听解耦注释掉监听器,登录接口依然正常业务与日志完全解耦
缓存清除修改密码重新登录,检查缓存键是否存在旧缓存被删除,新数据被写入
2. 进阶思考
  • 连接池配置优化:通过压测调整 max_connectionsmin_connections,观察 pool::getStats() 的方法(需自定义命令)监控连接使用情况。
  • 事件驱动架构的未来:我们可以将事件通过消息队列投递(如 RabbitMQ),实现跨服务的异步处理,这正是微服务松耦合的核心。
  • 缓存与数据库同步:更复杂的场景下,我们需要使用 CacheAside 模式或 Write-Through,后续学数据库时会结合模型事件实现。

六、今日作业与学习产出

  1. 提交代码:将 AuthControllerLoginEventLoginLogListenerUserController 及相关配置提交到 Git。
  2. 学习笔记:绘制事件分发的时序图,包括登录接口、事件分发、监听器执行和日志写入的全过程。
  3. 实战拓展
    • 创建一个 LogoutEvent 及监听器,清除用户缓存。
    • 使用 @Cacheable 注解实现一个文章列表接口,设置合适的 TTL,并测试缓存生效。
  4. 思考:为什么连接池在 Swoole 中如此重要?如果不用连接池,每个协程都新建 Redis 连接,会有什么后果?

今天的内容将缓存和事件驱动两大支柱注入到你的技术栈中,使你的微服务不仅高性能,而且高度解耦。明天我们将把这些组件整合,完成一个完整的单服务实战——文章系统。

更多推荐