uoload
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
|
||||
namespace StreamQueue;
|
||||
|
||||
use Redis;
|
||||
|
||||
/*
|
||||
* 需要redis-server5.0以上
|
||||
* php-redis扩展版本要适配redis-5.0
|
||||
*
|
||||
* 使用redis stream仿照beanstalk封装的队列服务
|
||||
*/
|
||||
|
||||
class RedisQueue
|
||||
{
|
||||
protected $_mRedis = null;
|
||||
protected $_mStream = '';
|
||||
protected $_mGroup = '';
|
||||
protected $_mConsumer = '';
|
||||
|
||||
//默认0 不限制长度
|
||||
protected $_mMaxLength = 0;
|
||||
|
||||
/*
|
||||
* 创建队列, stream+group确认唯一队列
|
||||
* $config必须包括:
|
||||
* stream: stream名
|
||||
* server: 格式ip:port[:auth]
|
||||
*
|
||||
* 可选参数:
|
||||
* maxLength:队列最大长度
|
||||
* group:分组名, 默认与stream相同. stream+group相当于beanstalk的tube
|
||||
* consumer:消费者名, 默认与stream相同.
|
||||
* */
|
||||
public function __construct(array $config, Redis $redis = null)
|
||||
{
|
||||
if (!isset($config['stream'])) {
|
||||
throw new \Exception("you must config the stream");
|
||||
}
|
||||
|
||||
$this->_mStream = $config['stream'];
|
||||
|
||||
if (is_null($redis)) {
|
||||
|
||||
if (!isset($config['server'])) {
|
||||
throw new \Exception("you must config the server");
|
||||
}
|
||||
|
||||
$tmp = explode(':', $config['server']);
|
||||
$host = $tmp[0];
|
||||
$port = $tmp[1];
|
||||
$auth = $tmp[2] ?? null;
|
||||
|
||||
if ($host && $port) {
|
||||
$this->_mRedis = new Redis();
|
||||
$this->_mRedis->connect($host, $port, 1);
|
||||
if ($auth) {
|
||||
$this->_mRedis->auth($auth);
|
||||
}
|
||||
} else {
|
||||
throw new \Exception("can not get redis server conf");
|
||||
}
|
||||
} else {
|
||||
$this->_mRedis = $redis;
|
||||
}
|
||||
|
||||
if (isset($config['maxLength'])) {
|
||||
$this->_mMaxLength = $config['maxLength'];
|
||||
}
|
||||
|
||||
$this->_mGroup = $config['group'] ?? $config['stream'];
|
||||
$this->_mConsumer = $config['consumer'] ?? $config['stream'];
|
||||
|
||||
$this->creatGroup();
|
||||
}
|
||||
|
||||
/*
|
||||
* 删除当前流(队列)
|
||||
* */
|
||||
public function destoryStream()
|
||||
{
|
||||
$this->_mRedis->del($this->_mStream);
|
||||
}
|
||||
|
||||
/*
|
||||
* 向流中添加任务
|
||||
* $data: array
|
||||
* $id: string
|
||||
* */
|
||||
public function addTask(array $data, string $id = "*")
|
||||
{
|
||||
return $this->_mRedis->xAdd($this->_mStream, $id, $data, $this->_mMaxLength);
|
||||
}
|
||||
|
||||
/*
|
||||
* 从group中获取任务
|
||||
* $block:阻塞时间,毫秒. null不阻塞
|
||||
* $count:读取条数, 只要有数据,条数不够也会立刻返回,即使设置了block
|
||||
* $start:'>'接受最新数据. 若设置id,则读取大于该id,且未被ack的历史任务
|
||||
*
|
||||
* return [
|
||||
* 'id1' => taskdata1,
|
||||
* 'id2' => taskdata2,
|
||||
* ... ...
|
||||
* ]
|
||||
*
|
||||
* 无数据返回[]
|
||||
* */
|
||||
public function getTask($block = null, $count = 1, $start = '>')
|
||||
{
|
||||
$d = $this->_mRedis->xReadGroup($this->_mGroup, $this->_mConsumer, [$this->_mStream => $start], $count, $block);
|
||||
|
||||
if (is_array($d) && count($d) > 0) {
|
||||
return $d[$this->_mStream];
|
||||
}
|
||||
|
||||
return $d;
|
||||
}
|
||||
|
||||
/*
|
||||
* 根据id
|
||||
* ack任务--从pending中删除
|
||||
* 同时从stream中删除
|
||||
*/
|
||||
public function delTask($ids)
|
||||
{
|
||||
if (!is_array($ids)) {
|
||||
$ids = array($ids);
|
||||
}
|
||||
$multi = $this->_mRedis->multi(Redis::PIPELINE);
|
||||
|
||||
$multi->xAck($this->_mStream, $this->_mGroup, $ids);
|
||||
$multi->xDel($this->_mStream, $ids);
|
||||
$res = $this->_mRedis->exec();
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function creatGroup($startID = 0)
|
||||
{
|
||||
return $this->_mRedis->xGroup('CREATE', $this->_mStream, $this->_mGroup, $startID, true);
|
||||
}
|
||||
|
||||
/*
|
||||
* 获取pending队列概要信息
|
||||
* return
|
||||
*
|
||||
array(4) {
|
||||
[0] => int(2) //未收到ack的总条数
|
||||
[1] => string(15) "1604908088616-0" //startID
|
||||
[2] => string(15) "1604908088616-1" //endID
|
||||
[3] => //每个消费者的概要
|
||||
array(1) {
|
||||
[0] =>
|
||||
array(2) {
|
||||
[0] => string(6) "normal" //消费者名子
|
||||
[1] => string(1) "2" //条数
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
protected function getPendingInfo()
|
||||
{
|
||||
return $this->_mRedis->xPending($this->_mStream, $this->_mGroup);
|
||||
}
|
||||
|
||||
/*
|
||||
* 获取pending数据
|
||||
* $count: 条数
|
||||
* $start: 起始id
|
||||
* $end: 终止id. - +表示全部
|
||||
* $consumer:获取特定consumer的pending数据
|
||||
*
|
||||
* return
|
||||
*array(1) {
|
||||
[0] => //单条数据
|
||||
array(4) {
|
||||
[0] => string(15) "1604908973008-0" //id
|
||||
[1] =>string(6) "normal" //消费者名
|
||||
[2] =>int(0) //被取走后经历的毫秒数
|
||||
[3] =>int(1) //投递次数
|
||||
}
|
||||
}
|
||||
*/
|
||||
protected function getPending($count = 1, $start = '-', $end = '+', $consumer = null)
|
||||
{
|
||||
if (!$consumer) {
|
||||
return $this->_mRedis->xPending($this->_mStream, $this->_mGroup, $start, $end, $count);
|
||||
}
|
||||
|
||||
return $this->_mRedis->xPending($this->_mStream, $this->_mGroup, $start, $end, $count, $consumer);
|
||||
}
|
||||
|
||||
/*
|
||||
* 取[$start, $end]范围内的数据, 注意是闭区间
|
||||
*
|
||||
* $count:条数,null时表示取全部
|
||||
* */
|
||||
protected function getRange($start = '-', $end = '+', $count = null)
|
||||
{
|
||||
if (is_null($count)) {
|
||||
return $this->_mRedis->xRange($this->_mStream, $start, $end);
|
||||
} else {
|
||||
return $this->_mRedis->xRange($this->_mStream, $start, $end, $count);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 将pending队列中超时的数据重新放回队列
|
||||
*
|
||||
* $idleTime: 超时时间, 毫秒
|
||||
* $perPage:每次取的任务数
|
||||
*
|
||||
* 注意:只能有一个进行执行pendingRestore
|
||||
*
|
||||
* 优点: consumer不需要做任何改动
|
||||
* 缺点:
|
||||
* 先del再add, 成本上不划算,
|
||||
* 如果del和add中间断掉任务就丢了
|
||||
*
|
||||
* return: restore的数量
|
||||
* */
|
||||
public function pendingRestore($idleTime = 5000, $perPage = 20)
|
||||
{
|
||||
/**
|
||||
* 比较简单粗暴的取pending数据方式
|
||||
* 依赖
|
||||
* 1.每次从pending取走/删除超时数据
|
||||
* 2.id是按时间排序,小id未超时,大id一定未超时
|
||||
*
|
||||
*/
|
||||
$restoreNum = 0;
|
||||
while (1) {
|
||||
$thisNum = 0;
|
||||
$data = $this->getPending($perPage);
|
||||
|
||||
foreach ($data as $one) {
|
||||
$id = $one[0];
|
||||
$duration = $one[2];
|
||||
if ($duration > $idleTime) {
|
||||
$data = $this->getRange($id, $id);
|
||||
$task = $data[$id];
|
||||
|
||||
$this->delTask($id);
|
||||
$this->addTask($task);
|
||||
|
||||
$thisNum++;
|
||||
}
|
||||
}
|
||||
|
||||
$restoreNum += $thisNum;
|
||||
|
||||
if ($thisNum < $perPage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $restoreNum;
|
||||
}
|
||||
|
||||
/*
|
||||
* 另一种恢复超时任务的方法
|
||||
* 思路:将超时任务放入newConsumer的pending中,后续可以从newConsume的历史中取出数据并处理
|
||||
*
|
||||
* 优点:恢复数据没有重复读,删,插,效率高
|
||||
* 缺点:
|
||||
* consumer需要做改动,至少要改变consumer的名子
|
||||
* 只能用单进程从历史数据中读数据,然后处理。
|
||||
*
|
||||
* 详见PendingProcessor类
|
||||
*
|
||||
* $idleTime: 超时时间, 毫秒
|
||||
* $newConsumer: 之后处理pending任务的消费者名称
|
||||
* $perPage: 每次取pending任务的条数
|
||||
*
|
||||
* return: 满足条件且成功claim的条数
|
||||
* */
|
||||
public function pendingClaim($idleTime = 5000, $newConsumer = null, $perPage = 20)
|
||||
{
|
||||
if (!$newConsumer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$info = $this->getPendingInfo();
|
||||
$startID = $info[1];
|
||||
$endID = $info[2];
|
||||
|
||||
$claimNum = 0;
|
||||
/*
|
||||
* 使用startid, endid遍历pending列表
|
||||
* 因为getpending取的是[startid, endid]
|
||||
* 所以边界处的id可能被重复取出,但不影响结果的正确性
|
||||
* perPage越大/符合xclaim条件的id越多,重复的可能性越小
|
||||
* */
|
||||
while ($startID != $endID) {
|
||||
//var_dump([$startID, $endID]);
|
||||
$data = $this->getPending($perPage, $startID, $endID, $this->_mConsumer);
|
||||
|
||||
foreach ($data as $one) {
|
||||
$ids[] = $one[0];
|
||||
$startID = $one[0];
|
||||
}
|
||||
|
||||
$res = $this->_mRedis->xClaim($this->_mStream, $this->_mGroup, $newConsumer, $idleTime, $ids, ['JUSTID']);
|
||||
|
||||
$thisNum = count($res);
|
||||
$claimNum += $thisNum;
|
||||
|
||||
//id是按时间排列,小id未超时,则后面不会超时
|
||||
//在所有id都有相同的投递次数的基础上
|
||||
//var_dump($thisNum);
|
||||
if ($thisNum < $perPage) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $claimNum;
|
||||
}
|
||||
}
|
||||
|
||||
/* demo
|
||||
$config = [
|
||||
'server' => '10.160.75.237:6379:auth',
|
||||
'stream' => 'balltube',
|
||||
'consumer' => 'normalprocessor'//可以不设置
|
||||
];
|
||||
|
||||
//创建队列
|
||||
$q = new RedisQueue($config);
|
||||
|
||||
//添加任务
|
||||
$task = ['task'=>1];
|
||||
$q->addTask($task);
|
||||
|
||||
//获取
|
||||
$timeout = 1000;
|
||||
$task = $q->getTask($timeout);
|
||||
|
||||
//确认并删除
|
||||
$taskid = key($task);
|
||||
$q->delTask($taskid);
|
||||
|
||||
//处理pending
|
||||
$q->pendingRestore();
|
||||
$q->pendingClaim();
|
||||
*/
|
||||
Reference in New Issue
Block a user