数据库与缓存详解
Hyperf 中的数据库操作和 Redis 缓存深入实践
1. Hyperf Database 组件
1.1 连接池配置
// config/autoload/databases.php
return [
'default' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 3306),
'database' => env('DB_DATABASE', 'hyperf'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'pool' => [
'min_connections' => 1, // 最小连接数
'max_connections' => 10, // 最大连接数
'connect_timeout' => 10.0, // 连接超时时间
'wait_timeout' => 3.0, // 等待超时时间
'heartbeat' => -1, // 心跳检测间隔
'max_idle_time' => 60, // 最大空闲时间
],
'options' => [
PDO::ATTR_CASE => PDO::CASE_NATURAL,
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL,
PDO::ATTR_STRINGIFY_FETCHES => false,
PDO::ATTR_EMULATE_PREPARES => false,
],
],
];
1.2 Model 操作
use Hyperf\DbConnection\Model\Model;
class User extends Model
{
protected $table = 'users';
protected $fillable = ['name', 'email', 'password'];
protected $hidden = ['password'];
protected $casts = [
'created_at' => 'datetime',
'is_active' => 'boolean',
];
}
// CRUD 操作
// 创建
$user = User::create([
'name' => 'Alice',
'email' => 'alice@example.com',
'password' => bcrypt('password'),
]);
// 查询
$user = User::find(1);
$user = User::where('email', 'alice@example.com')->first();
$users = User::where('is_active', true)->get();
// 更新
$user->update(['name' => 'Bob']);
User::where('id', 1)->update(['name' => 'Bob']);
// 删除
$user->delete();
User::destroy([1, 2, 3]);
1.3 事务
use Hyperf\DbConnection\Db;
// 方式 1:手动事务
Db::beginTransaction();
try {
$user = User::create($userData);
$order = Order::create($orderData);
Db::commit();
} catch (\Exception $e) {
Db::rollBack();
throw $e;
}
// 方式 2:闭包事务(推荐)
Db::transaction(function () use ($userData, $orderData) {
$user = User::create($userData);
$order = Order::create($orderData);
});
2. Redis 操作详解
2.1 Redis 连接池配置
// config/autoload/redis.php
return [
'default' => [
'host' => env('REDIS_HOST', 'localhost'),
'auth' => env('REDIS_AUTH', null),
'port' => (int) env('REDIS_PORT', 6379),
'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' => 60,
],
'options' => [
\Redis::OPT_SERIALIZER => \Redis::SERIALIZER_PHP, // 序列化方式
\Redis::OPT_PREFIX => 'hyperf:', // 键前缀
],
],
];
2.2 基本操作
字符串操作
use Hyperf\Redis\Redis;
class RedisService
{
#[Inject]
protected Redis $redis;
public function stringOperations()
{
// SET/GET
$this->redis->set('key', 'value');
$value = $this->redis->get('key');
// SETEX(设置并带过期时间)
$this->redis->setex('key', 60, 'value'); // 60 秒过期
// SETNX(不存在时设置)
$result = $this->redis->setnx('key', 'value');
// INCR/DECR(原子递增/递减)
$this->redis->incr('counter');
$this->redis->incrby('counter', 10);
$this->redis->decr('counter');
// MGET/MSET(批量操作)
$this->redis->mset(['key1' => 'value1', 'key2' => 'value2']);
$values = $this->redis->mget(['key1', 'key2']);
// APPEND(追加)
$this->redis->append('key', ' world');
// GETSET(获取旧值并设置新值)
$oldValue = $this->redis->getset('key', 'new value');
}
}
哈希操作
public function hashOperations()
{
// HSET/HGET
$this->redis->hset('user:1', 'name', 'Alice');
$name = $this->redis->hget('user:1', 'name');
// HMSET/HMGET(批量操作)
$this->redis->hmset('user:1', [
'name' => 'Alice',
'age' => 25,
'email' => 'alice@example.com'
]);
$data = $this->redis->hmget('user:1', ['name', 'age']);
// HGETALL(获取所有字段)
$user = $this->redis->hgetall('user:1');
// HINCRBY(原子递增)
$this->redis->hincrby('user:1', 'age', 1);
// HDEL(删除字段)
$this->redis->hdel('user:1', 'email');
// HEXISTS(字段是否存在)
$exists = $this->redis->hexists('user:1', 'name');
// HLEN(字段数量)
$count = $this->redis->hlen('user:1');
// HKEYS/HVALS(获取所有键/值)
$keys = $this->redis->hkeys('user:1');
$values = $this->redis->hvals('user:1');
}
列表操作
public function listOperations()
{
// LPUSH/RPUSH(左/右插入)
$this->redis->lpush('list', 'item1');
$this->redis->rpush('list', 'item2');
// LPOP/RPOP(左/右弹出)
$item = $this->redis->lpop('list');
$item = $this->redis->rpop('list');
// LRANGE(范围查询)
$items = $this->redis->lrange('list', 0, -1); // 获取所有
$items = $this->redis->lrange('list', 0, 9); // 获取前 10 个
// LLEN(长度)
$length = $this->redis->llen('list');
// LINDEX(获取指定位置)
$item = $this->redis->lindex('list', 0);
// LSET(设置指定位置)
$this->redis->lset('list', 0, 'new value');
// LTRIM(修剪列表)
$this->redis->ltrim('list', 0, 99); // 保留前 100 个
// BLPOP/BRPOP(阻塞弹出)
$item = $this->redis->blpop('list', 5); // 阻塞 5 秒
}
集合操作
public function setOperations()
{
// SADD(添加成员)
$this->redis->sadd('set', 'member1', 'member2');
// SMEMBERS(获取所有成员)
$members = $this->redis->smembers('set');
// SISMEMBER(成员是否存在)
$exists = $this->redis->sismember('set', 'member1');
// SREM(删除成员)
$this->redis->srem('set', 'member1');
// SCARD(集合大小)
$size = $this->redis->scard('set');
// SPOP(随机弹出)
$member = $this->redis->spop('set');
// SRANDMEMBER(随机获取,不删除)
$member = $this->redis->srandmember('set');
// 集合运算
// 并集
$union = $this->redis->sunion('set1', 'set2');
// 交集
$inter = $this->redis->sinter('set1', 'set2');
// 差集
$diff = $this->redis->sdiff('set1', 'set2');
}
有序集合操作
public function zsetOperations()
{
// ZADD(添加成员)
$this->redis->zadd('leaderboard', 100, 'player1');
$this->redis->zadd('leaderboard', 90, 'player2');
// ZRANGE(按分数排序)
$players = $this->redis->zrange('leaderboard', 0, -1);
// ZREVRANGE(降序)
$players = $this->redis->zrevrange('leaderboard', 0, -1);
// ZRANGEWITHSCORES(带分数)
$players = $this->redis->zrange('leaderboard', 0, -1, true);
// ZRANGEBYSCORE(按分数范围)
$players = $this->redis->zrangebyscore('leaderboard', 80, 100);
// ZINCRBY(增加分数)
$this->redis->zincrby('leaderboard', 10, 'player1');
// ZRANK(排名,从 0 开始)
$rank = $this->redis->zrank('leaderboard', 'player1');
// ZREVRANK(倒序排名)
$rank = $this->redis->zrevrank('leaderboard', 'player1');
// ZSCORE(获取分数)
$score = $this->redis->zscore('leaderboard', 'player1');
// ZREM(删除成员)
$this->redis->zrem('leaderboard', 'player1');
// ZCARD(成员数量)
$count = $this->redis->zcard('leaderboard');
// ZCOUNT(分数范围内的成员数量)
$count = $this->redis->zcount('leaderboard', 80, 100);
}
2.3 高级操作
管道(Pipeline)
// 批量操作,减少网络往返
public function pipelineExample()
{
$results = $this->redis->pipeline(function ($pipe) {
$pipe->set('key1', 'value1');
$pipe->set('key2', 'value2');
$pipe->set('key3', 'value3');
$pipe->get('key1');
$pipe->get('key2');
$pipe->get('key3');
});
// $results = [true, true, true, 'value1', 'value2', 'value3']
}
事务(Transaction)
public function transactionExample()
{
// 使用 MULTI/EXEC
$results = $this->redis->multi(function ($tx) {
$tx->set('key1', 'value1');
$tx->set('key2', 'value2');
$tx->incr('counter');
});
// 使用 WATCH(乐观锁)
$this->redis->watch('key');
$value = $this->redis->get('key');
if ($value < 100) {
$this->redis->multi();
$this->redis->set('key', $value + 1);
$result = $this->redis->exec(); // 如果 key 被修改,返回 false
}
}
Lua 脚本
public function luaExample()
{
// 原子性执行复杂逻辑
$script = <<<LUA
local current = redis.call('get', KEYS[1])
if current and tonumber(current) > tonumber(ARGV[1]) then
return redis.call('decrby', KEYS[1], ARGV[1])
else
return -1
end
LUA;
$result = $this->redis->eval($script, ['balance:1', 100], 1);
if ($result == -1) {
// 余额不足
}
}
// 限流脚本
public function rateLimitScript()
{
$script = <<<LUA
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local expire = tonumber(ARGV[2])
local current = redis.call('incr', key)
if current == 1 then
redis.call('expire', key, expire)
end
if current > limit then
return 0
else
return 1
end
LUA;
$allowed = $this->redis->eval($script, ['rate:' . $userId, 100, 60], 1);
if (!$allowed) {
throw new \Exception('Too many requests');
}
}
分布式锁
class RedisLock
{
#[Inject]
protected Redis $redis;
/**
* 获取分布式锁
*/
public function lock(string $key, int $ttl = 10): bool
{
$value = uniqid(); // 唯一标识
$result = $this->redis->set($key, $value, ['NX', 'EX' => $ttl]);
if ($result) {
// 保存锁的值,用于释放时验证
Context::set("lock:{$key}", $value);
return true;
}
return false;
}
/**
* 释放分布式锁(使用 Lua 保证原子性)
*/
public function unlock(string $key): bool
{
$value = Context::get("lock:{$key}");
$script = <<<LUA
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
LUA;
return (bool) $this->redis->eval($script, [$key, $value], 1);
}
}
// 使用
$lock = new RedisLock();
if ($lock->lock('resource:1', 10)) {
try {
// 执行业务逻辑
} finally {
$lock->unlock('resource:1');
}
}
2.4 Hyperf 缓存注解
@Cacheable(缓存查询结果)
use Hyperf\Cache\Annotation\Cacheable;
class UserService
{
/**
* 缓存用户信息
*
* @Cacheable(prefix="user", ttl=3600, listener="user-update")
*/
public function getUserById(int $id): ?User
{
// 首次查询会执行,结果会被缓存
// 后续查询直接返回缓存结果
return User::find($id);
}
}
@CachePut(更新缓存)
use Hyperf\Cache\Annotation\CachePut;
class UserService
{
/**
* 更新用户并刷新缓存
*
* @CachePut(prefix="user", ttl=3600)
*/
public function updateUser(int $id, array $data): User
{
$user = User::find($id);
$user->update($data);
// 返回值会更新缓存
return $user;
}
}
@CacheEvict(删除缓存)
use Hyperf\Cache\Annotation\CacheEvict;
class UserService
{
/**
* 删除用户并清除缓存
*
* @CacheEvict(prefix="user", all=false)
*/
public function deleteUser(int $id): bool
{
return User::destroy($id);
}
}
3. 缓存策略
3.1 Cache-Aside(旁路缓存)
class UserService
{
public function getUser(int $id): ?User
{
// 1. 查询缓存
$cacheKey = "user:{$id}";
$cached = $this->redis->get($cacheKey);
if ($cached) {
return unserialize($cached);
}
// 2. 缓存未命中,查询数据库
$user = User::find($id);
if ($user) {
// 3. 写入缓存
$this->redis->setex($cacheKey, 3600, serialize($user));
}
return $user;
}
public function updateUser(int $id, array $data): User
{
// 1. 更新数据库
$user = User::find($id);
$user->update($data);
// 2. 删除缓存(下次查询时重新缓存)
$this->redis->del("user:{$id}");
return $user;
}
}
3.2 缓存穿透
// 问题:查询不存在的数据,缓存和数据库都没有,导致每次都查询数据库
// 解决方案 1:缓存空值
public function getUser(int $id): ?User
{
$cacheKey = "user:{$id}";
$cached = $this->redis->get($cacheKey);
if ($cached !== false) {
return $cached === 'NULL' ? null : unserialize($cached);
}
$user = User::find($id);
if ($user) {
$this->redis->setex($cacheKey, 3600, serialize($user));
} else {
// 缓存空值,过期时间较短
$this->redis->setex($cacheKey, 60, 'NULL');
}
return $user;
}
// 解决方案 2:布隆过滤器
use Hyperf\Redis\RedisBloom;
class UserService
{
#[Inject]
protected RedisBloom $bloom;
public function getUser(int $id): ?User
{
// 先检查布隆过滤器
if (!$this->bloom->exists("users", (string)$id)) {
return null; // 一定不存在
}
// 可能存在,查询缓存和数据库
return $this->getUserFromCacheOrDb($id);
}
public function createUser(array $data): User
{
$user = User::create($data);
// 添加到布隆过滤器
$this->bloom->add("users", (string)$user->id);
return $user;
}
}
3.3 缓存击穿
// 问题:热点 key 过期,瞬间大量请求打到数据库
// 解决方案 1:互斥锁
public function getUser(int $id): ?User
{
$cacheKey = "user:{$id}";
$lockKey = "lock:user:{$id}";
// 1. 查询缓存
$cached = $this->redis->get($cacheKey);
if ($cached) {
return unserialize($cached);
}
// 2. 获取锁
if ($this->redis->set($lockKey, 1, ['NX', 'EX' => 10])) {
try {
// 3. 双重检查
$cached = $this->redis->get($cacheKey);
if ($cached) {
return unserialize($cached);
}
// 4. 查询数据库
$user = User::find($id);
// 5. 写入缓存
if ($user) {
$this->redis->setex($cacheKey, 3600, serialize($user));
}
return $user;
} finally {
// 6. 释放锁
$this->redis->del($lockKey);
}
}
// 7. 获取锁失败,等待并重试
usleep(50000); // 50ms
return $this->getUser($id);
}
// 解决方案 2:永不过期(逻辑过期)
public function getUser(int $id): ?User
{
$cacheKey = "user:{$id}";
$cached = $this->redis->get($cacheKey);
if ($cached) {
$data = unserialize($cached);
// 检查逻辑过期时间
if ($data['expire_at'] < time()) {
// 异步更新缓存
go(function () use ($id, $cacheKey) {
$user = User::find($id);
$this->redis->set($cacheKey, serialize([
'data' => $user,
'expire_at' => time() + 3600,
]));
});
}
return $data['data'];
}
// 首次查询
$user = User::find($id);
$this->redis->set($cacheKey, serialize([
'data' => $user,
'expire_at' => time() + 3600,
]));
return $user;
}
3.4 缓存雪崩
// 问题:大量 key 同时过期,数据库压力骤增
// 解决方案:过期时间加随机值
public function setCache(string $key, $value, int $ttl = 3600): bool
{
// 加上 0-300 秒的随机时间
$randomTtl = $ttl + rand(0, 300);
return $this->redis->setex($key, $randomTtl, serialize($value));
}
4. 高频问题
Q1: Hyperf 的数据库连接池如何工作?
答案: Hyperf 使用协程连接池管理数据库连接:
- 初始化时创建 min_connections 个连接
- 请求时从池中获取连接
- 使用完毕归还到池中
- 超过 max_connections 时等待或超时
- 定期检查连接健康状态(heartbeat)
优势:
- 连接复用,避免频繁创建
- 协程安全
- 自动重连
Q2: Redis 的数据类型有哪些?各有什么应用场景?
答案:
- String:缓存、计数器、分布式锁
- Hash:用户信息、商品信息
- List:消息队列、时间线
- Set:去重、共同好友、标签
- ZSet:排行榜、延时队列
Q3: 如何解决缓存穿透、击穿、雪崩?
答案:见上面 3.2、3.3、3.4 节
Q4: Redis 分布式锁如何实现?
答案:
// 1. 使用 SET NX EX 获取锁
$result = $redis->set($key, $value, ['NX', 'EX' => 10]);
// 2. 使用 Lua 脚本释放锁(保证原子性)
$script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
$redis->eval($script, [$key, $value], 1);
Q5: Hyperf 缓存注解的原理?
答案: Hyperf 使用 AOP(面向切面编程)实现缓存注解:
- 扫描注解,生成代理类
- 方法调用前,检查缓存
- 缓存命中,直接返回
- 缓存未命中,执行方法,缓存结果
- 支持缓存更新、删除
5. 总结
数据库与缓存核心:
- ✅ 连接池配置和优化
- ✅ Model 操作和事务
- ✅ Redis 数据类型和高级操作
- ✅ 缓存策略(Cache-Aside)
- ✅ 缓存问题解决(穿透、击穿、雪崩)
- ✅ 分布式锁
- ✅ 缓存注解
推荐学习:
- Hyperf 数据库文档
- Redis 官方文档
- 实践:搭建高可用缓存架构