Kafka學習之(四)PHP操作Kafka

OldBoy~發表於2018-01-17

簡單測試

環境:Centos6.4,PHP7,kafka伺服器IP:192.168.9.154,PHP伺服器:192.168.9.157

在192.168.9.157建立目錄和檔案。

//生產者
<?php
require './modules/kafka.php';
$rk = new kafka();
$rk->send(['hello my kafka']);
echo 'OK~';
//消費者
<?php
require './modules/kafka.php';
$rk = new kafka();
$rk->consumer();
//Kafka類
<?php
class kafka
{
    public $broker_list = '192.168.9.154:9092';  //現在是一個,也可以多個,用逗號隔開
    public $topic = 'mytopic';                      //定義topic
    public $partition = 0;                         //定義topic的物理分組,這裡是0
    public $logFile = './kafkalog/info.log';     //計劃在消費的時候寫日誌,指定日誌檔案

    protected $producer = null;                  //儲存producer物件
    protected $consumer = null;                     //儲存consumer物件

    public function __construct()
    {

        if (empty($this->broker_list)) 
        {
            echo 'broker not config';
        }
        $rk = new \Rdkafka\Producer();  //例項化物件

        if (empty($rk)) {
            echo 'producer error1';
        }
        $rk->setLogLevel(LOG_DEBUG);  //設定錯誤級別
        if(!$rk->addBrokers($this->broker_list)) {//設定Broker地址
            echo 'producer error2';
        }     
        $this->producer = $rk;
    }

    //生產者的方法(生產者把日誌向訊息佇列傳送)
    public function send($message = []) 
    {
        
        $topic = $this->producer->newTopic($this->topic);  //建立topic

        $topic->produce(RD_KAFKA_PARTITION_UA, $this->partition, json_encode([$message]); //生產
    }
    //消費者方法 (監聽訊息佇列)
    public function consumer()
    {
        $conf = new \Rdkafka\Conf();
        $conf->set('group.id', 0);
        $conf->set('metadata.broker.list', $this->broker_list);
        $topicConf = new \Rdkafka\topicConf();
        $topicConf->set('auto.offset.reset', 'smallest');

        $conf->setDefaultTopicConf($topicConf);

        $consumer = new \Rdkafka\kafkaConsumer($conf);

        $consumer->subscribe([$this->topic]); //訂閱

        echo "wating for message....\n";

        while(true) {
            $message = $consumer->consume(0*1000);
            switch ($message->err) {
                case RD_KAFKA_RESP_ERR_NO_ERROR:
                    echo '要處理訊息了~~~';
                    $messageInfo = $message->payload;
                    // echo $messageInfo."\n";
                    break;
            }
            sleep(1);
        }
    }
}

記住消費者PHP檔案要在終端執行:php consumer.php。

這裡就不測試了。

工作程式碼

/**
     * 將使用者的登陸資訊放到 Kafka
     *
     */
    public function sendCustomerLoginInfoToKafka($param){
        $customerLoginInfoServiceClient = new CustomerLoginInfoServiceClient();
        $msg =  json_encode($param);
        $topic=isset(Yii::app()->params['customer_login_info_topic'])?Yii::app()->params['customer_login_info_topic']:'e_user_login_info';
        $result = $customerLoginInfoServiceClient->add($topic, $msg);
    }
/**
 * 客戶登陸資訊 服務化介面呼叫client端
 */
class CustomerLoginInfoServiceClient {

    public function add($topic, $msg) {
        //直接進kafka不再呼叫java服務
        EdjKafka::getInstance()->putin(array("topic" => $topic, "payload" => $msg));
    }

}
class EdjKafka {

    private static $instance;

    public static function getInstance($className=__CLASS__) {
        if (empty(self::$instance)) {
            self::$instance = new $className();
        }
        return self::$instance;
    }

    public function putin($params) {
        $task = array(
            'class' => __CLASS__,
            'method' => 'putin_job',
            'params' => $params,
        );
        Queue::model()->putin($task, 'phptokafka_0000');
    }

    public function putin_job($params) {
        KafkaProducer::getInstance()->putin($params["topic"], $params["payload"]);
    }
}
<?php
require_once(Yii::app()->basePath.'/vendors/kafka/autoload.php');  //kafka包在最下面

class KafkaProducer {

    private static $instance;
    private $producer;
    private $partitionCountMap = array();

    public static function getInstance($className=__CLASS__) {
        if (empty(self::$instance)) {
            self::$instance = new $className();
        }
        return self::$instance;
    }

    public function __construct() {
        $brokers = Yii::app()->params['kafka']['brokers'];

        $newProducer = \Kafka\Produce::getInstance($brokers, 3000, $brokers);
        $newProducer->setRequireAck(-1);
        $this->producer = $newProducer;
    }

    private function getPartitionCount($topic, $force=false) {
        $now = time();

        //3分鐘查詢一次patition
        if( !$force && array_key_exists($topic, $this->partitionCountMap) && $this->partitionCountMap[$topic]["expire"] > $now ) {
            return $this->partitionCountMap[$topic]["count"];
        }


        //獲取到topic下可用的partitions
        $this->producer->getClient()->refreshMetadata();
        $partitions = $this->producer->getAvailablePartitions($topic);
        EdjLog::info(__METHOD__.'|'.$topic.'|get partition|'.json_encode($partitions));
        $partitionCount = count($partitions);
        if ($partitionCount == 0) {
            EdjLog::error(__METHOD__."|".$topic."|topic partitions count 0");
        }

        $this->partitionCountMap[$topic] = array("count" => $partitionCount, "expire" => $now + 180);

        return $partitionCount;
    }

    public function putin($topic, $payload) {
        if(empty($topic)) {
            return;
        }

        $partitionCount = $this->getPartitionCount($topic);

        if ($partitionCount != 0) {
            try {
                $pid = time() % $partitionCount;
                $this->producer->setMessages($topic, $pid, array($payload));
                $result = $this->producer->send();
                EdjLog::debugLog(__METHOD__.'|'.$topic.'|'.$pid);
            } catch (\Kafka\Exception $e) {
                EdjLog::error(__METHOD__.'|'.$e->getMessage());
                $this->getPartitionCount($topic, true);
            }
        }
    }
}
<?php
return array(
    'brokers' => "123.123.123.123:9092,123.123.123.123:9093,123.123.123.123:9094",  //ip一樣,埠不一樣
    //topic名的對映,推薦用class名字做key
    //測試環境和線上用不同的配置檔案
    'topicmap' => array(
        "RDriverPositionToKafka" => "driver_location_test",
        "ROrderToKafka" => "order_test",
        "SubmitOrderAutoService_saveOrderInfoJob" => "finished_order_picture",
        'vip_customer_change' => 'vip_customer_change',
     ),
);
連結:https://pan.baidu.com/s/1xiHAt8mbxpdPLhqZbKL1LQ 
提取碼:l92h  //kafka包
<?php
/**
 * 基於redis的queue佇列
 */
class Queue {
    private static $_models;

    public $queue_max_length = array(
    );

    public static function model($className=__CLASS__) {
        $model=null;
        if (isset(self::$_models[$className]))
            $model=self::$_models[$className];
        else {
            $model=self::$_models[$className]=new $className(null);
        }
        return $model;
    }

    //確定redis
    private function select_redis($type) {
        return QueuePool::model()->get_zone($type);
    }

    private function trim($queue_name) {

        $type = str_replace("queue_", "", $queue_name);
        $max = 0;
        if (isset($this->queue_max_length[$type])) {
            $max = intval($this->queue_max_length[$type]);
        }
        if ($max>0) {
            $zone = $this->select_redis($type);
            if($zone) {
                $zone['redis']->lTrim($queue_name, 0, $max-1);
            }
            else {
                EdjLog::error("can not find zone, queue name: " . $type);
                return;
            }
        }
    }

    /**
     * 放入佇列,統一佇列對外暴露方法,增加型別預設放task佇列,指定了就放對應的佇列,同時如果不在指定型別內的,也放預設佇列
     *
     * @author sunhongjing 2013-07-04
     * @param unknown_type $params
     * @param unknown_type $type
     * @return mixed
     */
    public function putin($params=null, $type){
        $type = empty($type) ? 'error' : strtolower($type);

                $base_qname = QNameManagerService::model()->get_base_qname($type);

        if(!empty($base_qname)) {
            $this->queue_name = 'queue_'.$base_qname;
        }else{
            $this->queue_name = 'queue_error';
        }

        if ($params===null) {
            return $this->get();
        } else {
            return $this->add($params);
        }
    }

    /**
     * 取一條佇列資料,封裝多個佇列,統一呼叫方法
     * @author sunhongjing 2013-07-09
     * @param string $type
     * @return array
     */
    public function getit($type='default')
    {
        $base_qname = QNameManagerService::model()->get_base_qname($type);

        if(!empty($base_qname)) {
            $this->queue_name = 'queue_'.$base_qname;
        }else{
            return array();
        }

        $zone = $this->select_redis($type);
        if($zone) {
            if($zone['brpop']) {
                $json = '';
                $result = $zone['redis']->brPop($this->queue_name, $zone['brpop']);
                if(!empty($result) && isset($result[1])) {
                    $json = $result[1];
                }
            }
            else {
                $json = $zone['redis']->rPop($this->queue_name);
            }
        }
        else {
            EdjLog::error("can not find zone, queue name: " . $type);
            return array();
        }

        return json_decode($json, true);
    }

    /**
     * 返回佇列接收的型別列表
     * @author sunhongjing 2013-07-04
     * @return array
     */
    public function getQueueTypeList()
    {
        $list = QNameManager::model()->findall();
        if($list) {
            return $list;
        }

        EdjLog::error("Error: get queue list from database");
        return array();
    }

    /**
     * 設定或者讀取位置佇列
     * @param array $params
     * @return mixed
     */
    public function position($params=null) {
        $this->queue_name='queue_position';

        if ($params===null) {
            return $this->get();
        } else {
            return $this->add($params);
        }
    }

    /**
     * 心跳佇列
     * @param string $params
     * @return mixed
     */
    public function heartbeat($params=null) {
        $this->queue_name='queue_heartbeat';

        if ($params===null) {
            return $this->get();
        } else {
            return $this->add($params);
        }
    }

    /**
     * 最高優先順序佇列
     * @param string $params
     * @return mixed
     */
    public function task($params=null) {
        $this->queue_name='queue_task';

        if ($params===null) {
            return $this->get();
        } else {
            return $this->add($params);
        }
    }

    /**
     * 儲存日誌到資料庫
     * @param string $params
     * @return mixed
     */
    public function dumplog($params=null) {
        $this->queue_name='queue_dumplog';

        if ($params===null) {
            return $this->get();
        } else {
            return $this->add($params);
        }
    }

    /**
     * 返回各個佇列中的任務總數
     */
    public function length() {

        $queue = $this->getQueueTypeList();

        $queue_length=array();
        $reg = "/P[0-9]+$/";
        foreach($queue as $item) {
            $base_qname = $item->base_qname;
            $zone = $this->select_redis($base_qname);
            $key = 'queue_'.$base_qname;
            if($zone) {
                $len = $zone['redis']->lLen($key);
                if(isset($item->max) && $len > $item->max) {
                    $key = '!'.$key;
                }

                $pkey = '';
                if(preg_match($reg, $zone['name'])) {
                    $pkey = $key.'@'.$zone['name'];
                }
                else {
                    $pkey = $key.'@'.$zone['name']."_P".$item->level;
                }

                $queue_length[$pkey] = $len;
            }
            else {
                EdjLog::error("can not find zone, queue name: " . $key);
            }
        }

        return $queue_length;
    }

    private function get() {
        $type = str_replace("queue_", "", $this->queue_name);
        $zone = $this->select_redis($type);
        if($zone) {
            if($zone['brpop']) {
                $json = '';
                $result = $zone['redis']->brPop($this->queue_name, $zone['brpop']);
                if(!empty($result) && isset($result[1])) {
                    $json = $result[1];
                }
            }
            else {
                $json = $zone['redis']->rPop($this->queue_name);
            }
        }
        else {
            EdjLog::error("can not find zone, queue name: " . $type);
            return array();
        }
        return json_decode($json, true);
    }

    private function add($params) {
        $json=json_encode($params);
        $type = str_replace("queue_", "", $this->queue_name);
        $zone = $this->select_redis($type);
        $return = 0;
        if($zone) {
            try {
                $return = $zone['redis']->lPush($this->queue_name, $json);
            } catch (Exception $e) {
                EdjLog::error("write redis error,msg:".$e->getMessage());
                //echo $e->getMessage();
            }
        }
        else {
            EdjLog::error("can not find zone, queue name: " . $type);
        }

        return $return;
    }

    public function processTask($task) {
        if(!isset($task['method'], $task['params'])) {
            $task_content = json_encode($task);
            EdjLog::error("can not run task due to no 'method' or 'params' specified, task is $task_content");
            return;
        }

        $method=$task['method'];
        $params=$task['params'];
        $class = isset($task['class']) ? $task['class'] : "QueueProcess";
        EdjLog::info("REDIS_QUEUE_OUT CLASS:$class METHOD:$method PARAMS:".json_encode($params));
        try {
            //throw new Exception("Value must be 1 or below");
            $queue_process=new $class();
            // check this method is exist, if not throw ReflectionException
            new ReflectionMethod($queue_process, $method);
            call_user_func_array(array($queue_process, $method), array($params));
        } catch(Exception $e) {
            $errmsg = $e->getMessage();
            EdjLog::error("execption queue_run method:$method err: $errmsg");
        }
    }

    public function getLengthByType($type){
        $type = empty($type) ? 'error' : strtolower($type);
        $base_qname = QNameManagerService::model()->get_base_qname($type);
        $zone = $this->select_redis($base_qname);
        $key = 'queue_'.$base_qname;
        $len = 0;
        if($zone) {
            $len = $zone['redis']->lLen($key);
        } else {
            EdjLog::error("can not find zone, queue name: " . $base_qname);
        }
        return $len;
    }
}

 

相關文章