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的一万个理由,没有之一。