以非同步的方式寄送 Email 加快執行速度

傳統寄送 email 是採用同步的方式,也就是當你寄出一封信,必須等 email server 回應後,才可以繼續後續的程式動作,因此使用者會有明顯的等待時間;若能搭配 queue 機制,寄送 email 後,馬上以非同步的方式回到原來程式繼續執行,會有另外一個 process 去消耗 queue,負責寄送 email。

Motivation


Laravel 的 Mail::send() 是以同步方式寄送 email,會有明顯的等待時間;而 Mail:queue() 則是以非同步的方式寄送 email,速度非常快,不過必須搭配 queue 以及其他配套機制。

Version


PHP 7.0.8
Laravel 5.2.45

實際案例


先以 Mail::send() 透過 Gmail 寄送信件,然後再改用 Mail::queue() 方式寄送信件。

單元測試 : 由 Sync 寄信


MailServiceTest.php 1 1GitHub Commit : 單元測試 : 由 sync 寄送信件

tests/MailServiceTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use App\Services\MailService;

class MailServiceTest extends TestCase
{

/** @test */
public functionSync寄送信件()
{

/** arrange */

/** act */
App::call(MailService::class . '@mailBySync');

/** assert */
$this->assertTrue(true);
}
}

這裡沒做任何測試,只是透過 PHPUnit 啟動 sync 方式寄送信件。

設定使用 Gmail 寄信


.env 2 2GitHub Commit : 設定使用 Gmail 寄信

.env
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
APP_ENV=local
APP_DEBUG=true
APP_KEY=base64:PABSMLELX37jW2jJdwm9Fk6LvUWupulQXAWDZcfA7xE=
APP_URL=http://localhost

DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
#DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_DRIVER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=[your gmail address]
MAIL_PASSWORD=[your password]
MAIL_ENCRYPTION=tls

21 行

1
2
3
4
5
6
MAIL_DRIVER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=[your gmail address]
MAIL_PASSWORD=[your password]
MAIL_ENCRYPTION=tls

  • MAIL_HOST : 設定為 smtp.gmail.com
  • MAIL_PORT : 設定為 587
  • MAIL_USERNAME : 設定你的 Gmail 帳號,如 example@gmail.com
  • MAIL_PASSWORD : 設定你的 Gmail 密碼。
  • MAIL_ENCRYPTION : 設定為 tls

由 Sync 寄信


MailService.php 3 3GitHub Commit : 由 sync 寄送信件

tests/MailService.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace App\Services;

use Illuminate\Mail\Message;
use Mail;

class MailService
{

public function mailBySync()
{

Mail::send('welcome', [], function (Message $message) {
$message->sender('oomusou@gmail.com');
$message->subject('Laravel 5.2 mail by Sync');
$message->to('oomusou@gmail.com');
});
}
}

使用 Mail::send() 寄送信件。

  • 第 1 個參數為 blade 檔案名稱。
  • 第 2 個參數為要傳給 blade 的陣列,若不傳任何資料,也要傳一個空陣列。
  • 第 3 個參數為 closure,主要是 Laravel 希望你將 Message 物件資料填滿。

實際執行會發現,Mail::send() 會需要等 1 到 2 秒才會執行完,因為是同步,要等 smtp server 回應後才會繼續執行。

單元測試 : 由 Queue 寄信


MailServiceTest.php 4 4GitHub Commit : 單元測試 : 由 queue 寄送信件

tests/MailServiceTest.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use App\Services\MailService;

class MailServiceTest extends TestCase
{

/** @test */
public functionqueue寄送信件()
{

/** arrange */

/** act */
App::call(MailService::class . '@mailByQueue');

/** assert */
$this->assertTrue(true);
}
}

這裡沒做任何測試,只是透過 PHPUnit 啟動 queue 方式寄送信件。

設定本機環境使用 Queue


.env 5 5GitHub Commit : 設定本機環境使用 queue

.env
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
APP_ENV=local
APP_DEBUG=true
APP_KEY=base64:PABSMLELX37jW2jJdwm9Fk6LvUWupulQXAWDZcfA7xE=
APP_URL=http://localhost

DB_CONNECTION=sqlite
DB_HOST=127.0.0.1
DB_PORT=3306
#DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret

CACHE_DRIVER=file
SESSION_DRIVER=file
#QUEUE_DRIVER=sync
QUEUE_DRIVER=database

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_DRIVER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=587
MAIL_USERNAME=[your gmail address]
MAIL_PASSWORD=[your password]
MAIL_ENCRYPTION=tls

15 行

1
2
#QUEUE_DRIVER=sync
QUEUE_DRIVER=database

QUEUE_DRIVERsync 改成 database

設定測試環境使用 Queue


phpunit.xml 6 6GitHub Commit : 設定測試環境使用 queue

phpunit.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="bootstrap/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">

<testsuites>
<testsuite name="Application Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
<exclude>
<file>./app/Http/routes.php</file>
</exclude>
</whitelist>
</filter>
<php>
<env name="APP_ENV" value="testing"/>
<env name="QUEUE_DRIVER" value="database"/>
<env name="DB_CONNECTION" value="sqlite"/>
</php>
</phpunit>

24 行

1
2
3
4
5
<php>
<env name="APP_ENV" value="testing"/>
<env name="QUEUE_DRIVER" value="database"/>
<env name="DB_CONNECTION" value="sqlite"/>
</php>

  • CACHE_DRIVERSESSION_DRIVER 部分刪除。
  • QUEUE_DRIVER 改成 database
  • DB_CONNECTION 改成 sqlite

CACHE_DRIVERSESSION_DRIVER 必須刪除,否則無法再測試環境使用非同步方式寄送 email。

儘管我們之前已經在 .envQUEUE_DRIVER 改成 database,但 phpunit.xmlQUEUE_DRIVERsync 設定會覆蓋掉 .env 的設定,所以這邊也要改成 database

不能如一般單元測試的 DB_CONNECTION 設定成 sqlite_testing,也就是將資料庫設定成 :memory:,因為這種方式是使用 SQLite in Memory,只要測試一執行完,SQLite in Memory 就會被刪除,因此無法被 Forever 使用,因而無法使用非同步方式寄送 email。

建立 jobs table


1
oomusou@mac:~/MyProject$ php artisan queue:table
oomusou@mac:~/MyProject$ php artisan migrate

目前是將 queue 建立在資料庫,因此須建立 jobs table。7 7GitHub Commit : 建立 jobs table

安裝 Forever


在 Laravel 的 Queues 文件中,是建議大家使用 Supervisor,它會讓 php artisan queue:listen 在背景執行,持續地消耗 queue,不過 Supervisor 是 Linux 的程式,無法在 Windows 與 macOS 執行,因此對於開發並不方便。

這裡介紹的是由 Node.js 開發的 Forever,功能與 Supervisor 完全一樣,但因為由 Node.js 所開發,在 Windows、 macOS 與 Linux 都可執行,只要能能成功安裝 Node.js 即可。

1
oomusou@mac:~/$ npm install -g forever

forever 全域安裝。

啟動 Forever


1
oomusou@mac:~/$ forever start -l /Users/oomusou/Code/Laravel/Laravel52QueueForever/forever.log -c php artisan queue:work
  • start : 使用 forever 啟動其他服務。
  • -l : 指定 log 位置, Forever 預設將 log 放在 ~/.forever 目錄下,且每次啟動為亂數,若你想將 log 指定在特定目錄,並使用特定檔名,則必須使用 -l,且必須使用完整路徑。
  • -c : 要啟動的 CLI 命令,使用 php artisan queue:work 執行 queue worker。

由 Queue 寄信


MailService.php 8 8GitHub Commit : 由 queue 寄送信件

tests/MailService.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
namespace App\Services;

use Illuminate\Mail\Message;
use Mail;

class MailService
{

public function mailByQueue()
{

Mail::queue('welcome', [], function (Message $message) {
$message->sender('oomusou@gmail.com');
$message->subject('Laravel 5.2 mail by Queue');
$message->to('oomusou@gmail.com');
});
}
}

使用 Mail::queue() 寄送信件,其他參數與 Mail::send() 完全相同。

實際執行發現,Mail::queue() 會馬上執行完,不會有任何等待,因為是非同步,不用等 smtp server 回應就可繼續執行。

Conclusion


  • 由同步改成非同步方式寄信,在程式方面,只要將 Mail::send() 改成 Mail::queue() 即可。
  • Laravel 支援多種 queue,本文使用最簡單的資料庫方式。
  • Forever 為 Node.js 所開發,在 Windows、macOS 與 Linux 都可使用,非常方便。

Sample Code


完整的範例可以在我的 GitHub 上找到。

Reference


Taylor Otwell, Mail
Taylor Otwell, Queues
foreverjs, forever

2016-11-25