基于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以上了

 

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