本文目录导读:

是的,缓存和索引是提升 Laravel 应用性能最核心、最有效的两种策略,它们分别解决了不同层面的瓶颈:
- 索引:主要解决数据库查询慢的问题(I/O 瓶颈)。
- 缓存:主要解决重复计算/重复查询的问题(CPU 和 I/O 瓶颈)。
下面我详细拆解这两者在 Laravel 中的具体实践和配置方法。
数据库索引:查询性能的基石
没有合适的索引,哪怕只有 10 万条数据,一个简单的 WHERE 查询也可能让你等上好几秒。
什么时候必须加索引?
WHERE子句:WHERE user_id = ?中的user_id。JOIN的表连接字段:ON orders.user_id = users.id中的user_id。ORDER BY字段:ORDER BY created_at中的created_at。GROUP BY字段。- 唯一性约束:邮箱、手机号等。
Laravel 迁移中的索引实践
在迁移文件里,不要只盯着字段类型,索引声明同样重要。
// database/migrations/xxxx_create_orders_table.php
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('order_no')->unique(); // 唯一索引,快速查找订单号
$table->string('status')->index(); // 普通索引,按状态筛选
$table->timestamp('created_at')->index(); // 排序或按时间范围查询
$table->foreignId('payment_method_id')->constrained(); // 外键自动创建索引
// 联合索引:经常同时按 user_id 和 status 查询时
$table->index(['user_id', 'status']);
});
避免索引失效的常见坑
- 不要在索引列上使用函数:
WHERE DATE(created_at) = '2024-01-01'(失效)。- ✅ 正确做法:
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02'
- ✅ 正确做法:
- 避免
LIKE前置通配符:WHERE name LIKE '%张三%'(失效,除非用全文索引)。- ✅ 部分场景可考虑:
WHERE name LIKE '张三%'(有效)。
- ✅ 部分场景可考虑:
- 类型不一致:
user_id是 int 类型,where('user_id', '1')是字符串 '1',可能导致隐式类型转换(失效)。- ✅ 始终使用正确的类型:
where('user_id', 1)。
- ✅ 始终使用正确的类型:
如何检查索引效果?
在 Laravel Tinker 或 MySQL 客户端中,对你怀疑慢的查询前面加上 EXPLAIN:
EXPLAIN SELECT * FROM orders WHERE user_id = 123;
关注 type 和 rows 字段:
type = ALL:全表扫描,必须加索引。type = ref或range:使用了索引。rows数字越小越好。
缓存:消灭重复劳动
索引只能加速 SQL,但无法解决“同一个页面被 1000 个人访问,就查 1000 次数据库”的问题,缓存就是干这个的。
Laravel 的缓存门面(Facade)
Laravel 提供了统一的 Cache 门面,可以无缝切换文件、Redis、Memcached 等驱动。生产环境强烈建议用 Redis 或 Memcached。
use Illuminate\Support\Facades\Cache;
// 1. 从缓存中获取数据,如果没有则通过闭包计算并存入缓存
$users = Cache::remember('active_users', 3600, function () {
return User::where('is_active', 1)->orderBy('name')->get();
});
// 这里 3600 秒 = 1 小时内,相同的请求只会查一次数据库
// 2. 手动存储
Cache::put('key', 'value', $seconds);
// 3. 检查是否存在
if (Cache::has('key')) {
//
}
缓存常用场景
- 全页面缓存:对整个响应进行缓存(适合内容基本不变的页面)。
- 片段缓存:
- 配置数据:
Setting::first()(网站全局设置)。 - 状态列表:省份、城市、文章分类等枚举型数据。
- 热门排行榜:文章阅读量 Top 10,每小时更新一次。
- 用户 Token / Session:使用 Redis 避免文件 Session 的性能问题。
- 配置数据:
缓存失效策略(非常重要)
缓存最大的敌人是“数据不一致”。
- 时间过期:设置合理的 TTL(生存时间),如上例的 3600 秒。
- 主动失效(标签/手动清除):当数据被修改时,必须立即删除或更新缓存。
示例:更新用户信息时清理缓存
public function update(Request $request, User $user)
{
$user->update($request->validated());
// 关键步骤:删除与该用户相关的缓存
// 假设你在其他地方用 'user_'.$user->id 缓存了用户数据
Cache::forget('user_'.$user->id);
// 或者使用缓存标签(如果驱动支持,如 Redis/Memcached)
Cache::tags(['users', 'profiles'])->flush();
return redirect()->back()->with('success', '更新成功');
}
查询结果缓存(神器:Remember
除了显式调用 Cache,你还可以在模型上直接链式调用 remember():
$users = User::where('account_type', 'vip')
->remember(1440) // 缓存 1440 分钟(24小时)
->get();
注意:
remember()在多次查询不同参数时会生成不同的缓存键,但需要谨慎使用,确保数据变化时你能手动清除对应键。
组合策略:索引 + 缓存实战
假设你有个电商首页,显示“热销商品 Top 10”:
- 第一步(索引):确保
products表的sales_count字段有索引。 - 第二步(缓存):把查询结果缓存起来,设置 5 分钟过期。
// app/Http/Controllers/HomeController.php
public function index()
{
$hotProducts = Cache::remember('hot_products', 300, function () {
// 有了索引,这个查询本身很快
// 但缓存让它更快,且不再消耗数据库连接
return Product::where('status', 'on_sale')
->orderBy('sales_count', 'desc')
->take(10)
->get();
});
return view('home', compact('hotProducts'));
}
当一个商品被购买(sales_count 变化)时:
// 在 OrderController 的 placeOrder 方法或 ProductObserver 的 updated 事件中
Cache::forget('hot_products'); // 清除热销排行缓存
其他关键性能优化点
-
配置缓存(生产环境必做):
php artisan config:cache php artisan route:cache php artisan view:cache
这会把配置文件合并为单一数组,路由也转为静态数组,大幅减少文件 I/O。
-
队列处理耗时任务:发邮件、生成 PDF、处理图片等放到队列里。
-
Opcache(PHP 扩展):开启 Opcache 可以避免每次请求都重新编译 PHP 文件。
-
静态资源优化:合并 CSS/JS、开启 Gzip、使用 CDN。
| 策略 | 解决的核心问题 | 典型工具/做法 |
|---|---|---|
| 数据库索引 | 查询速度慢(扫描行数多) | ->index() 迁移、EXPLAIN 分析 |
| 缓存 | 重复计算/重复查询(高并发场景) | Cache::remember()、Redis、Cache::forget() |
| 两者结合 | 先让每个查询快起来,再让高频查询变零次查询 | 见上面的“热销商品”例子 |
一句话建议:先确保每个慢查询都有合适的索引,再用缓存把它变成一个“不发生查询”的请求。 两者缺一不可,先用索引打好基础,再用缓存削平峰值。