05-数据库与缓存详解

9 阅读6分钟

数据库与缓存详解

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 的数据类型有哪些?各有什么应用场景?

答案

  1. String:缓存、计数器、分布式锁
  2. Hash:用户信息、商品信息
  3. List:消息队列、时间线
  4. Set:去重、共同好友、标签
  5. 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(面向切面编程)实现缓存注解:

  1. 扫描注解,生成代理类
  2. 方法调用前,检查缓存
  3. 缓存命中,直接返回
  4. 缓存未命中,执行方法,缓存结果
  5. 支持缓存更新、删除

5. 总结

数据库与缓存核心:

  1. ✅ 连接池配置和优化
  2. ✅ Model 操作和事务
  3. ✅ Redis 数据类型和高级操作
  4. ✅ 缓存策略(Cache-Aside)
  5. ✅ 缓存问题解决(穿透、击穿、雪崩)
  6. ✅ 分布式锁
  7. ✅ 缓存注解

推荐学习

  • Hyperf 数据库文档
  • Redis 官方文档
  • 实践:搭建高可用缓存架构