Laravel 8 基于文件缓存的高并发请求计数无丢失存储方案咨询
Laravel文件缓存高并发请求计数丢失解决方案
问题根因
计数丢失问题核心来自两个层面:
- 竞态条件:Laravel文件缓存的读写操作是非原子性的,高并发下多个请求同时执行「读缓存-改数值-写回缓存」的流程时,多个请求会读到相同的旧值,各自修改后写回的结果会互相覆盖,最终导致计数丢失。
- 现有实现缺陷:
- 用数组存储计数完全冗余,额外增加了序列化/反序列化、计数统计的性能开销
- 定时任务全量执行
Cache::flush()会误删同步过程中新写入的缓存数据 - 数据库操作直接拼接SQL存在注入风险,且先查后改的非原子操作也可能导致数据库计数丢失
不更换缓存驱动的优化方案
如果暂时无法更换文件缓存驱动,可以通过加锁、优化逻辑解决计数丢失问题:
1. 计数逻辑改造
使用Laravel自带的缓存锁保证同一时间只有一个请求修改对应计数,同时将数组存储改为直接存储整数计数:
$countKey = $key.'Count'; // 给对应计数key加独占锁,锁超时时间设为5秒避免死锁 $lock = Cache::lock($countKey.'_lock', 5); if ($lock->get()) { $currentCount = Cache::get($countKey, 0); Cache::put($countKey, $currentCount + 1, env('CACHE_TIMER_IN_SECONDS', 300)); $lock->release(); }
2. 定时任务逻辑改造
删除全量缓存清空逻辑,改用原子性的数据库增量更新,仅删除已同步完成的缓存key:
$schedule->call(function () { $clients = Cache::get('consumed_clients', []); if (empty($clients[0])) { return; } foreach ($clients[0] as $clientKey) { $clientObj = Cache::get($clientKey); if (empty($clientObj[0])) { continue; } $countKey = $clientObj[0]->risk_api_key.'Count'; $lock = Cache::lock($countKey.'_lock', 10); if ($lock->get()) { $cacheCount = Cache::get($countKey, 0); if ($cacheCount > 0) { // 直接用数据库原子增量更新,省去查询步骤也避免计数冲突 DB::table('clients') ->where('key', $clientObj[0]->key) ->increment('num_of_consumed_requests', $cacheCount); // 仅删除已同步的计数key Cache::forget($countKey); } $lock->release(); } } // 仅清空已处理的客户端列表,不清空全量缓存 Cache::forget('consumed_clients'); })->everyTwoMinutes();
更优方案:更换Redis缓存驱动
文件缓存本身不适用于高并发计数场景,更换为Redis驱动可以从根本上解决竞态问题,性能提升可达数十倍:
Laravel原生支持Redis的原子递增操作,无需手动加锁,计数逻辑一行代码即可完成:
// increment是原子操作,高并发下不会出现计数覆盖 Cache::increment($key.'Count', 1, env('CACHE_TIMER_IN_SECONDS', 300));
定时任务逻辑可以直接沿用上面改造后的代码,无需额外加锁也能保证计数准确,可轻松应对数千QPS的请求量。
注意事项
- 若坚持使用文件缓存,需确保缓存目录挂载在SSD磁盘上,避免IO瓶颈导致锁超时、计数丢失
- 可根据业务情况适当调高定时任务执行频率(比如1分钟一次),减少单次同步的数据量,降低风险
- 禁止全量执行
Cache::flush(),避免误删未同步的缓存数据
内容的提问来源于stack exchange,提问作者Rajesh
相关产品推荐
相关产品推荐

