首页
/ Coolify 中的 Laravel 缓存最佳实践:从 Cache::remember 到 Failover 存储的完整实战指南

Coolify 中的 Laravel 缓存最佳实践:从 Cache::remember 到 Failover 存储的完整实战指南

2026-09-04 13:37:23作者:尤峻淳Whitney

本文基于 Coolify 仓库内置的 Laravel 最佳实践规则文件 caching.md 展开,系统讲解七条核心缓存准则:Cache::remember()Cache::flexible()Cache::memo()、缓存标签、Cache::add() 原子写入、once() 请求级记忆化以及 Failover 存储配置。读完本文,你既能掌握每条准则的正确用法与适用场景,也能在 Coolify 的真实源码(如 User 模型SSH 复用连接助手实例设置模型)中看到这些模式是如何落地的,从而在自己的 Laravel 项目中安全地应用同等方案。

为什么缓存准则要放在"一致性优先"的大前提下

Coolify 仓库在 SKILL.md 中为这套最佳实践设定了总原则:一致性优先——在应用任何规则之前,先检查代码库已有的写法,不一致比次优模式更糟糕。缓存规则只是"当项目中还没有既定模式时"的默认选择。这一点对自托管 PaaS(部署静态站点、数据库、全栈应用及 280 多种一键服务)尤为重要:Coolify 自身通过 Laravel + Livewire 构建管理面,通过 SSH/Sentinel 管理目标服务器,缓存既用于减少数据库查询,也用于进程间的分布式锁协调,两类用途的准则不能混用。

Coolify 的缓存基础设施配置在 config/cache.php 中,关键事实如下:

  • 默认 store 为 env('CACHE_DRIVER', 'redis')config/cache.php#L18),即生产环境默认走 Redis;
  • 内置 apcarraydatabasefilememcachedredisdynamodboctane 等多种 store,其中 redis store 使用 cache 连接、锁使用 default 连接(config/cache.php#L76-L80);
  • 全局键前缀由 CACHE_PREFIX 控制,默认 Str::slug(env('APP_NAME', 'laravel'), '_').'_cache_'config/cache.php#L108),多应用共用同一 Redis 时靠它避免键冲突。

用 Cache::remember() 替代手工 Get/Put

最基础也最高频的准则:不要手写"先 get,没有再算,再 put"的样板代码,用 Cache::remember() 一行表达 cache-aside 模式;存在竞态(多个请求同时未命中、同时触发昂贵计算)时,配合 Cache::lock() 使用。

不推荐:

$val = Cache::get('stats');
if (! $val) {
    $val = $this->computeStats();
    Cache::put('stats', $val, 60);
}

推荐:

$val = Cache::remember('stats', 60, fn () => $this->computeStats());

Coolify 源码中有大量 Cache::remember() 的实际用法,例如:

  • 团队当前会话的模型缓存:User::currentTeam()Cache::remember('user:'.$this->id.':team:'.$sessionTeamId, 3600, fn () => Team::find($sessionTeamId)) 把"当前团队"缓存 1 小时。该方法是整个权限体系的热点路径——role() 等方法都会经由它取团队,一次缓存命中就能省掉一轮数据库查询。
  • 全局搜索的可搜索项聚合:GlobalSearch.php#L253Cache::remember($cacheKey, 300, ...) 缓存 5 分钟,而任何资源变更时(如 GlobalSearch.php#L103)调用 Cache::forget(self::getCacheKey($teamId)) 主动失效。
  • 代理域名信任校验:TrustHosts.php#L54Cache::remember('instance_settings_fqdn_host', 300, ...) 缓存 FQDN 配置,并在配置变更时于 InstanceSettings 模型updated 钩子里 Cache::forget('instance_settings_fqdn_host')

值得注意的细节是键命名与失效时机。Coolify 采用 user:{id}:team:{id}server:{id}:traefik:dashboard_available 这类带语义层级的键,并且失效点放在数据变更处而非读取处:如 DeleteTeam.php#L61 删除团队时 Cache::forget("user:{$user->id}:team:{$team->id}")Livewire/Team/Member.php#L94-L95 移除成员时同时遗忘 team:{id}user:{id}:team:{teamId} 两个键。这演示了 remember 模式的完整生命周期:写入、读取、精确失效,三者缺一不可。

用 Cache::flexible() 实现过期但可服务(Stale-While-Revalidate)

高流量键有一个经典问题:缓存过期的那一瞬间,恰好有一个用户请求撞上,他会拿到一个明显慢的响应(缓存击穿)。Cache::flexible() 的解法是:过期后不再立即拒绝服务,而是先返回略旧的数据,同时通过延迟函数在后台刷新。

不推荐:

Cache::remember('users', 300, fn () => User::all());

推荐:

Cache::flexible('users', [300, 600], fn () => User::all());

两个 TTL 参数的含义:300 是"新鲜期",5 分钟内所有请求拿到的都是新鲜数据;600 是"可服务过期上限",最多到 10 分钟才彻底失效,中间的窗口期返回旧值并由后台任务刷新。适合的场景是"数据允许短时陈旧、但绝不希望用户感知到毛刺"的列表页,比如资源状态列表、仪表盘统计。

在 Coolify 仓库中,flexible() 目前尚未被直接使用(从源码搜索结果看没有调用点),这属于"规则先行、按需采用"的状态:仓库当前的读多写少数据更多采用"配置变更时主动 forget"的精确失效策略(见上文 FQDN 例子)。如果你在自己的 Laravel 项目中引入高流量聚合键,可以把这条准则作为 remember() 的升级选项。

用 Cache::memo() 消除同一请求内的重复命中

如果一个缓存键在单次请求中被读取多次(例如某个服务被多处调用),每次调用都是一次真实的 Redis 往返。Cache::memo() 会把解析后的值暂存在内存中,同一请求内的后续调用直接返回内存值。

Cache::memo()->get('settings');

规则文件给出的量化描述是:同一请求内对 settings 的 5 次调用 = 1 次 Redis 往返,而不是 5 次。它与 once() 的区别在于:memo() 仍然会命中一次缓存存储(跨请求仍享受持久缓存),只是请求内去重;而 once() 完全不碰缓存存储。

用缓存标签(Tags)原子失效一组相关条目

没有标签时,要失效"用户 1 相关的所有缓存",你必须追踪他名下每一个键逐个 forget。标签让你把一组条目原子地整体刷掉:

Cache::tags(['user-1'])->flush();

标签的硬性限制(规则文件明确声明):只有 redismemcacheddynamodb 驱动支持标签,filedatabase 不支持。Coolify 默认驱动是 Redis(见 config/cache.php#L18),因此满足前提;但如果你把 CACHE_DRIVER 改成 file(单机自托管、不想额外跑 Redis 的简化部署),标签会静默退化为不支持——写代码时应意识到这一点。

需要说明:从源码结构看,Coolify 当前并未使用 Cache::tags(),而是用层级键名(如 server:{id}:traefik:dashboard_available)加逐键 forget 管理失效,例如 ProxyDashboardCacheServiceclearCache() / clearCacheForServers() 方法按 server id 循环遗忘。这印证了规则文件的"一致性优先":当键空间可控(每服务器一两条键)时,层级键 + 显式遗忘比标签更直白。标签的价值体现在一个实体关联大量键、且需要"一键清空"的场景。

用 Cache::add() 做原子条件写入

"先检查再写入"不是原子的:两个请求都可能先看到"锁不存在",然后都去写入。Cache::add() 只在键不存在时写入,由存储层保证原子性:

不推荐:

if (! Cache::has('lock')) {
    Cache::put('lock', true, 10);
}

推荐:

Cache::add('lock', true, 10);

Coolify 里有一个教科书式的用法:CleanupInstanceStuffsJob 在任务开始时执行 if (! Cache::add('backup-retention-enforcement', true, 1800))——只有成功抢到键的请求才继续执行备份保留策略清理(30 分钟防重入),任务结束时在 L79 调用 Cache::forget('backup-retention-enforcement') 释放。这正对应规则文件中 add() 的"原子、无检查与写入之间竞态"特性。

对于比 add() 更复杂的并发控制(拿到锁后要执行一段可能失败的操作、需要阻塞等待),Coolify 使用的是 Cache::lock()->block() 模式,典型例子是 SshMultiplexingHelper::ensureMultiplexedConnection():多个请求同时要为某台服务器建立 SSH 复用主连接时,只有一个能拿到锁,其余在 block(config('constants.ssh.mux_lock_timeout')) 内等待,超时则捕获 LockTimeoutException 并降级为非复用连接。同文件 L131 还会在进程退役时 Cache::forget 对应的清理键。类似的锁还见于 SentinelController(10 秒锁 + 5 秒阻塞窗口,保护 hash 推送)和 AdminDeleteUser(600 秒锁保护删用户命令)。

用 once() 做请求级函数记忆化

once() 把一个闭包的计算结果记忆化到对象(或请求,对闭包而言)生命周期内,完全不访问缓存存储——纯内存。

public function roles(): Collection
{
    return once(fn () => $this->loadRoles());
}

多次调用 roles() 都返回第一次的计算结果,不会重新执行。选择准则:请求内被多次调用的昂贵计算用 once();既想请求内去重、又想跨请求持久缓存的用 Cache::memo()

Coolify 中一个非常典型的例子是 InstanceSettings::get()

public static function get()
{
    return once(fn () => InstanceSettings::findOrFail(0));
}

单例设置行在整个请求内无论被读多少次都只查一次库。更关键的是失效设计:模型的 booted() 钩子(InstanceSettings.php#L101-L116)在 created/updated 事件里调用 Once::flush(),确保设置被修改后同一进程内的后续调用拿到新数据,同时在 FQDN 变化时级联 Cache::forget('instance_settings_fqdn_host')。这是"纯内存记忆化 + 变更时主动清空"的完整闭环,也是使用 once() 时最容易遗漏的一环:只 flush 不遗忘,跨请求就会读到脏数据

生产环境配置 Failover 缓存存储

Redis 宕机不应导致整个 PaaS 不可用。Laravel 支持把多个 store 组合成 failover 驱动,主存储不可用时自动回退到备用存储:

'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],

把它加进 config/cache.phpstores 数组并把 default 指向它即可。结合 Coolify 的配置:database store 使用 cache 表(config/cache.php#L45-L50),而 Coolify 的迁移目录中并没有创建该表的迁移文件(从 database/migrations 目录结构看,cache 表需自行通过 php artisan cache:table 或手动建表生成)。因此若要在 Coolify 上启用 redis → database 的 failover,前提是先把 cache 表建好——否则回退路径本身会失败。这也说明 failover 是"多一层兜底"而非"免维护配置":备用的 database store 只是把延迟放大,不能替代恢复 Redis。

各准则速查与在 Coolify 中的对应

准则 适用场景 Coolify 源码印证
Cache::remember() 一切 cache-aside 场景,替代手工 get/put User::currentTeam()TrustHosts
Cache::lock() / block() 未命中时的竞态、进程间协调 SshMultiplexingHelperSentinelController
Cache::add() 原子"仅当不存在时写入"(防重入) CleanupInstanceStuffsJob
once() 请求内昂贵计算去重,不碰存储 InstanceSettings::get(),配 Once::flush() 失效
Cache::memo() 请求内去重 + 跨请求持久缓存 规则文件定义,Coolify 暂未采用
缓存标签 一组键原子失效(仅 redis/memcached/dynamodb) Coolify 采用层级键 + 逐键 forget 替代
Cache::flexible() 高流量键的 stale-while-revalidate 规则文件定义,Coolify 暂未采用
Failover store 主存储宕机自动回退 配置模板见 config/cache.php,启用需先建 cache

小结

这七条准则可以归纳为三个层次:模式层remember() 替代样板、add()/lock() 消灭竞态)、去重层once() 请求内纯内存、memo() 请求内去重加持久缓存、flexible() 高流量键平滑过期)与治理层(标签或层级键的失效策略、前缀隔离、failover 兜底)。Coolify 源码展示了其中大部分准则的真实落地方式,也展示了"一致性优先"的现实版本:并非每条新 API 都会立即被采用,未采用的部分(flexible()memo()、tags)在引入前应先评估现有键空间与失效策略是否能覆盖。对自托管 PaaS 这类"缓存兼作协调锁"的系统,最后一条容易被忽略的纪律是:每个 put/remember 都必须有对应的 forget 或明确的 TTL 兜底,且失效点放在数据变更处——InstanceSettingsbooted() 钩子是最好的范本。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384