我使用Laravel 5构建一个Web应用程序,该应用程序创建指向Web应用程序的链接,访问时显示学生进度报告的表单。这些链接由Web应用程序发送到学生所在机构的联系人的电子邮件中,以便收件人完成电子邮件中链接访问的进度报告。
我面临的问题是创建和发送链接时。我有一些代码可以与几百名学生一起使用,但在现实世界中使用该应用程序可能会同时创建和发送3000多个链接。我编写的代码无法及时处理如此大的数字,应用程序崩溃。奇怪的是,虽然我没有通过laravel收到任何超时错误(我需要仔细检查php日志)。
虽然我非常欢迎其他建议,但我相信问题的答案是利用队列。我在发送电子邮件时已经使用了队列(参见代码),但我想将代码的其他部分用于队列,但我有点不确定如何做到这一点!
简要数据库架构
Student
hasMany Link
Student
hasMany InstitutionContact
(我的申请仅限于两位)
Link
hasMany InstitutionContact
(我的申请仅限于两位)
Email
manyToMany Link
我想要实现的目标
获取所有需要新Student
Link
为每个Link
Student
将Student
的当前InstitutionContact
分配给Link
' InstitutionContact
(A Student
& #39;机构联系人可以更改,因此如果需要重新发送,我会将InstitutionContact
链接到该链接。
循环浏览所有新创建的Links
,以便通过共享InstitutionContact
将它们组合在一起 - 这样就不会发送每封链接的电子邮件(因此可能会发送多封电子邮件链接到同一地址),相应的链接应该由同一个电子邮件/联系人组合在一起,并在适用的情况下一起发送
循环浏览通过电子邮件/联系人分组的所有Link
和:
Link
电子邮件地址发送包含InstitutionContact
信息(网址,学生姓名等)的电子邮件Email
的副本写入数据库Email
加入其中发送的Link
(因此该应用程序可用于搜索在哪个电子邮件中发送的链接)因此,我面临的主要挑战是使用大型数据集执行上述任务。我已经考虑通过队列逐个创建和发送Link
,但这不允许我通过联系人/电子邮件将所有Link
组合在一起。由于任务不会定期执行,我可以考虑执行任务,因为这会增加内存和时间,但是在尝试使用{{{}时,我没有取得多大成功。发送任何链接之前1}}和set_time_limit(0);
。
任何帮助都会非常感激,谢谢你,如果你读到这里了!
代码
应用\ HTTP \控制器\ LinkController.php
ini_set('memory_limit','1056M');
应用\模型\ Link.php
public function storeAndSendMass(Request $request)
{
$this->validate($request, [
'student_id' => 'required|array',
'subject' => 'required|max:255',
'body' => 'required|max:5000',
]);
$studentIds = $request->get('student_id');
$subject = $request->get('subject');
$body = $request->get('body');
$students = $this->student
->with('institutionContacts')
->whereIn('id', $studentIds)
->where('is_active', 1)
->get();
// create link, see Link.php below for method
$newLinks = $this->link->createActiveLink($students);
// send link to student's contact(s), see LinkEmailer.php below for method
$this->linkEmailer->send($newLinks, ['subject' => $subject, 'body' => $body], 'mass');
// return
return response()->json([
'message' => 'Creating and sending links'
]);
}
应用\电子邮件\ LinkEmailer.php
public function createActiveLink($students)
{
$links = [];
foreach ($students as $student) {
$newLink = $this->create([
'token' => $student->id, // automatically hashed
'status' => 'active',
'sacb_refno' => $student->sacb_refno,
'course_title' => $student->course_title,
'university_id' => $student->university_id,
'student_id' => $student->id,
'institution_id' => $student->institution_id,
'course_id' => $student->course_id,
]);
$studentContacts = $student->institutionContacts;
if ($studentContacts) {
foreach ($studentContacts as $studentContact) {
$newLink->contacts()->create([
'type' => $studentContact->pivot->type,
'institution_contact_id' => $studentContact->pivot->institution_contact_id
]);
$newLink->save();
}
}
$links[] = $newLink->load('student');
}
return $links;
}
修改1
我现在正在使用namespace App\Emails;
use App\Emails\EmailComposer;
class LinkEmailer
{
protected $emailComposer;
public function __construct(EmailComposer $emailComposer)
{
$this->emailComposer = $emailComposer;
}
public function send($links, $emailDetails, $emailType)
{
$contactsAndLinks = $this->arrangeContactsToLinks($links);
foreach ($contactsAndLinks as $linksAndContact) {
$emailData = array_merge($linksAndContact, $emailDetails);
// send/queue email
\Mail::queue('emails/queued/reports', $emailData, function ($message) use ($emailData) {
$message
->to($emailData['email'], $emailData['formal_name'])
->subject($emailData['subject']);
});
// compose email message, returns text of the email
$emailMessage = $this->emailComposer->composeMessage($emailData);
// // create Email
$email = \App\Models\Email::create([
'to' => $emailData['email'],
'from' => 'report@domain.org',
'subject' => $emailData['subject'],
'body' => $emailMessage,
'type' => $emailType,
'user' => $_SERVER['REMOTE_USER']
]);
foreach ($linksAndContact['links'] as $link) {
$link->emails()->attach($email->id);
}
}
}
// group links by contact
public function arrangeContactsToLinks($links)
{
$contactsForLinks = [];
$assigned = false;
$match = false;
foreach ($links as $link) { // 1, n
if ($link->contacts) {
foreach ($link->contacts as $contact) { // 1, 2
if ($contactsForLinks) {
$assigned = false;
foreach ($contactsForLinks as $key => $contactLink) { // n
// assign links to existing email in array
if ($contactLink['email'] === $contact->institutionContact->email) {
$match = false;
// check link hasn't already been included
foreach ($contactsForLinks[$key]['links'] as $assignedLink) {
if ($assignedLink === $link) {
$match = true;
}
}
// if there was no match add to list of links
if (!$match) {
$contactsForLinks[$key]['links'][] = $link->load('student');
$assigned = true;
break;
}
}
}
if (!$assigned) {
$contactsForLinks[] = [
'email' => $contact->institutionContact->email,
'formal_name' => $contact->institutionContact->formal_name,
'requires_id' => $contact->institutionContact->institution->requires_id,
'requires_course_title' => $contact->institutionContact->institution->requires_course_title,
'links' => [$link->load('student')],
];
}
} else {
$contactsForLinks[] = [
'email' => $contact->institutionContact->email,
'formal_name' => $contact->institutionContact->formal_name,
'requires_id' => $contact->institutionContact->institution->requires_id,
'requires_course_title' => $contact->institutionContact->institution->requires_course_title,
'links' => [$link->load('student')],
];
}
}
}
}
return $contactsForLinks;
}
}
和set_time_limit(0);
这需要花费8分钟来完成3000名学生。
修改2
我正在运行Laravel Framework版本5.1.6(LTS),MySQL for DB。
编辑3
到目前为止,感谢所有答案,谢谢大家。我想我可以将ini_set('memory_limit','1056M');
创建过程放到一个队列中,该队列中的数据库中有一个相关实体,类似于link
,当链接的Batch
完成后,将Batch
中的所有Link
分组并发送给他们。
我可以使用@ denis-mysenko建议的方法,在Batch
表中设置sent_at
字段,并有一个预定的流程来检查Link
的避风港&#39已被发送然后发送给他们。但是,使用上述方法,我可以在Link
完成所有已创建完成后发送Batch
,而Link
方法则使用预定进程查找sent_at
1}}尚未发送的信息可能会在所有链接尚未创建时发送一些链接。
答案 0 :(得分:2)
如果您使用少量数据测试了代码并且成功而没有崩溃,那么很明显问题(正如您所说)是您所记录的相当多的记录处理。为什么不使用 chunk 方法处理您的收藏?
根据Laravel文档:
如果您需要处理数千个Eloquent记录,请使用chunk命令。块方法将检索" chunk" Eloquent模型,将它们送到给定的Closure进行处理。使用块方法将在处理大型结果集时节省内存
无论如何,我认为在这种情况下需要使用队列。我认为,由于请求超时的高风险,应该绝对避免在HTTP请求上处理大量数据。排队过程没有执行时间限制。
为什么不同时在集合中使用队列和块方法?这将使您能够:
Laravel文档涵盖了您所需要的一切:Eloquent - Retrieving multiple models(参见" Chunking结果"更深入地了解如何在处理大型数据集时节省内存)和{{3}用于创建作业并分离不应在您的网络服务器下运行的软件的某些部分,以避免超时的风险
答案 1 :(得分:1)
我建议改变架构。我认为它不必要地过于复杂。
控制器可能喜欢:
public function storeAndSendMass(Request $request, LinkEmailer $linkEmailer)
{
$this->validate($request, [
'student_id' => 'required|array',
'subject' => 'required|max:255',
'body' => 'required|max:5000',
]);
$students = $this->student
->with('institutionContacts')
->whereIn('id', $request->input('student_id'))
->where('is_active', 1)
->get();
// Don't use Link.php method at all
foreach ($students as $student) {
$student->links()->create([
'token' => $student->id, // automatically hashed
'status' => 'active',
'sent_at' => null,
'email_body' => $request->input('body'),
'email_subject' => $request->input('subject')
]);
}
return response()->json([
'message' => 'Creating and sending links'
]);
}
为什么要在Link模型中保留这么多字段,这些字段已经存在于Student模型中并且可以通过student()关系访问?您可以保留状态和令牌(我假设它是链接的一部分?),以及' sent_at'时间戳。如果链接通常只发送一次,那么保留电子邮件正文和主题也是合理的。
如果学生更新其机构联系人,则在撰写电子邮件时将使用新数据,因为您没有明确链接到机构联系人的链接。
然后,我会创建一个命令(比如说newLinkNotifier),例如,每隔10分钟就会运行一次,这样就可以找到尚未发送的链接($links->whereNull('sent_at')
),通过电子邮件($link->student->institutionContacts
)和电子邮件内容($link->email_body, $link->email_subject
)对它们进行分组,并创建排队的电子邮件作业。然后队列工作人员会发送这些电子邮件(或者您可以将队列设置为' async'以便立即发送它们。)
由于此命令将以异步方式运行,因此如果需要5分钟才能完成,这并不重要。但在现实生活中,成千上万的物体需要不到一分钟的时间。
如何进行分组?我可能只是依赖MySQL(如果你使用它),它将比PHP更快地完成工作。由于所有3个字段都可以从SQL表中访问(两个直接,另一个来自JOIN) - 它实际上是一个非常简单的任务。
最后,您的电子邮件send()方法将变得如此简单:
public function send()
{
$links = Link::whereNull('sent_at')->get();
// In this example, we will group in PHP, not in MySQL
$grouped = $links->groupBy(function ($item, $key) {
return implode('.', [$item->email, $item->email_body, $item->email_subject]);
});
$grouped->toArray();
foreach ($grouped as $group) {
// We know all items in inside $group array got the same
// email_body, email, email_subject values anyway!
Mail::queue('emails/queued/reports', $group[0]->email_body, function ($message) use ($group) {
$message
->to($group[0]->email)
->subject($group[0]->email_subject);
});
}
}
这还不完美,我还没有测试过这段代码 - 我在这里写了它,但它显示了提出的概念。
如果您不打算获得数百万条款 - 这可能已经足够了。否则,您也可以将链接创建移动到单独的异步作业中。
答案 2 :(得分:0)
假设你正在运行5.0版本,那么如何将初始处理传递给队列呢?
应用\ HTTP \控制器\ LinkController.php 强>
// Accept request, validate $students
// Send this work strait to the cue
Bus::dispatch(
new CreateActiveLinks($students));
);
// return
return response()->json([
'message' => 'Creating and sending links. This will take a while.'
]);
app \ Console \ Commands \ CreateActiveLinks.php (排队作业)
class CreateActiveLinks extends Command implements SelfHandling, ShouldQueue {
protected $studentIds;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct($studentIds)
{
$this->studentIds = $studentIds;
}
/**
* This part is executed in the queue after the
* user got their response
*
* @return void
*/
public function handle()
{
$students = Student::with('institutionContacts')
->whereIn('id', $studentIds)
->where('is_active', 1)
->get();
foreach ($students as $student) {
// Process and create records...
$newLinks[] = $newLink->load('student');
}
// Emailer job would run like normal
LinkEmailer::send($newLinks, ['subject' => $subject, 'body' => $body], 'mass');
// Notify user or something...
}
}
Queuing Commands in Laravel 5.0
在5.1前进中,这些被称为Jobs并且工作方式略有不同。
此代码未经测试,我对您的应用程序结构没有很好的掌握,所以请不要将其视为福音。它只是基于我在面对类似情况时在我自己的应用程序中完成的工作。也许这至少会给你一些想法。如果您确实拥有大量记录,那么在chunk()
类查询中添加CreateActiveLinks
方法可能会有所帮助。
答案 3 :(得分:0)
我发现创建一个Event / Listener并实现队列更加容易。您所需要做的就是为您的电子邮件流程(LinkEmailer)创建一个事件和监听器,然后按照文档中的说明实现ShouldQueue接口。