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

今日目标
- 配置 Redis 连接池,理解连接池在常驻内存下的重要性。
- 使用
hyperf/cache和hyperf/redis实现数据缓存,掌握注解@Cacheable等用法。 - 理解 PSR-14 事件规范,掌握 Hyperf 事件组件的使用。
- 编写用户登录接口,结合 Redis 缓存用户信息,并监听登录事件异步写入日志。
- 通过压测和 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_connections和min_connections,观察pool::getStats()的方法(需自定义命令)监控连接使用情况。 - 事件驱动架构的未来:我们可以将事件通过消息队列投递(如 RabbitMQ),实现跨服务的异步处理,这正是微服务松耦合的核心。
- 缓存与数据库同步:更复杂的场景下,我们需要使用
CacheAside模式或Write-Through,后续学数据库时会结合模型事件实现。
六、今日作业与学习产出
- 提交代码:将
AuthController、LoginEvent、LoginLogListener、UserController及相关配置提交到 Git。 - 学习笔记:绘制事件分发的时序图,包括登录接口、事件分发、监听器执行和日志写入的全过程。
- 实战拓展:
- 创建一个
LogoutEvent及监听器,清除用户缓存。 - 使用
@Cacheable注解实现一个文章列表接口,设置合适的 TTL,并测试缓存生效。
- 创建一个
- 思考:为什么连接池在 Swoole 中如此重要?如果不用连接池,每个协程都新建 Redis 连接,会有什么后果?
今天的内容将缓存和事件驱动两大支柱注入到你的技术栈中,使你的微服务不仅高性能,而且高度解耦。明天我们将把这些组件整合,完成一个完整的单服务实战——文章系统。
更多推荐
所有评论(0)