数据库与缓存
1. 数据库连接池
1.1 为什么需要连接池?
传统 PHP-FPM 模式:
请求1 → 创建连接 → 查询 → 关闭连接
请求2 → 创建连接 → 查询 → 关闭连接
请求3 → 创建连接 → 查询 → 关闭连接
每次都要:
1. TCP 三次握手
2. MySQL 认证
3. TCP 四次挥手
Hyperf 连接池模式:
启动时:创建 10 个连接,放入连接池
请求1 → 从池中获取连接 → 查询 → 归还连接
请求2 → 从池中获取连接 → 查询 → 归还连接
请求3 → 从池中获取连接 → 查询 → 归还连接
复用连接,无需重复创建和销毁
1.2 连接池配置
config/autoload/databases.php:
<?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' => 10, // 最小连接数(预热)
'max_connections' => 32, // 最大连接数
'connect_timeout' => 10.0, // 连接超时时间
'wait_timeout' => 3.0, // 等待超时时间
'heartbeat' => -1, // 心跳检测间隔(-1 表示禁用)
'max_idle_time' => 60, // 最大空闲时间
],
'commands' => [
'gen:model' => [
'path' => 'app/Model',
'force_casts' => true,
'inheritance' => 'Model',
],
],
],
];
参数说明:
| 参数 | 说明 | 推荐值 |
|---|---|---|
| min_connections | 最小连接数,启动时创建 | 10 |
| max_connections | 最大连接数,超过则等待 | 32(CPU 核心数 * 2) |
| connect_timeout | 连接超时时间(秒) | 10.0 |
| wait_timeout | 等待可用连接的超时时间(秒) | 3.0 |
| heartbeat | 心跳检测间隔(秒),-1 表示禁用 | -1 |
| max_idle_time | 连接最大空闲时间(秒) | 60 |
1.3 连接池监控
<?php
use Hyperf\DbConnection\Pool\PoolFactory;
use Hyperf\Di\Annotation\Inject;
class MonitorService
{
#[Inject]
private PoolFactory $poolFactory;
public function getPoolStatus()
{
$pool = $this->poolFactory->getPool('default');
return [
'current_connections' => $pool->getCurrentConnections(), // 当前连接数
'max_connections' => $pool->getOption()->getMaxConnections(),
'min_connections' => $pool->getOption()->getMinConnections(),
'idle_count' => $pool->getConnectionsInChannel(), // 空闲连接数
];
}
}
2. 数据库查询优化
2.1 避免 N+1 查询
问题代码:
<?php
// 查询所有用户(1 次查询)
$users = User::all();
// 循环查询每个用户的订单(N 次查询)
foreach ($users as $user) {
echo $user->orders->count(); // 每次都查询数据库
}
// 总查询次数:1 + N
优化方案:使用预加载
<?php
// 只查询 2 次(1 次用户 + 1 次订单)
$users = User::with('orders')->get();
foreach ($users as $user) {
echo $user->orders->count(); // 直接从内存读取
}
2.2 使用索引
问题查询:
<?php
// 查询邮箱(未建索引)
$user = User::where('email', 'test@example.com')->first();
// 执行计划:全表扫描,扫描 10 万行
优化方案:
<?php
// 1. 创建索引
Schema::table('users', function (Blueprint $table) {
$table->index('email');
});
// 2. 查询
$user = User::where('email', 'test@example.com')->first();
// 执行计划:索引扫描,只扫描 1 行
2.3 批量操作
问题代码:
<?php
// 循环插入(1000 次 SQL)
foreach ($data as $item) {
User::create($item);
}
优化方案:
<?php
// 批量插入(1 次 SQL)
User::insert($data);
// 或者使用 chunk 分批插入(避免单次数据过大)
collect($data)->chunk(100)->each(function ($chunk) {
User::insert($chunk->toArray());
});
2.4 只查询需要的字段
问题代码:
<?php
// 查询所有字段
$users = User::where('status', 1)->get();
// 实际只需要 id 和 name
优化方案:
<?php
// 只查询需要的字段
$users = User::where('status', 1)->select('id', 'name')->get();
// 或者使用 pluck(只返回单列)
$names = User::where('status', 1)->pluck('name');
2.5 使用 chunk 处理大量数据
问题代码:
<?php
// 一次性加载 100 万条数据到内存
$users = User::all();
foreach ($users as $user) {
// 处理用户
}
// 内存占用:几百 MB
优化方案:
<?php
// 分批处理,每次 1000 条
User::chunk(1000, function ($users) {
foreach ($users as $user) {
// 处理用户
}
});
// 内存占用:几 MB
3. 缓存策略
3.1 缓存使用场景
适合缓存的数据:
- ✅ 读多写少的数据(商品信息、配置项)
- ✅ 计算复杂的数据(统计数据、排行榜)
- ✅ 热点数据(首页数据、热门商品)
不适合缓存的数据:
- ❌ 实时性要求高的数据(订单状态、库存)
- ❌ 个性化数据(用户偏好设置)
- ❌ 写多读少的数据
3.2 缓存注解
@Cacheable(生成缓存)
<?php
use Hyperf\Cache\Annotation\Cacheable;
class ProductService
{
/**
* 查询商品(首次查询数据库,后续查询缓存)
*/
#[Cacheable(
prefix: 'product', // 缓存前缀
ttl: 3600, // 过期时间(秒)
value: '#{id}', // 缓存 key(支持 SpEL 表达式)
listener: 'product-update' // 监听器(可选)
)]
public function getProduct(int $id)
{
return Product::find($id);
}
}
生成的缓存 key:c:product:1(c 是固定前缀)
@CachePut(更新缓存)
<?php
use Hyperf\Cache\Annotation\CachePut;
class ProductService
{
/**
* 更新商品(每次都执行方法,并更新缓存)
*/
#[CachePut(
prefix: 'product',
ttl: 3600,
value: '#{product.id}' // 从方法参数获取 key
)]
public function updateProduct(array $product)
{
$model = Product::find($product['id']);
$model->update($product);
return $model;
}
}
@CacheEvict(删除缓存)
<?php
use Hyperf\Cache\Annotation\CacheEvict;
class ProductService
{
/**
* 删除商品(删除对应的缓存)
*/
#[CacheEvict(
prefix: 'product',
value: '#{id}'
)]
public function deleteProduct(int $id)
{
Product::destroy($id);
}
/**
* 删除所有商品缓存
*/
#[CacheEvict(
prefix: 'product',
all: true // 删除所有缓存
)]
public function clearAllCache()
{
// 清空缓存
}
}
3.3 手动操作缓存
<?php
use Hyperf\Redis\Redis;
use Hyperf\Di\Annotation\Inject;
class CacheService
{
#[Inject]
private Redis $redis;
/**
* 获取缓存
*/
public function get(string $key, callable $callback, int $ttl = 3600)
{
// 1. 尝试从缓存获取
$value = $this->redis->get($key);
if ($value !== null) {
return json_decode($value, true);
}
// 2. 缓存未命中,执行回调
$data = $callback();
// 3. 保存到缓存
if ($data !== null) {
$this->redis->setex($key, $ttl, json_encode($data));
}
return $data;
}
/**
* 设置缓存
*/
public function set(string $key, $value, int $ttl = 3600)
{
$this->redis->setex($key, $ttl, json_encode($value));
}
/**
* 删除缓存
*/
public function delete(string $key)
{
$this->redis->del($key);
}
/**
* 批量删除(模糊匹配)
*/
public function deletePattern(string $pattern)
{
$keys = $this->redis->keys($pattern);
if ($keys) {
$this->redis->del(...$keys);
}
}
}
3.4 缓存穿透、击穿、雪崩
缓存穿透(查询不存在的数据)
问题:
// 用户恶意查询不存在的 ID(如 -1)
$product = $this->getProduct(-1);
// 每次都查询数据库,缓存无效
解决方案:缓存空值
<?php
public function getProduct(int $id)
{
$key = "product:{$id}";
$value = $this->redis->get($key);
// 缓存命中(包括空值)
if ($value !== false) {
return $value === 'null' ? null : json_decode($value, true);
}
// 查询数据库
$product = Product::find($id);
if ($product === null) {
// 缓存空值(短时间,如 60 秒)
$this->redis->setex($key, 60, 'null');
} else {
// 缓存数据
$this->redis->setex($key, 3600, json_encode($product));
}
return $product;
}
缓存击穿(热点 key 过期)
问题:
时间:2:00:00 - 热点商品的缓存过期
瞬间 10000 个请求同时到达
所有请求都去查询数据库
数据库压力骤增,可能宕机
解决方案:分布式锁
<?php
public function getProduct(int $id)
{
$key = "product:{$id}";
$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->getProduct($id);
}
try {
// 双重检查
$value = $this->redis->get($key);
if ($value !== false) {
return json_decode($value, true);
}
// 查询数据库
$product = Product::find($id);
// 保存到缓存
$this->redis->setex($key, 3600, json_encode($product));
return $product;
} 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 同时过期)
问题:
时间:2:00:00 - 1000 个商品的缓存同时过期
瞬间大量请求查询数据库
数据库压力骤增
解决方案:过期时间加随机值
<?php
public function setCache(string $key, $value, int $baseTTL = 3600)
{
// 过期时间加 0-10 分钟的随机值
$ttl = $baseTTL + mt_rand(0, 600);
$this->redis->setex($key, $ttl, json_encode($value));
}
3.5 多级缓存
<?php
use Psr\SimpleCache\CacheInterface; // 本地缓存
class ProductService
{
#[Inject]
private Redis $redis; // Redis 缓存
#[Inject]
private CacheInterface $cache; // 本地缓存(进程内存)
public function getProduct(int $id)
{
$key = "product:{$id}";
// 1. 本地缓存(最快,但只在单个进程有效)
$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) {
$this->redis->setex($key, 3600, json_encode($product));
$this->cache->set($key, $product, 60);
}
return $product;
}
}
优势:
- 本地缓存:无网络开销,极快
- Redis 缓存:全局共享,持久化
- 数据库:数据源
4. 事务处理
4.1 基本事务
<?php
use Hyperf\DbConnection\Db;
// 方式一:闭包
Db::transaction(function () {
// 创建订单
$order = Order::create([...]);
// 扣减库存
Product::where('id', $productId)->decrement('stock');
// 如果发生异常,自动回滚
});
// 方式二:手动控制
Db::beginTransaction();
try {
// 业务逻辑
$order = Order::create([...]);
Product::where('id', $productId)->decrement('stock');
Db::commit();
} catch (\Exception $e) {
Db::rollBack();
throw $e;
}
4.2 事务注解(AOP)
<?php
namespace App\Aspect;
use App\Annotation\Transaction;
use Hyperf\DbConnection\Db;
use Hyperf\Di\Annotation\Aspect;
use Hyperf\Di\Aop\AbstractAspect;
use Hyperf\Di\Aop\ProceedingJoinPoint;
#[Aspect]
class TransactionAspect extends AbstractAspect
{
public array $annotations = [
Transaction::class,
];
public function process(ProceedingJoinPoint $proceedingJoinPoint)
{
return Db::transaction(function () use ($proceedingJoinPoint) {
return $proceedingJoinPoint->process();
});
}
}
使用:
<?php
use App\Annotation\Transaction;
class OrderService
{
#[Transaction]
public function createOrder(array $data)
{
// 自动开启事务
$order = Order::create($data);
Product::where('id', $data['product_id'])->decrement('stock');
// 方法执行成功,自动提交
// 如果抛出异常,自动回滚
return $order;
}
}
5. 要点
必须掌握
- 连接池的作用和配置
- N+1 查询问题及解决方案
- 缓存的使用场景
- 缓存穿透、击穿、雪崩的区别和解决方案
- 事务的使用
加分项
- 数据库索引优化
- 分布式锁的实现
- 多级缓存架构
- 读写分离和分库分表
高频题
1. 什么是 N+1 查询?如何解决?
答:N+1 查询是指查询主表 1 次,然后循环查询关联表 N 次,导致大量数据库查询。
解决方案:使用预加载(with)一次性查询所有数据。
2. 如何避免缓存雪崩?
答:缓存雪崩是大量缓存同时过期,导致数据库压力骤增。
解决方案:
- 过期时间加随机值,避免同时过期
- 使用多级缓存
- 设置热点数据永不过期
3. 分布式锁如何实现?
答:使用 Redis 的 SET NX EX 命令:
$locked = $redis->set($key, $value, ['NX', 'EX' => 10]);
- NX:只在 key 不存在时设置
- EX:设置过期时间
释放锁时使用 Lua 脚本保证原子性。
下一步:阅读 06-微服务架构.md 了解微服务