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