php中的批处理
大杂货店连锁有一个大问题。 每天,人们购买杂货时,每家商店都会发生数千笔交易。 企业高管希望挖掘这些数据。 哪些产品畅销? 哪一个不是 有机产品在哪里卖得好? 冰淇淋呢?
为了捕获此数据,组织必须将所有事务性数据汇总为一个更适合于生成公司需要的报告类型的数据模型。 但是,这需要时间,并且随着链的增长,处理一天的数据量可能需要一天以上的时间。 因此,大问题。
现在,您的Web应用程序可能未处理那么多数据,但是任何站点所花费的时间都可能比客户愿意等待的时间长。 客户在流程出现“缓慢”之前可以等待的通常接受的时间为200毫秒。 这个数字是基于桌面应用程序的,我认为网络已经训练我们变得更加宽容。 但是,您不想让客户等待超过几秒钟。 因此,这里有一些用于PHP批处理作业的策略。
散文和克朗
cron
守护程序是UNIX®计算机上批处理中的主要角色。 该守护程序读取一个配置文件,告诉该文件运行哪些命令行以及运行频率。 然后,守护程序会像发条一样执行它们。 它甚至将任何错误输出发送到指定的电子邮件地址,以便您可以在问题发生时进行调试。
现在,我知道那些大力倡导与咖啡相关的竞争性Web技术的工程师正在摇头。 “线程!线程是进行后台处理的真正方法cron
守护程序是恐龙。”
我有礼貌地不同意。
我都做过,而且我认为cron
具有“保持简单,愚蠢”(KISS)原则的优势。 它使后台处理保持简单。 您无需拥有永久运行的多线程作业处理应用程序,从而永远不会泄漏内存,而是拥有一个简单的批处理脚本,可以通过cron
启动它。 该脚本确定是否有任何事情要做,执行,然后退出。 无需担心内存泄漏。 无需担心线程停滞或陷入无限循环。
那么, cron
如何工作? 好吧,这取决于您的托管解决方案。 我将继续使用旧的简单UNIX命令行版本的cron
,您可以咨询系统管理员以了解如何在Web应用程序中实现它。
这是一个简单的cron
配置,每天晚上11点运行一次PHP脚本:
0 23 * * * jack /usr/bin/php /users/home/jack/myscript.php
前五个字段定义了应启动脚本的时间。 然后是应该用来运行脚本的用户名。 该行的其余部分是要执行的命令行。 时间字段是分钟,小时,一个月中的某天,一个月和一周中的某天。 这里还有一些例子。
命令:
15 * * * * jack /usr/bin/php /users/home/jack/myscript.php
每小时15分钟运行一次脚本。
命令:
15,45 * * * * jack /usr/bin/php /users/home/jack/myscript.php
以每小时15分钟和45分钟的时间运行脚本。
命令:
*/1 3-23 * * * jack /usr/bin/php /users/home/jack/myscript.php
从凌晨3点到晚上11点每分钟运行一次脚本
命令:
30 23 * * 6 jack /usr/bin/php /users/home/jack/myscript.php
在星期六(指定为6
的星期几)的晚上11:30运行脚本。
如您所见,组合是无限的。 您可以根据需要控制脚本运行的时间。 您还可以指定多个脚本来运行,以便某些脚本每分钟运行一次,而其他脚本(也许是备份脚本)每天仅运行一次。
要指定报告的任何错误应通过电子邮件发送到哪里,请使用MAILTO
指令,如下所示:
MAILTO=jherr@pobox.com
注意 :对于Microsoft®Windows®用户,有一个等效的“计划任务”系统可用于定期启动命令行进程(如PHP脚本)。
批处理架构的基础
批处理相对简单。 在大多数情况下,它归结为两个工作流程之一。 第一个用于报告; 该脚本每天运行一次,以生成报告并将其发送给一组人员。 第二个是响应某些请求而创建的批处理作业。 例如,我登录到Web应用程序,并要求它向系统中注册的所有用户发送一条消息,告诉他们有关一项强大的新功能的信息。 因为系统上有10,000人,所以必须批量执行此操作。 这样的任务将花费PHP一段时间才能完成,因此必须由浏览器外部的工作来完成。
在第二个工作流程中,Web应用程序只是将信息放置在与批处理应用程序共享的位置中。 该信息指定了工作的性质(例如,“将电子邮件发送给系统上的所有人员”)。 批处理处理器运行作业,然后删除该作业。 或者,处理器将作业标记为已完成。 无论哪种方式,都应将作业标识为已完成,以便不再运行。
本文的其余部分展示了在Web应用程序前端和批处理后端之间共享数据的各种方法。
邮件队列
第一个版本是专用的邮件排队系统。 在此模型中,数据库中有一个表,其中包含应发送给各个人的电子邮件列表。 Web界面使用mailouts
类将电子邮件添加到队列中。 电子邮件处理器使用mailouts
类检索挂起的电子邮件,然后再次使用它从队列中删除挂起的消息。
该模型以MySQL模式开始。
清单1. mailout.sql
DROP TABLE IF EXISTS mailouts;
CREATE TABLE mailouts (
id MEDIUMINT NOT NULL AUTO_INCREMENT,
from_address TEXT NOT NULL,
to_address TEXT NOT NULL,
subject TEXT NOT NULL,
content TEXT NOT NULL,
PRIMARY KEY ( id )
);
这个模式非常简单。 每行都有一个from
和to
地址,以及电子邮件的主题和内容。
PHP mailouts
类包装在数据库的mailouts
表中。
清单2. mailouts.php
<?php
require_once('DB.php');
class Mailouts
{
public static function get_db()
{
$dsn = 'mysql://root:@localhost/mailout';
$db =& DB::Connect( $dsn, array() );
if (PEAR::isError($db)) { die($db->getMessage()); }
return $db;
}
public static function delete( $id )
{
$db = Mailouts::get_db();
$sth = $db->prepare( 'DELETE FROM mailouts WHERE id=?' );
$db->execute( $sth, $id );
return true;
}
public static function add( $from, $to, $subject, $content )
{
$db = Mailouts::get_db();
$sth = $db->prepare( 'INSERT INTO mailouts VALUES (null,?,?,?,?)' );
$db->execute( $sth, array( $from, $to, $subject, $content ) );
return true;
}
public static function get_all()
{
$db = Mailouts::get_db();
$res = $db->query( "SELECT * FROM mailouts" );
$rows = array();
while( $res->fetchInto( $row ) ) { $rows []= $row; }
return $rows;
}
}
?>
该脚本包括Pear :: DB数据库访问类。 然后,它定义一个具有三个中央静态功能的mailouts
类: add
, delete
和get_all
。 add()
方法将电子邮件添加到队列中,供前端使用。 get_all()
方法返回表中的所有数据。 delete()
方法删除单个方法。
您可能会问,为什么我不只是在脚本结尾处调用delete_all()
方法。 这样的方法不存在有两个原因:如果我在发送完每条消息后都将其删除,则在出现问题后重新运行脚本的情况下,两次发送消息都是不可能的; 并且可能在批处理作业的开始到完成之间添加了新消息。
下一步是编写一个简单的测试脚本,将一个条目添加到队列中。
清单3. mailout_test_add.php
<?php
require 'mailout.php';
Mailouts::add( 'donotreply@mydomain.com',
'molly@nocompany.com.org',
'Test Subject',
'This is a test of the batch mail sendout' );
?>
在这种情况下,我要向某公司的Molly添加mailout
,其中包含测试主题和电子邮件正文。 我可以在命令行上运行此脚本: php mailout_test_add.php
。
要发送电子邮件,我需要另一个脚本作为我的工作处理器。
清单4. mailout_send.php
<?php
require_once 'mailout.php';
function process( $from, $to, $subject, $email ) {
mail( $to, $subject, $email, "From: $from" );
}
$messages = Mailouts::get_all();
foreach( $messages as $msg ) {
process( $msg[1], $msg[2], $msg[3], $msg[4] );
Mailouts::delete( $msg[0] );
}
?>
该脚本使用get_all()
方法检索所有电子邮件,然后使用PHP的mail()
方法一个接一个地发送消息。 成功发送每个记录后, delete()
方法将从队列中删除该单独的记录。
该脚本将使用cron
守护程序定期运行。 脚本运行的频率取决于您和应用程序的需求。
注意: PHP扩展和应用程序存储库(PEAR)存储库包含一个出色的现实世界中的邮件队列系统实现 ,可以免费下载。
更通用的东西
拥有专门用于电子邮件发送的解决方案很好,但是更通用的解决方案呢? 它使我无需等待浏览器就可以发送电子邮件或生成报告或进行其他耗时的处理。
为此,我可以利用PHP是一种解释型语言这一事实,方法是将PHP代码存储在数据库的队列中,然后再执行。 这样做需要两个表,如清单5所示。
清单5.generic.sql
DROP TABLE IF EXISTS processing_items;
CREATE TABLE processing_items (
id MEDIUMINT NOT NULL AUTO_INCREMENT,
function TEXT NOT NULL,
PRIMARY KEY ( id )
);
DROP TABLE IF EXISTS processing_args;
CREATE TABLE processing_args (
id MEDIUMINT NOT NULL AUTO_INCREMENT,
item_id MEDIUMINT NOT NULL,
key_name TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY ( id )
);
第一个表processing_items
包含作业处理器要调用的函数。 第二个表processing_args
包含使用键/值对作为散列表发送给函数的参数。
这两个表就像mailouts
表一样,由PHP类包装,这次称为ProcessingItems
。
清单6.generic.php
<?php
require_once('DB.php');
class ProcessingItems
{
public static function get_db() { ... }
public static function delete( $id )
{
$db = ProcessingItems::get_db();
$sth = $db->prepare( 'DELETE FROM processing_args WHERE item_id=?' );
$db->execute( $sth, $id );
$sth = $db->prepare( 'DELETE FROM processing_items WHERE id=?' );
$db->execute( $sth, $id );
return true;
}
public static function add( $function, $args )
{
$db = ProcessingItems::get_db();
$sth = $db->prepare( 'INSERT INTO processing_items VALUES (null,?)' );
$db->execute( $sth, array( $function ) );
$res = $db->query( "SELECT last_insert_id()" );
$id = null;
while( $res->fetchInto( $row ) ) { $id = $row[0]; }
foreach( $args as $key => $value )
{
$sth = $db->prepare( 'INSERT INTO processing_args
VALUES (null,?,?,?)' );
$db->execute( $sth, array( $id, $key, $value ) );
}
return true;
}
public static function get_all()
{
$db = ProcessingItems::get_db();
$res = $db->query( "SELECT * FROM processing_items" );
$rows = array();
while( $res->fetchInto( $row ) )
{
$item = array();
$item['id'] = $row[0];
$item['function'] = $row[1];
$item['args'] = array();
$ares = $db->query( "SELECT key_name, value FROM
processing_args WHERE item_id=?", $item['id'] );
while( $ares->fetchInto( $arow ) )
$item['args'][ $arow[0] ] = $arow[1];
$rows []= $item;
}
return $rows;
}
}
?>
此类包含三个重要的方法: add()
, get_all()
和delete()
。 就像mailouts
系统一样,前端使用add()
,处理引擎使用get_all()
和delete()
。
清单7显示了一个将脚本元素添加到处理项目队列的测试脚本。
清单7.generic_test_add.php
<?php
require_once 'generic.php';
ProcessingItems::add( 'printvalue', array( 'value' => 'foo' ) );
?>
在这种情况下,我要添加一个对printvalue
函数的调用,并将value
参数设置为foo
。 我使用PHP命令行解释器来运行该脚本,并将方法调用放入队列中。 然后,我使用以下处理脚本来运行该方法。
清单8.generic_process.php
<?php
require_once 'generic.php';
function printvalue( $args ) {
echo 'Printing: '.$args['value']."\n";
}
foreach( ProcessingItems::get_all() as $item ) {
call_user_func_array( $item['function'],
array( $item['args'] ) );
ProcessingItems::delete( $item['id'] );
}
?>
这个脚本非常简单。 它需要get_all()
返回的处理项,然后使用call_user_func_array
(一个内部PHP函数)来使用给定的参数动态调用该方法。 在这种情况下,将调用局部printvalue
函数。
为了演示此功能,我显示了在命令行上发生的情况:
% php generic_test_add.php
% php generic_process.php
Printing: foo
%
看起来不多,但是您明白了。 通过这种机制,您可以将任何PHP函数的处理延迟到以后。
现在,如果您不喜欢将PHP函数名和参数放入数据库中的想法,那么另一种方法是在PHP代码中进行映射,以在数据库中存储的“处理作业类型”名称与实际PHP处理程序之间进行映射。功能。 这样,如果您以后决定更改PHP后端的其他名称,则如果“处理作业类型”字符串匹配,它仍然可以工作。
转储数据库
为了完成代码示例,我展示了一个稍微不同的角度,那就是使用目录中的文件来存储批处理作业,而不是使用数据库。 在这里,我并不是以“以这种方式而不是在数据库中进行操作”的思路来提供这种想法,而是作为一种设计替代方案,您可以选择使用或不使用。
显然,没有架构,因为我们没有使用数据库。 因此,我从包装上面示例中使用的相同类型的add()
, get_all()
和delete()
方法的类开始。
清单9. batch_by_file.php
<?php
define( 'BATCH_DIRECTORY', 'batch_items/' );
class BatchFiles
{
public static function delete( $id )
{
unlink( $id );
return true;
}
public static function add( $function, $args )
{
$path = '';
while( true )
{
$path = BATCH_DIRECTORY.time();
if ( file_exists( $path ) == false )
break;
}
$fh = fopen( $path, "w" );
fprintf( $fh, $function."\n" );
foreach( $args as $k => $v )
{
fprintf( $fh, $k.":".$v."\n" );
}
fclose( $fh );
return true;
}
public static function get_all()
{
$rows = array();
if (is_dir(BATCH_DIRECTORY)) {
if ($dh = opendir(BATCH_DIRECTORY)) {
while (($file = readdir($dh)) !== false) {
$path = BATCH_DIRECTORY.$file;
if ( is_dir( $path ) == false )
{
$item = array();
$item['id'] = $path;
$fh = fopen( $path, 'r' );
if ( $fh )
{
$item['function'] = trim(fgets( $fh ));
$item['args'] = array();
while( ( $line = fgets( $fh ) ) != null )
{
$args = split( ':', trim($line) );
$item['args'][$args[0]] = $args[1];
}
$rows []= $item;
fclose( $fh );
}
}
}
closedir($dh);
}
}
return $rows;
}
}
?>
BatchFiles
类具有三个主要方法: add()
, get_all()
和delete()
。 该类无需进入数据库,而是从名为batch_items的目录中读取和写入文件。
要添加新的批处理项目,我使用以下测试代码。
清单10. batch_by_file_test_add.php
<?php
require_once 'batch_by_file.php';
BatchFiles::add( "printvalue", array( 'value' => 'foo' ) );
?>
值得注意的是,除了类名BatchFiles
,实际上没有任何方法可以知道作业的存储方式。 因此,稍后无需更改接口就可以轻松地将其转换为数据库样式的存储。
最后,有处理器代码。
清单11. batch_by_file_processor.php
<?php
require_once 'batch_by_file.php';
function printvalue( $args ) {
echo 'Printing: '.$args['value']."\n";
}
foreach( BatchFiles::get_all() as $item ) {
call_user_func_array( $item['function'], array( $item['args'] ) );
BatchFiles::delete( $item['id'] );
}
?>
该代码几乎与数据库版本相同,但文件和类名有所更改。
结论
如前所述,服务器上的线程处理后台批处理有很多支持。 当然,在某些情况下,从助手线程中解脱来处理小任务要容易一些。 但是很容易看出,通过使用常规工具( cron
,MySQL,标准的面向对象PHP和Pear :: DB),在PHP应用程序中创建批处理作业很容易做到,易于部署和易于维护。 。
翻译自: https://www.ibm.com/developerworks/opensource/library/os-php-batch/index.html
php中的批处理