Laravel 应用实践:如何优雅的完成全量数据同步

需求场景:

我们公司有个产品,用户可以通过第三方应用中的积分进行消费抵扣,好比用肯德基的积分去买麦当劳。每次积分交易都要去第三方系统同步积分余额进行校验,本地没有最新的积分余额信息。

负责运营的同学要定期统计第三方系统中用户剩余积分余额,技术同学为此做了一个批量同步功能,基本思路就是从数据库表里捞出全量数据,做一个大循环,每条用户记录调用同步接口把第三方的积分余额同步过来,再导出成Excel给运营。

早期,这样的实现没有问题,但是随着用户记录越来越多,运营同学统计的等候时间越来越慢,跑完一次同步需要很长时间,更严重的是引起锁表。

解决问题:

为了解决这个问题,提出几个优化目标:

  1. 数据统计不能影响正常业务,减少对数据库的压力
  2. 数据处理进度可见
  3. 按时提取,自动生成结果数据

优化方案

公司技术框架刚转Laravel,凡事先考虑Laravel现有框架是否能有方案支持,解题思路如下:

  1. Redis:把待同步的用户基础数据放在Redis队列中,通过LPOP方法,每次冒泡处理一个记录,减少处理过程中对数据库的依赖
  2. 通过对对队列长度的判断和数据总数,计算当前进度
  3. 以上逻辑用Artisan 命令行方式写成组件,可手动执行,也可以放在任务中定时运行
  4. 结果数据用插件 maatwebsite/excel 生成Excel文件供运营下载

实践

用到的Laravel框架知识栈:
Redis操作
Artisan 命令行
任务调度

整个方案核心就一个命令行组件,关于如何写Artisan命令行,可以看这篇 Laravel 5.1 Artisan 命令行实战

public function handle()
    {
        //命令行参数 --pre为预处理
        $pre = $this->option('pre');

        if ($pre) {

            $this->info('sync pre points!');    
            $users=User::all();

            //进度条
            $bar = $this->output->createProgressBar(count($users));
            //待处理记录放进队列
            foreach ($users as $user){
                Redis::RPUSH('sync_points',$user->youzan_wx_id);
                $bar->advance();
            }

            $bar->finish();

        } else {

            $total=Redis::LLEN('sync_points');
            if($total>0){
                $bar = $this->output->createProgressBar($total);
                for ($i = 0; $i < $total; $i++) {
                    $youzan_id=Redis::LPOP('sync_points');
                    //同步数据
                    $this->service->syncUser($youzan_id);
                    $bar->advance();
                }
                $bar->finish();
            }

            //调用另外一个命令行组件导出excel
            $this->callSilent('export:points');
        }
    }

命令编写完成后,需要注册 Artisan 后才能使用。注册文件为 app/Console/Kernel.php。

敲这个命令的时候,运维瞬间感觉到了优雅

php artisan sync:points --pre

尤其是看到这个进度表:
file

Artisan 命令行的进度条实现简直太方便:

$users = App\User::all();

// 多少个任务
$bar = $this->output->createProgressBar(count($users));

foreach ($users as $user) {
    $this->performTask($user);

    // 一个任务处理完了,可以前进一点点了
    $bar->advance();
}

$bar->finish();

通过这个命令,同步数据可以手动执行,也可以放在定时任务中在业务不繁忙的夜间自动同步
如何把命令行放在定时任务中执行,看这里:任务调度
有了定时任务自动处理,每天早上,运营同学都可以在后台,自动拿到一份昨晚同步好的积分余额清单,瞬间也感觉到了优雅
file

总结

以上需求,从提出目标到实现,也就一个小时时间,结果皆大欢喜,大家都觉得很优雅。当然,关于处理这种场景,肯定还有更优化的方案,我在这里总结出来,仅仅是从具体场景分享一下Laravel框架的便捷和高度集成,能够快速响应业务需求,并且保持简洁,是我喜欢Laravel的一万个理由,没有之一。

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 7年前 自动加精
老财
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 11

不是特别了解需求,如果第三方积分的消费不止你们一个场景的话,应该还是得每次消费的时候获取当前消费者的积分吧?

7年前 评论

用户量大的话一口气user::all()数据也会很大的

7年前 评论

@Figo User::all() 可以改成 chunk:

User::chunk(500, function ($users) {
    foreach ($users as $user) {
        //
    }
});
7年前 评论
老财

@overtrue 感谢各位大神给建议,现在不止优雅,开始性感起来!

7年前 评论

@老财 下一步就是爆款了哈哈

7年前 评论
老财

@haowt 是的,每次消费的时候要去对方系统同步积分。

7年前 评论

每次用户消费之后,生成一个任务来同步积分余额到本地数据库,这样也不用每天跑全量同步吧

7年前 评论
lijinma

chunk 是正解,要不然数据多直接就挂了。

7年前 评论
TimJuly

感觉这是个毫无意义的队列,而且里面有坑,提前跑或者重复跑都有问题.

7年前 评论
老财

@TimJuly 这个功能不是用于交易,只是运营的日常数据需求,知道当前还有多少未消耗的积分以便安排后续促销工作。

7年前 评论

@overtrue 这样,进度条需要怎么处理?:bowtie:

7年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!