记一次Redis在投票活动中的优化实践

环境

macOS 10.14 + Redis + php7

为了验证 Redis 扩展是否安装好,可以运行下面的脚本

<?php
$redisObject=new Redis();
if(!$redisObject->connect('127.0.0.1',6379)){
    die("Can't connect to Redis Server");
}else{
    echo 'Redis is ready!';
}

需求和业务流程

投票活动,大量用户瞬间访问投票通道,投完之后要马上看到实时投票情况。

对于高并发环境下,直接进行 MySQL 写入或读取,会极大消耗服务器资源。

利用 Redis 缓存用户的操作(热数据),周期性保存到 MySQL 中(冷数据),然后把冷数据从 Redis 删除,周而复始

思路

用户投票直接写入 Redis:vid:{uid, ip, name}
脚本检测 CPU 负载,当负载小于阈值的时候,将热数据取出来写入 MySQL

投票

投票页面 index.html

三个按钮,使用Ajax调用投票接口

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
  <title>Document</title>
</head>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<body>
  <p><span id="uid1">0</span> <input type="button" value="用户1" onclick="vote(1);" /></p>
  <p><span id="uid2">0</span> <input type="button" value="用户2" onclick="vote(2);" /></p>
  <p><span id="uid3">0</span> <input type="button" value="用户3" onclick="vote(3);" /></p>
</body>
<script>
  getVotes(1);
  getVotes(2);
  getVotes(3);
  // 获取票数
  function getVotes(i){
    $.get('votes.php?uid='+i,function(rs){
      var span = '#uid'+i;
      $(span).html(rs);
    });
  }

  // 投票
  function vote(i){
    $.get('vote.php?uid='+i,function(rs){
      var span = '#uid'+i;
      $(span).html(rs);
    });
  }
</script>
</html>

投票接口 vote.php

逻辑:

  • 连接 Redis 服务器
  • 保存投票人uid,用uid作为键值存票数,返回给前端用
  • 使用 voteid_sum 作为键存储总票数。
  • 记录 uidiptime等数据 vid:{uid, ip, name}
<?php 
$redisObj = new Redis();
if(!$redisObj->connect('127.0.0.1', 6379)){
    die("Can't connect to Redis Server");
}

$uid = intval($_GET['uid']);
//$uid = mt_rand(1,3);//随机指定投票人员,方便进行压力测试
echo $redisObj->incr($uid);

// redis操作, 记录当前票的情况 vid:{uid, ip, name}
$voteid = $redisObj->incr('voteid_sum'); // incr是将 key 中储存的数字值增一
$redisObj->set('vote:'.$voteid.':uid', $uid);
$redisObj->set('vote:'.$voteid.':ip', $_SERVER['REMOTE_ADDR']);
$redisObj->set('vote:'.$voteid.':time', time());

// 下面是获票接口 votes.php
<?php 
$redisObj = new Redis();
if(!$redisObj->connect('127.0.0.1', 6379)){
    die("Can't connect to Redis Server");
}
$uid = intval($_GET['uid']);
echo $redisObj->get($uid);

冷热数据交换 swap.php

连接数据库和Redis

五秒钟循环一次,检测 CPU 负载,小于 70 的时候:从redis取出键值对,拼接 SQL 语句,执行写入操作。每次写完,记录 last值表示最后插入的位置,以便下次从这个之后插入

<?php
//连接数据库
$pdo = new PDO('mysql:host=127.0.0.1;dbname=redis_vote','root','root777');
$pdo->query('set names utf8');

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

// 循环
while(true){
    $cpu_used_status = get_used_status()['cpu_usage'];
    if($cpu_used_status < 70){
        $vid = $redis->get('voteid_sum');//自增长的主键
        $last = $redis->get('last');//最近一次插入mysql的投票主键
        //如果没有插入数据库,刚开始的肯定为true
        if(!$last){
            $last = 0;//设置为0
        }
        // 遍历到末尾,库都写完
        if($vid == $last){
            echo "Completed, waitting for data\n";//输出等待
        }else{
            // 写库
            $sql = 'insert into vote(vid, uid, ip, time) values';
            for($i = $vid; $i>$last; $i--){
                $k1 = 'vote:'.$i.':uid';
                  $k2 = 'vote:'.$i.':ip';
                $k3 = 'vote:'.$i.':time';
                $row = $redis->mget([$k1, $k2, $k3]); // 返回所有(一个或多个)给定 key 的值
                $sql .= "($i, $row[0], '$row[1]', $row[2]),"; // 拼接SQL
                $redis->delete($k1, $k2, $k3);
            }
            $sql = substr($sql, 0, -1);
            $pdo->exec($sql);
            $redis->set('last', $vid);//设置插入的主键位置
            echo "Data writing\n";
        }
    }
    sleep(5);// 时延五秒
}

function get_used_status(){
    $fp = popen('top -b -n 2 | grep -E "^(Cpu|Mem|Tasks)"',"r");//获取某一时刻系统cpu和内存使用情况
    $rs = "";
    while(!feof($fp)){
       $rs .= fread($fp,1024);
    }
    pclose($fp);
    $sys_info = explode("\n", $rs);
    $tast_info = explode(",", $sys_info[3]);//进程 数组
    $cpu_info = explode(",", $sys_info[4]);  //CPU占有量  数组
    $mem_info = explode(",", $sys_info[5]); //内存占有量 数组
    //正在运行的进程数
    $tast_running = trim(trim($tast_info[1], 'running'));
    //CPU占有量
    $cpu_usage = trim(trim($cpu_info[0], 'Cpu(s): '),'%us');  //百分比

    //内存占有量
    $mem_total = trim(trim($mem_info[0], 'Mem: '),'k total');
    $mem_used = trim($mem_info[1], 'k used');
    $mem_usage = round(100*intval($mem_used)/intval($mem_total),2);  //百分比
     
    /*硬盘使用率 begin*/
    $fp = popen('df -lh | grep -E "^(/)"',"r");
    $rs = fread($fp, 1024);
    pclose($fp);
    $rs = preg_replace("/\s{2,}/", ' ', $rs);  //把多个空格换成 “_”
    $hd = explode(" ", $rs);
    $hd_avail = trim($hd[3], 'G'); //磁盘可用空间大小 单位G
    $hd_usage = trim($hd[4], '%'); //挂载点 百分比
    //print_r($hd);
    /*硬盘使用率 end*/ 

    //检测时间
    $fp = popen("date +\"%Y-%m-%d %H:%M\"","r");
    $rs = fread($fp, 1024);
    pclose($fp);
    $detection_time = trim($rs);

    /*获取IP地址  begin*/
    /*
    $fp = popen('ifconfig eth0 | grep -E "(inet addr)"','r');
    $rs = fread($fp,1024);
    pclose($fp);
    $rs = preg_replace("/\s{2,}/",' ',trim($rs));  //把多个空格换成 “_”
    $rs = explode(" ",$rs);
    $ip = trim($rs[1],'addr:');
    */
    /*获取IP地址 end*/
    /*
    $file_name = "/tmp/data.txt"; // 绝对路径: homedata.dat
    $file_pointer = fopen($file_name, "a+"); // "w"是一种模式,详见后面
    fwrite($file_pointer,$ip); // 先把文件剪切为0字节大小, 然后写入
    fclose($file_pointer); // 结束
    */

    return  array(
        'cpu_usage' => $cpu_usage,
        'mem_usage' => $mem_usage,
        'hd_avail' => $hd_avail,
        'hd_usage' => $hd_usage,
        'tast_running' => $tast_running,
        'detection_time' => $detection_time
    );
}

数据表

表结构如下

+-------+------------------+------+-----+---------+----------------+
| Field | Type             | Null | Key | Default | Extra          |
+-------+------------------+------+-----+---------+----------------+
| vid   | int(11)          | NO   | PRI | NULL    | auto_increment |
| uid   | int(11)          | YES  |     | NULL    |                |
| ip    | char(20)         | YES  |     | NULL    |                |
| time  | int(10) unsigned | YES  |     | NULL    |                |
+-------+------------------+------+-----+---------+----------------+

脚本:

CREATE TABLE votes(
    vid int(11) PRIMARY KEY AUTO_INCREMENT,
    uid int(11),
    ip char(20),
    time int(10) unsigned 
);

本文链接:https://ariser.cn/index.php/archives/422/
本站文章采用 知识共享署名4.0 国际许可协议进行许可,请在转载时注明出处及本声明!