Coolify 中的 Laravel 缓存最佳实践:从 Cache::remember 到 Failover 存储的完整实战指南
本文基于 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; - 内置
apc、array、database、file、memcached、redis、dynamodb、octane等多种 store,其中redisstore 使用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#L253 用
Cache::remember($cacheKey, 300, ...)缓存 5 分钟,而任何资源变更时(如 GlobalSearch.php#L103)调用Cache::forget(self::getCacheKey($teamId))主动失效。 - 代理域名信任校验:TrustHosts.php#L54 用
Cache::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();
标签的硬性限制(规则文件明确声明):只有 redis、memcached、dynamodb 驱动支持标签,file 和 database 不支持。Coolify 默认驱动是 Redis(见 config/cache.php#L18),因此满足前提;但如果你把 CACHE_DRIVER 改成 file(单机自托管、不想额外跑 Redis 的简化部署),标签会静默退化为不支持——写代码时应意识到这一点。
需要说明:从源码结构看,Coolify 当前并未使用 Cache::tags(),而是用层级键名(如 server:{id}:traefik:dashboard_available)加逐键 forget 管理失效,例如 ProxyDashboardCacheService 的 clearCache() / 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.php 的 stores 数组并把 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() |
未命中时的竞态、进程间协调 | SshMultiplexingHelper、SentinelController |
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 兜底,且失效点放在数据变更处——InstanceSettings 的 booted() 钩子是最好的范本。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00