前言
在进行项目开发的时候, 我们经常能遇到需要实现定时任务的应用场景, 比如: 登录Session管理、计划任务、按时间服务的计费订单. 通常情况下我们使用(MinHeap)最小堆来实现.
什么是 MinHeap
MinHeap 是一个满足任意非叶子节点的数据均不大于其左子节点和右节点的值的完全二叉树。在针对超时任务的场景中,MinHeap 能保证最小的节点永远都是根节点。
PHP的SPL标准库也实现了一套MinHeap, 通过使用它,我们很容易就能实现一套定时器任务. 不过也仅此而已,当你想进一步完善你的作品的时候 splMinHeap 的局限性也就显露出来了.
下面贴上一个使用splMinHeap的例子:
//超时任务管理类
class SessionTaskMgr extends \SplMinHeap
{
/**
* 内部的比较函数
*/
protected function compare(mixed $a, mixed $b) : int {
if($a->expire > $b->expire)
return -1;
if($a->expire < $b->expire)
return 1;
return 0;
}
}
class SessionTask {
protected int $expire; # Session 保存时间
protected int $tickId; # 定时器ID
public function __construct(int $expire){
$this->expire = $expire;
static::$mgr->insert($this);
}
/**
启动超时任务
*/
public static function start(){
// 创建超时任务管理器
static::$mgr = $mgr = new FileLifeTaskMgr();
# 定时检查最小对里面是否存在达到了执行时间的任务.
static::$tickeId = Timer::tick(1000, function() use($mgr) {
$time = time();
while(!$mgr->isEmpty())
{
$node = $mgr->extract();
if($node->expire > $time) {
$mgr->insert($node);
break;
}
$node->execute();
}
});
}
/**
关闭超时任务
*/
public static function shutdown(bool $force = true) {
Timer::clear(static::$tickeId);
$tasks = static::$tasks;
foreach($tasks as $task){
$task->cancel($basedir);
}
}
public function execute(){
echo "超时任务执行...\n";
}
public function cancel(){
echo "超时任务取消...\n";
}
}
看上去很简单对吧, 但由于 splMinHeap 并未实现 删除接口, 当任务的时间发生改变后, 也无法调整节点的执行时间, 因此灵活性很大程度受到限制。
需求1: 我们做了一个登录Session管理, Sesison 超时设定为 2小时, 用户每次有网络请求的时候. 我们就将超时设定为当前时间往后延2小时. 即使不能调整位置,目前的splMinHeap也是能做到的,只需要在到达时间后 弹出该节点判断下是否真的超时,如果没有超时则重新加入 splMinHeap中即可。
需求2: 如果我们希望实现用户点击退出登录后立即从 splMinHeap中移除该节点呢? 这就无法做到了。我们也只能老老实实等到2小时后 实际超时了再去做处理。
或许同学们可能要说,我们在session节点中加入一个成员,判断该节点是否有效不就行了,反正到了时间就能从 splMinHeap中弹出来。 是的,在不考虑不必要的内存开销的情况下,该方案也的确是可行的,又不是不能用^_^。
如果换一个场景,张三开了个洗脚城,顾客可以往会员卡里面进行充值。顾客到店后选好套餐。就开始服务。离店系统自动根据消费时长扣费。 有大量顾客充值。每次顾客进店我们就根据用户的越创建一个定时任务。钱扣光就自动结束任务。 或者用户主动离店。提前结束任务。假设有大量用户在充值了大额的费用,一笔订单加入到定时器后。可能要1年后才被触发。 这个时候 由于 splMinHeap 没有删除功能, 这笔订单会一直保存在内存中,会导致内存占用量一直增长。
我很好奇为什么不能提供删除功能。通过阅读 spl库源代码才发现,人家是通过数组实现的。 由于数组删除成员会导致大量复制的缘故 因此没有提供删除功能。
尝试解决
参考 libevent 中的 minheap实现。我们自己使用C语言为php开发一个扩展来实现可以调整节点、可以删除节点的 MinHeap类。同时使用上也需要做到更加灵活。
实现后的原型如下:
namespace mexti;
class MinHeap{
/**
* 获取内部成员数目
*/
public function count() : int;
/**
* 是否为空 等价于 count() == 0
*/
public function isEmpty() : bool;
/**
* 插入一个成员到最小堆中
*/
public function insert(MinHeapNode $n) : bool|int;
/**
* 从最小堆中移除一个成员
*/
public function erase(MinHeapNode $n) : bool|int;
/**
* 成员键值更新后, 调整在最小堆中的位置
*/
public function adjust(MinHeapNode $n) : bool|int;
/**
* 从最小堆中弹出一个成员
*/
public function extract() : MinHeapNode;
/**
* 获取最小堆中下一个将要弹出的成员(并不会弹出),
*/
public function top() : MinHeapNode;
}
class MinHeapNode{
/**
* 需要被继承类实现的比较方法
*/
public abstract compare(\mexti\MinHeapNode $b) : int;
/**
* 是否在 MinHeap池中.
*/
public function inHeap() : bool;
/**
* 索引更新后调整位置
*/
public function adjust(): bool;
/**
* 从MinHeap中移除自身
*/
public function erase() : bool;
}
复制代码
用法:
直接上代码吧:
class SampleNode extends MinHeapNode {
// 比较键值
protected int $key;
/*
实现比较方法
*/
public function compare(\MinHeapNode $b) : int {
if($this->key > $b->key) return 1;
elseif($this->key < $b->key) return -1;
return 0;
}
}
$heap = new \mexti\MinHeap();
function addnodes($heap, int $count){
echo "add {$count} nodes...\n";
for($i=0;$i < $count; $i++){
$heap->insert(new MyNode(rand(0,999)));
}
}
addnodes($heap, 1000);
while(!$heap->isEmpty()){
# 弹出一个节点
$node = $heap->extract();
}
目前仓库已经开源: MinHeap PHP扩展代码
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。