Laravel 应用实践:如何优雅的完成全量数据同步
需求场景:
我们公司有个产品,用户可以通过第三方应用中的积分进行消费抵扣,好比用肯德基的积分去买麦当劳。每次积分交易都要去第三方系统同步积分余额进行校验,本地没有最新的积分余额信息。
负责运营的同学要定期统计第三方系统中用户剩余积分余额,技术同学为此做了一个批量同步功能,基本思路就是从数据库表里捞出全量数据,做一个大循环,每条用户记录调用同步接口把第三方的积分余额同步过来,再导出成Excel给运营。
早期,这样的实现没有问题,但是随着用户记录越来越多,运营同学统计的等候时间越来越慢,跑完一次同步需要很长时间,更严重的是引起锁表。
解决问题:
为了解决这个问题,提出几个优化目标:
- 数据统计不能影响正常业务,减少对数据库的压力
- 数据处理进度可见
- 按时提取,自动生成结果数据
优化方案
公司技术框架刚转Laravel,凡事先考虑Laravel现有框架是否能有方案支持,解题思路如下:
- Redis:把待同步的用户基础数据放在Redis队列中,通过LPOP方法,每次冒泡处理一个记录,减少处理过程中对数据库的依赖
- 通过对对队列长度的判断和数据总数,计算当前进度
- 以上逻辑用Artisan 命令行方式写成组件,可手动执行,也可以放在任务中定时运行
- 结果数据用插件 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
尤其是看到这个进度表:
Artisan 命令行的进度条实现简直太方便:
$users = App\User::all();
// 多少个任务
$bar = $this->output->createProgressBar(count($users));
foreach ($users as $user) {
$this->performTask($user);
// 一个任务处理完了,可以前进一点点了
$bar->advance();
}
$bar->finish();
通过这个命令,同步数据可以手动执行,也可以放在定时任务中在业务不繁忙的夜间自动同步
如何把命令行放在定时任务中执行,看这里:任务调度
有了定时任务自动处理,每天早上,运营同学都可以在后台,自动拿到一份昨晚同步好的积分余额清单,瞬间也感觉到了优雅
总结
以上需求,从提出目标到实现,也就一个小时时间,结果皆大欢喜,大家都觉得很优雅。当然,关于处理这种场景,肯定还有更优化的方案,我在这里总结出来,仅仅是从具体场景分享一下Laravel框架的便捷和高度集成,能够快速响应业务需求,并且保持简洁,是我喜欢Laravel的一万个理由,没有之一。
本作品采用《CC 协议》,转载必须注明作者和本文链接
不是特别了解需求,如果第三方积分的消费不止你们一个场景的话,应该还是得每次消费的时候获取当前消费者的积分吧?
用户量大的话一口气user::all()数据也会很大的
@Figo
User::all()
可以改成 chunk:@overtrue 感谢各位大神给建议,现在不止优雅,开始性感起来!
@老财 下一步就是爆款了哈哈
@haowt 是的,每次消费的时候要去对方系统同步积分。
每次用户消费之后,生成一个任务来同步积分余额到本地数据库,这样也不用每天跑全量同步吧
chunk 是正解,要不然数据多直接就挂了。
感觉这是个毫无意义的队列,而且里面有坑,提前跑或者重复跑都有问题.
@TimJuly 这个功能不是用于交易,只是运营的日常数据需求,知道当前还有多少未消耗的积分以便安排后续促销工作。
@overtrue 这样,进度条需要怎么处理?:bowtie: