DB更新とメール送信とをトランザクションにつつむ
結論:
- Webアプリで「データベースに値をつっこむと同時にメールも送信する」という仕様はよくある。たとえば、ECサイトで注文があったら注文テーブルに書き込むと同時にお礼のメールを送信する、とか。
- 単純に「SQL発行」と「メール送信」をソース上に並べてるだけのケースが多いだろうし、それはそれで特別まずいわけでもないと思う。
- しかし、DB書き込みとメール送信とを厳密に(アトミックに)処理したいのであれば、
- メールは、そのとき本当に送信しようとするのではなく、データベースのテーブル上にメールの内容をINSERTする方式にして(つまりキューイングみたいな概念)、
- アプリ内では、DB接続→SQL準備→BEGIN→SQL発行(ex.受注処理とか)→メール送信用SQL発行(キューイング)→両方のSQLが成功したらCOMMIT。
- 別立てで作っておいたメール送信処理バッチ(デーモン)がメールキューテーブルの中を読んでメール送信
というお話。そこそこの規模と信頼性を求められるサイトをやっている人にとってはめずらしくもない方法論だが、本屋さんにある入門書やサンプル集とにらめっこしながら作っている人にとってはこういう発想の転換っぽいことはなかなか気づかないんじゃないだろうか。
ECサイトの注文受付処理の典型的な仕様として、
1. 受注テーブルにINSERTする。
2. お礼のメールをユーザーに送信する。
というのがある。ここで、「DBサーバの調子が悪くて受注テーブルにうまく書き込めなかった。でもお礼のメールは送ってしまった」あるいは「受注テーブルには入ったけど、メールサーバの調子が悪くてメールは送れなかったらしい」といった事態はどちらも避けたいものだ。(特に前者は)
「DB接続→BEGIN→受注テーブルINSERT→メール送信(SMTPまたはsendmailコマンド直たたき)→INSERTとメール送信が両方とも成功しているようならCOMMIT」 とかやっているケースが多いと思う。
でもまず、「メール送信が成功したかどうか」の厳密な判定は難しい(気がする)。postfix付属のsendmailコマンドはpostfixがコマンドを受け付けただけで正常値を返しそうだし(違ったらごめん)。それでいいっちゃいいけど。
受注webアプリ(が呼び出すメール送信モジュール)が直にSMTPをしゃべるのであれば、そのしゃべり相手のメールサーバが混みあって反応がトロかったりしたら、それのせいで受注Webアプリ内のDBトランザクションがタイムアウトしたりしないだろうか。あるいはブラウザを見つめてるユーザーを必要以上に待たせることにならないだろうか。それに、そもそも送り先ユーザのメールサーバが死んでたりしたら?
いずれにせよE-Mail(SMTP)そのものがRDBMSほどの信頼性を持ってないと考えるのであれば、もうどうしようもないのだろう。
だったら、DB更新処理とメール送信処理とをひとまず分けてしまってはどうだろう。DB上にメールキュー用のテーブルをひとつ作って、「メール送信処理」を「メールキューテーブルにメール情報をINSERTする処理」に置換して、本当にメール送信する処理は別のバッチ処理にでも託し、問題を先送り分けて考えるのだ。メール送信に若干のタイムラグが発生するだろうが、どうせ「メールが届くのに数分程度かかる場合があります」って説明が画面上にもともと書いてあったりするし(笑)。それに、「同じメールを再度送信する」という処理が必要になったときに、メール処理だけで独立している構造のほうが追加実装しやすいという利点がありそうだ。
さて実際の実装だが、perlの場合は Tripletail::Sendmail::MailQueueというのがある。DB上にキューイングするのではなく単純にファイルに書いてキューとするものだが、実用には十分耐えるだろう。そもそもキューイングでDBに負荷かけないのはいいことかもしれない。
PHPの場合は、うーん、どうしよう。 PEARのMail_Queueってのがあるんだが、はて、DB上のテーブルを使うようになってはいるものの、外部からトランザクションを制御する(orトランザクション制御済みのDB接続オブジェクトを受け渡しする)ための機構がなさげ。うーん。
コメントする
(初めてのコメントの時は、コメントが表示されるためにこのブログのオーナーの承認が必要になることがあります。承認されるまでコメントは表示されませんのでしばらくお待ちください)