You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Eloquent模型统计数据的高效生成与存储方案探讨

问题描述

我正在开发一款用户可签署请愿书的应用,每份请愿书包含subject(主题)和location(地点)属性,对应主题落地页、地点落地页及请愿书(主题+地点组合)落地页。我拥有一个Signature Eloquent模型,其关联的signatures数据透视表包含user_id、subject_id、location_id字段。

各页面需展示如下统计标题:

  • 主题落地页:「[主题] 累计XX个签名,覆盖YY个地点!」
  • 地点落地页:「[地点] 累计XX个签名,覆盖YY个主题!」
  • 请愿书落地页:「[主题]在[地点]的累计签名数为XX!」
  • 首页:「累计XX个签名,覆盖YY个主题、ZZ个地点!」

我计划创建一个SignatureObserver,在签名新增或删除时增减对应缓存值,代码如下:

class SignatureObserver
{
    public function created(Signature $signature)
    {
        Cache::increment("total_signatures");
        Cache::increment("location_signatures:{$signature->location_id}");
        Cache::increment("subject_signatures:{$signature->subject_id}");
        Cache::increment("subject_location_signatures:{$signature->subject_id}:{$signature->location_id}");
    }

    public function deleted(Signature $signature)
    {
        Cache::decrement("total_signatures");
        Cache::decrement("location_signatures:{$signature->location_id}");
        Cache::decrement("subject_signatures:{$signature->subject_id}");
        Cache::decrement("subject_location_signatures:{$signature->subject_id}:{$signature->location_id}");
    }
}

尚未测试该方案,现咨询:

  • 此方法是否为存储与访问统计数据的最优方案?
  • 使用事件/监听器是否更合适?
  • 是否需要专用统计数据表?
  • 若数据库已有大量签名或缓存被清空,缓存值为空时如何高效重新计数?
问题解答

1. 现有Observer方案是否为最优解?

你的Observer方案在常规业务场景下是高效且合理的,但有几个关键细节需要补全:

  • 并发与原子性:increment/decrement是原子操作(Redis等主流缓存驱动支持),但要避免使用文件缓存这类不支持原子操作的驱动,防止并发场景下统计值出错。
  • 事务一致性:如果签名操作包裹在数据库事务中,Observer的created方法会在事务提交前触发,一旦事务回滚,缓存值会和数据库数据不一致。解决办法是用afterCommit延迟缓存更新:
    public function created(Signature $signature)
    {
        $signature->afterCommit(function () use ($signature) {
            Cache::increment("total_signatures");
            // 其他缓存操作...
        });
    }
    
  • 缺失统计维度:当前代码只维护了签名数,但主题落地页的「覆盖地点数」、地点落地页的「覆盖主题数」、首页的「主题/地点总数」这些维度没有处理,需要补充对应的缓存更新逻辑。

2. 使用事件/监听器是否更合适?

Laravel的Observer本质是模型事件监听的封装,和事件/监听器功能等价,区别仅在于代码组织方式:

  • 如果统计逻辑仅和Signature模型绑定,用Observer更简洁,所有相关逻辑聚合在一个类中。
  • 如果后续需要扩展其他业务逻辑(比如签名后发送通知、记录操作日志),拆分事件/监听器会让代码更解耦,不同职责的逻辑放在独立监听器中。

没有绝对优劣,根据你的代码扩展需求选择即可——当前仅做统计缓存的话,Observer足够用。

3. 是否需要专用统计数据表?

取决于你的业务需求:

  • 不需要的场景:如果只需要实时展示当前统计值,且不需要回溯历史数据(比如每日签名增长趋势),纯缓存方案配合重建逻辑完全足够。
  • 需要的场景:
    • 要追踪统计数据的历史变化(比如生成周/月签名报表)。
    • 缓存频繁失效,且全量重建缓存的成本极高(比如百万级签名数据)。
    • 需要更复杂的统计维度(比如按时间段过滤签名)。

如果要做专用统计数据表,可以设计statistics表,包含type(total/subject/location等)、target_id(subject_id/location_id)、count、related_count(比如主题对应的地点数)等字段,在Observer中更新表数据后再缓存查询结果。

4. 缓存为空时如何高效重新计数?

推荐两种高效方案:

方式一:懒加载重建(访问时自动计算)

在获取统计值的地方先判断缓存是否存在,不存在则计算并写入缓存,可封装成工具类:

class SignatureStats
{
    public static function getTotalSignatures()
    {
        return Cache::rememberForever('total_signatures', function () {
            return Signature::count();
        });
    }

    public static function getSubjectLocationCount($subjectId)
    {
        return Cache::rememberForever("subject_locations:{$subjectId}", function () use ($subjectId) {
            return Signature::where('subject_id', $subjectId)->distinct('location_id')->count('location_id');
        });
    }

    // 其他统计方法同理
}

优点:无需手动触发,用户访问时自动重建;缺点:首次访问大数据量时会导致页面加载变慢。

方式二:Artisan命令批量重建

编写Artisan命令,后台批量计算所有统计值并写入缓存,适合缓存清空或初始化数据时使用:

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Models\Signature;
use App\Models\Subject;
use App\Models\Location;
use Illuminate\Support\Facades\Cache;

class RebuildSignatureStats extends Command
{
    protected $signature = 'stats:rebuild-signatures';
    protected $description = 'Rebuild all signature statistics cache';

    public function handle()
    {
        // 总签名数
        Cache::forever('total_signatures', Signature::count());

        // 主题相关统计(分批处理避免内存溢出)
        Subject::chunk(100, function ($subjects) {
            foreach ($subjects as $subject) {
                $signatureCount = Signature::where('subject_id', $subject->id)->count();
                $locationCount = Signature::where('subject_id', $subject->id)->distinct('location_id')->count('location_id');
                Cache::forever("subject_signatures:{$subject->id}", $signatureCount);
                Cache::forever("subject_locations:{$subject->id}", $locationCount);
            }
        });

        // 地点相关统计
        Location::chunk(100, function ($locations) {
            foreach ($locations as $location) {
                $signatureCount = Signature::where('location_id', $location->id)->count();
                $subjectCount = Signature::where('location_id', $location->id)->distinct('subject_id')->count('subject_id');
                Cache::forever("location_signatures:{$location->id}", $signatureCount);
                Cache::forever("location_subjects:{$location->id}", $subjectCount);
            }
        });

        $this->info('Signature statistics cache rebuilt successfully!');
    }
}

优点:后台执行不影响用户体验;缺点:需手动触发(或定时任务),如果主题+地点组合数据量极大,建议组合统计用懒加载替代全量重建。


内容的提问来源于stack exchange,提问作者Yev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 03:12:06