排查redis占用内存达90%以上

帮别人排查一个问题,项目还没上线但redis占用内存很高。思路如下:

1、登陆redis控制台,首先用 keys * 获取所有的key

> keys *
x:x:a
x:x:b
x:x:c

发现key也就十来个,用 TYPE x:x:a 发现a是一个list数据类型

用lrange命令查看list中指定索引的值
用法: lrange key start end #获取列表中从 start 到 end 的值。
start 从0开始计,start、end也可为负数,倒数第一元素的位置为-1,倒数第二为-2,以此类推
lrange key 0 -1 # 第一个元素到倒数第一个元素(全部元素)

> lrange 0 1

发现值为json格式的数据

# 统计list长度,用 LLEN 命令可以返回列表的长度

> LLEN x:x:a

统计出 x:x:a 这个key有十几万多个索引

写了个脚本统计了下所有的key长度约370万,这样推测是数据量太大导致占用内存非常高。
经询问项目里有个模块用redis,个人推测要么用来做消息队列但没被消费,要么是做缓存,但是旧数据没有清理导致堆积越来越大。

PS:主要介绍了几个redis操作命令。

redis实现分布式锁

以下为测试分布式锁的实现方法。

场景:
比如50个人向B转账,B的余额当前为0,转50次,每次1元,理论来说B的余额应该为50元。
当在高并发的情况下,那么最终结果就不一定是50了。

现在有张表名为balance,3个字段id(自增),name(姓名),balance(余额)

现在用php模拟一下简单的转账操作。

<?php header("Content-type =>  text/html; charset=utf-8");
// session start
define("SESSION_ON", true);

// define project's config
define("CONFIG", '/conf/web.php');

// debug switch
define("DEBUG", true);

// include framework entrance file
include('../common.php');

use pt\framework\debug\console as debug;
use pt\framework\template as template;
use pt\tool\page as page;
use pt\framework\db as db;

include(COMMON_PATH.'web_func.php');

$db = db::init();

// 查询B用户余额
$balance = "select balance from balance where id = 1;";
$balance = $db -> prepare($balance) -> execute();
$balance = $balance[0]['balance'];

// B余额加1
$sql = "update balance set balance = :balance + 1 where id = 1;";
$rs = $db -> prepare($sql) -> execute(array(':balance' => $balance));

使用jmeter并发50测试且50个请求全部成功。去查看数据库。

可以看到数据库中balance字段值为44,不是理论的50元。
原因就是在高并发情况下出现了问题。
比如有2个请求同时从数据库中获得了一样的余额,比如1,第一个请求先进行了update操作但是还没结束,
第二个请求发现第一个请求正在更新数据库,第一个修改请求会将balance表的id=1这条数据处于行锁状态(innodb引擎+索引才能实现行锁)。
这样第二个请求就处于等待状态,等待第一个请求update完成并释放行锁后,再更新id=1的数据,问题来了。
第一个请求已经将余额修改为2,第二个请求也把余额改为2了。(其实并没有真正更新,发现一样就不改了,在sqlyog中会提示(0 row(s) affected),影响了0行)
(完整的转账应该至少3条记录要通过事务更新,from减,to加,插入资金流水表)

现在为了解决这个问题,引入redis,通过setnx这个方法实现,具体解释请百度。
当redis中存在to(我这里把to的用户id做为key)这个key时,则停止转账。

<?php header("Content-type =>  text/html; charset=utf-8");
// session start
define("SESSION_ON", true);

// define project's config
define("CONFIG", '/conf/web.php');

// debug switch
define("DEBUG", true);

// include framework entrance file
include('../common.php');

use pt\framework\debug\console as debug;
use pt\framework\template as template;
use pt\tool\page as page;
use pt\framework\db as db;

include(COMMON_PATH.'web_func.php');

$db = db::init();

$to = 1;  // 用户id做为key

try
{
    $redis = new Redis();
    $redis -> connect('127.0.0.1', 6379);
    $redis -> auth('111111');
    $connected = $redis -> ping();
}
catch(Exception $e)
{
    echo '连接失败:' . $e -> getMessage();
    exit;
}

// 获取用户余额
$balance = "select balance from balance where id = 1;";
$balance = $db -> prepare($balance) -> execute();
$balance = $balance[0]['balance'];

$expire = 60;  // key超时时间
$key = $to;
$content = 'running';
$rs = $redis -> setnx($key, $content);

if ($rs === true) // 加锁
{
    $redis -> expire($key, $expire);  // 设置key超时时间,防止某个机器加锁以后挂掉造成key死锁(一直存在)。
    $sql = "update balance set balance = :balance + 1 where id = 1;";
    $rs = $db -> prepare($sql) -> execute(array(':balance' => $balance));

    if ($rs === false)
    {
        echo 'add balance failed';
    }
    else
    {
        echo 'add balance success';
    }

    $redis -> delete($to); // 删除锁
}
else
{
    //echo 'lock false , lock by others' . PHP_EOL;
    //echo 'after ' . $redis -> ttl($key) . 's release';
    header('HTTP/1.1 403 Forbidden');  // 这里输出403错误方便jmeter中查看。实际中可能返回个友好的错误提示,比如请重试。
}

再用jmeter并发50测试。

发现有34个请求成功,16个失败,总共50个请求。
再去看数据库中balance字段。余额为34。


这样就通过redis实现了分布式锁,当有多个服务器做负载均衡时,每次转账都会先请求redis,查这个用户是否存在key,防止出现同一时间转账导致结果错误。
如有错误,请指正。

PS:
另外我觉得也可以通过消息队列实现,排队转账。

redis实现目录层级结构存储

想实现层级目录存储key,类似/project/func/username 这种方式存储。
在redis中set key的时候把目录层级用冒号连接,如
/a/b/c/username的key值为simon

127.0.0.1:6379> set a:b:c:username simon
OK
127.0.0.1:6379> get a:b:c:username
"simon"

安装redis的php扩展模块

从 https://github.com/nicolasff/phpredis/releases 可以找到已发布的版本。
这里找到的版本为:phpredis-2.2.5
安装redis的php扩展模块:
[root@localhost ~]# cd phpredis-2.2.5
[root@localhost phpredis-2.2.5]# phpize
Configuring for:
PHP Api Version:         20090626
Zend Module Api No:      20090626
Zend Extension Api No:   220090626
[root@localhost phpredis-2.2.5]# ./configure
[root@localhost phpredis-2.2.5]# make
[root@localhost phpredis-2.2.5]# make install
Installing shared extensions:     /usr/lib64/php/modules/
修改php.ini,添加载入模块:
extension_dir=/usr/lib64/php/modules/
extension=redis.so
重启apache。

一个基于php的web管理redis程序-phpRedisAdmin。
下载地址:https://github.com/ErikDubbelboer/phpRedisAdmin/releases
下载phpRedisAdmin.zip这个文件包。
传到服务器中。通过url访问。

CentOS6.5下配置Redis-2.8.9主从复制

redis复制原理:
当设置好slave服务器后,slave会建立和master的连接,然后发送sync命令。无论是第一次同步建立的连接还是连接断开后的重新连 接,master都会启动一个后台进程,将数据库快照保存到文件中,同时master主进程会开始收集新的写命令并缓存起来。后台进程完成写文件 后,master就发送文件给slave,slave将文件保存到磁盘上,然后加载到内存恢复数据库快照到slave上。接着master就会把缓存的命 令转发给slave。而且后续master收到的写命令都会通过开始建立的连接发送给slave。从master到slave的同步数据的命令和从 client发送的命令使用相同的协议格式。当master和slave的连接断开时slave可以自动重新建立连接。如果master同时收到多个 slave发来的同步连接命令,只会使用启动一个进程来写数据库镜像,然后发送给所有slave。

环境信息:
selinux、iptables 关闭
master centos6.5_x65 ip:192.168.0.76
slave    centos6.5_x65 ip:192.168.0.75

两台机器安装redis,过程略。
修改master主配置文件:
[root@localhost ~]# vi /usr/local/redis/conf/redis.conf
daemonize yes #守护进程
保存退出。
启动master:
[root@localhost ~]# /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis.conf

修改slave主配置文件:
daemonize yes #守护进程
slaveof 192.168.0.76 6379 #定义master的ip及端口
[root@localhost ~]# /usr/local/redis/bin/redis-server /usr/local/redis/conf/redis.conf
[root@localhost ~]# lsof -i:6379 #看到服务已启动并且与master建立连接
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
redis-ser 4713 root 4u IPv6 21909 0t0 TCP *:6379 (LISTEN)
redis-ser 4713 root 5u IPv4 21911 0t0 TCP *:6379 (LISTEN)
redis-ser 4713 root 6u IPv4 21915 0t0 TCP 192.168.0.75:44736->192.168.0.76:6379 (ESTABLISHED)

测试:
在master执行:
[root@localhost ~]# /usr/local/redis/bin/redis-cli
127.0.0.1:6379> set test1 aaa #添加test1
OK
127.0.0.1:6379> get test1
“aaa”
127.0.0.1:6379> set test2 bbb ##添加test2
OK
127.0.0.1:6379> get test2
“bbb”
127.0.0.1:6379>

在slave执行:
[root@localhost ~]# /usr/local/redis/bin/redis-cli
127.0.0.1:6379> get test1 #获取数据
“aaa”
127.0.0.1:6379> get test2
“bbb”
127.0.0.1:6379>

默认slave是只能读取,不能添加数据的。在slave添加数据时会报错:
127.0.0.1:6379> set test3 ccc
(error) READONLY You can’t write against a read only slave.

要启用slave的写操作,需要修改配置文件,将slave-read-only改为no
slave-read-only no #yes改为no

redis的主从复制与mysql比较来说配置还是很简单的。