基於redis隊列優化投票系統性能

 1、投票要求
(1)投票系統規則
每個ip每天限投10票,10票可同一選項,也可不同選項
(2)訪問量要求日均1000萬
按照80/20法則,系統的每秒要達到併發量:1000萬*80%/(24*3600*20%)≈463。
(3)計數要準確,支持高併發,每個用戶的投票記錄也要保存

2、實現
投票數據先放在redis隊列中,再通過定時任務把投票日誌保存到mysql,投票計數器保存在txt文件中。

2.1、用戶點擊投票操作
在用戶進行投票點擊時,投票傳輸到服務器端,服務器端進行3個步驟處理:
(1)用戶答案按以ip爲key保存到redis中,避免用戶重複提交
(2)用戶答案保存到redis的lPush的隊列中,等待後臺程序讀取入庫保存
(3)同時標記當前ip的用戶投票未入庫,前臺通過這個標記判斷,讓當前ip用戶實時看到投票數據更新

<?php
include_once "VoteUtils.php";
session_start();
$vote = new VoteUtils();
$vote->openRedis();
// 1、獲取客戶端IP
$ip = $vote->getClientIp();
// 2、獲取用戶投票選項,
$ids = $vote->getUserOptions();

// 3、判斷當前ip是否已有投票記錄
$vote_options = $vote->getVoteOption($ip);
$vote_count = count($vote_options);

// 4.1如果已有投票記錄、提示重複投票
if($vote_count > 0){ // 重複投票
    $result = ['status'=>2, 'msg'=>'success','vote_count'=>$vote_count,'vote_options'=>$ids, 'vote_options2'=>$vote_options];
}
else{
    // 4.2如果沒有記錄,保存數據
    if(count($ids) == 10){
        // 投票選項保存到redis,按key='ip_vote_city_data_' . $ip保存,避免用戶重複投票
        $vote->setVoteOption($ip, $ids);
        // 添加redis投票隊列,按lPush隊列key=vote_topic保存
        $vote->addQueues($ip, $ids);
        // 標明當前ip的投票選項未入庫標誌,讓當前ip用戶實時看到投票數據更新
        $vote->setVoteCompute($ip);
        $result = ['status'=>0, 'msg'=>'success','vote_count'=>count($ids),'vote_options'=>$ids];
    }
    else{  // 當前投票選項未足10票
        $result = ['status'=>1, 'msg'=>'error','vote_count'=>count($ids),'vote_options'=>$ids];
    }
}
$vote->closeRedis();
echo json_encode($result);
?>

2.2、服務器端後臺任務處理
後臺通過守護進程一直讀取隊列中的投票數據,並把數據持久化保存:
(1)通過redis的rPop讀取隊列中的投票記錄,把投票日誌批量保存到mysql。
(2)投票總數保存到文件中。
(3)清除用戶投票操作(3)中的未入庫狀態

class VoteController extends Controller
{
    private function getRedis(){
        $redis = new \Redis();
        $redis->connect('127.0.0.1', 6379);
        return $redis;
    }
    public function actionRedisVote(){
        $redis = $this->getRedis();
        $data_count = 0;
        $datas = array();
        $updateStatus = 0;
        while(true){
            $data = $redis->rPop('vote_topic');
            $updateStatus++;
            if($updateStatus % 500 == 0){
                $updateStatus = 0;
            }
            // 如果隊列中沒有新數據,先把變量中已有數據保存
            if(empty($data) == true ){
                // 保存數據
                if( count($datas) > 0){
                    $this->saveLog($datas, $redis);
                    $data_count = 0;
                    unset($datas);
                    $datas = array();
                }
                sleep(10);
            }
            else{
                $data_count++;
                $datas[] = $data;
                // 當達到500條投票記錄,批量保存
                if($data_count >= 500){
                    // 保存數據
                    $this->saveLog($datas, $redis);
                    $data_count = 0;
                    unset($datas);
                    $datas = array();
                }
            }
        }
    }

    private function getIp($ip){
        $ipinfo = IpTool::getInfo($ip);
        $region = empty($ipinfo['region']) == false ? $ipinfo['region'] : '';
        $country = empty($region) == false ? explode('|', $region)[0] : '';
        return $country;
    }

    private function saveLog($datas, $redis){
        $rows = [];
        $dataCount = [];
        foreach($datas as $data){
            $data = json_decode($data, true);
            if(empty($data) == false){
                $client_ip = $data[0];
                $country = $this->getIp($client_ip);
                $city_ids = $data[1];
                $create_time = $data[2];
                $user_agent = $data[3];
                $create_date = date('Y-m-d H:i:s', $create_time);
                $create_day = date('md', $create_time);
                $create_hour = date('mdH', $create_time);
                $create_minute = date('mdHi', $create_time);
                foreach($city_ids as $city_id){
                    $dataCount[$city_id] = isset($dataCount[$city_id]) == true ? $dataCount[$city_id] + 1 : 1;
                    $client_browser = mb_strlen($user_agent, 'UTF-8') < 200 ? $user_agent : mb_substr($user_agent, 0, 200, 'UTF-8');
                    $rows[] = [
                        $city_id,
                        $client_ip,
                        $country,
                        $client_browser,
                        $create_date,
                        $create_day,
                        $create_hour,
                        $create_minute,
                    ];
                }
                $vote_compute_key = 'ip_vote_compute_key_'.$client_ip;
                // 刪除當前ip的投票選項未入庫標記
                $redis->del($vote_compute_key);
            }
        }
        VoteLog::getDb()->createCommand()->batchInsert(
            'vote_log',
            ['city_id', 'client_ip', 'country', 'client_browser','create_date','create_day','create_hour', 'create_minute'],
            $rows
        )->execute();
        $this->updateDbCount($dataCount);
    }

    /**
     * 更新本地記錄數,把每個選項的記錄數保存到文件中
     */
    public function updateDbCount($dataCount){
        $data = array();
        $filePath = Yii::$app->basePath.'/count/';
        $filenames = $this->getPathFiles($filePath);
        if(count($filenames) == 0){
            for($i = 1; $i <= 15; $i++){
                $file = $filePath . $i;
                if(is_file($file) == false){
                    file_put_contents($file, 0);
                }
                $filenames[] = strval($i);
            }
        }
        if(count($filenames) > 0){
            $total = 0;
            foreach($filenames as $name){
                $id = $name;
                $idInt = intval($id);
                if($idInt > 0 && $idInt < 20){
                    $file = $filePath . $name;
                    $count = intval(file_get_contents($file));
                    // 新增加的投票
                    if(isset($dataCount[$id]) == true){
                        $count = $count + $dataCount[$id];
                        file_put_contents($file, $count);
                    }
                    $total = $total + $count;
                    $data[$id] = $count;
                    $is_success = VoteCity::getDb()->createCommand(" update vote_city set vote_count = {$count} where id = {$idInt} ")->execute();
                }
            }
            $data['100'] = $total;
            $data['time'] = time();
            file_put_contents($filePath.'100', json_encode($data));
            chmod($filePath.'100', 0755);
        }

    }
}

2.3、用戶訪問投票頁面
投票頁面內容純html化,放在通過cdn降低服務器壓力,另外像當前票數這些數據是實時變動的,需要通過動態接口獲取:
(1)讀取投票總數文件,如果用戶存在答案未入庫狀態的,把當前用戶投票記錄也要加進總數中,這樣用戶投票記錄雖然還在隊列中,但是也能讓用戶實時看到投票數量變化。

<?php
include_once 'VoteUtils.php';
session_start();
$vote = new VoteUtils();
$vote->openRedis();
$ip = $vote->getClientIp();
$voteOptions = $vote->getVoteOption($ip);
// 基於文件緩存
$dataCount = $vote->getFileVoteCount();
// 檢查數據是否已經被統計
$isVoteCompute = $vote->isVoteCompute($ip);
if(empty($voteOptions) == false && $isVoteCompute == false){
    foreach($voteOptions as $option){
        if(isset($dataCount[$option]) == true){
            $dataCount[$option] = $dataCount[$option] + 1;
        }
    }
}
$result = [
    'vote_options'=>$voteOptions,
    'data_count'=>$dataCount,
    'ip'=>$ip,
];
$vote->closeRedis();
echo json_encode($result);

VoteUtils.php

<?php
class VoteUtils
{
    public $redis;
    public $expire_time = 86400; // 24小時
    function __construct() {
    }
    public function openRedis(){
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1',6379);
    }
    public function closeRedis(){
        $this->redis->close();
    }
    /**
     * 基於文件緩存獲取所有當前城市投票總數
     * @return array
     */
    public function getFileVoteCount(){
        $data = array();
        $filePath = __DIR__.'/../runtime/count/';
        $totalFile = $filePath . '100';
        if(file_exists($totalFile) == true){
            $totalData = file_get_contents($totalFile);
        }
        else{
            $totalData='["1":0,"2":0,"3":0,"4":0,"5":0,"6":0,"7":0,"8":0,"9":0,"10":0,"11":0,"12":0,"13":0,"14":0,"15":0]';
        }
        $totalData = json_decode($totalData, true);
        if(empty($totalData) == false && (time() - $totalData['time']) < 10){
            return $totalData;
        }
        else{
            $filenames = $this->getPathFiles($filePath);
            if(count($filenames) > 0){
                $total = 0;
                foreach($filenames as $name){
                    $id = $name;
                    $file = $filePath . $name;
                    $count = intval(file_get_contents($file));
                    $total = $total + $count;
                    $data[$id] = $count;
                }
                $data['100'] = $total;
                $data['time'] = time();
                file_put_contents($totalFile,json_encode($data));
                chmod($totalFile, 0755);
            }
            return $data;
        }
    }
    private function getPathFiles($logdir){
        $filenames = scandir($logdir);
        $files = array();
        foreach($filenames as $name){
            $file = $logdir.'/'.$name;
            if(is_file($file) == true){
                $files[] = $name;
            }
        }
        return $files;
    }


    /**
     * 獲取客戶端IP
     * @return string 返回ip地址,如127.0.0.1
     */
    public function getClientIp()
    {
        $onlineip = $_SERVER['REMOTE_ADDR'];
        return $onlineip;
    }

    /**
     * 獲取用戶投票選項
     * 一次投5票
     */
    public function getUserOptions(){
        $ids = $_GET['ids'];
        if(empty($ids) == true || is_array($ids) == false){
            echo json_encode(['status'=>0, 'msg'=>'Data can not be null!', "idstemp"=>$ids]);  // 數據不能爲空!
            exit();
        }
        else{
            $idstemp = [];
            foreach($ids as $idtemp){
                $id = intval($idtemp);
                if($id > 0 && $id <=15){
                    $idstemp[] = $idtemp;
                }
            }
            return $idstemp;
        }
    }
    /**
     * 是否爲已投票選項
     */
    public function isVoteOption($ip, $id){
        $vote_option_key = 'ip_vote_city_data_' . $ip;
        $vote_option = $this->getVoteOption($ip);
        if(empty($vote_option) == true){
            $this->redis->setex($vote_option_key, $this->expire_time, json_encode([$id]));
        }
        else if(in_array($ip, $vote_option) == false){
            $vote_option[] = $id;
            $this->redis->setex($vote_option_key, $this->expire_time, json_encode($vote_option));
        }
    }
    public function setVoteOption($ip, $vote_options){
        $vote_option_key = 'ip_vote_city_data_' . $ip;
        $this->redis->setex($vote_option_key, $this->expire_time, json_encode($vote_options));
    }

    /**
     * @param $ip
     * @return array|mixed
     */
    public function getVoteOption($ip){
        $vote_option_key = 'ip_vote_city_data_' . $ip;
        $vote_option = $this->redis->get($vote_option_key);
        $vote_option = json_decode($vote_option, true);
        if(empty($vote_option) == true){
            $vote_option = [];
        }
        return $vote_option;
    }

    /**
     * 添加到隊列
     * 一次投5票
    */
    public function addQueues($ip,$ids){
        $user_agent = $_SERVER['HTTP_USER_AGENT'];
        $this->redis->lPush('vote_topic', json_encode([$ip, $ids, time(),$user_agent]));
    }

    /**
     * 當前ip的投票記錄是否已經入庫
     * @param $ip
     */
    public function isVoteCompute($ip){
        $vote_compute_key = 'ip_vote_compute_key_'.$ip;
        $result = $this->redis->get($vote_compute_key);
        if(empty($result) == false){ // 如果緩存中存在數據,則爲未入庫
            return false;
        }
        else{
            return true;
        }
    }

    /**
     * 當前ip的投票記錄是否已經入庫
     * @param $ip
     */
    public function setVoteCompute($ip){
        $vote_compute_key = 'ip_vote_compute_key_'.$ip;
        $this->redis->setex($vote_compute_key, $this->expire_time, 'y');
    }
}

jmeter簡單本機壓測一下,tps最大值可達到1000以上了

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章