>  기사  >  백엔드 개발  >  PHP Redis의 메모리 사용량을 줄이는 방법(그림 및 텍스트)

PHP Redis의 메모리 사용량을 줄이는 방법(그림 및 텍스트)

黄舟
黄舟원래의
2017-03-24 09:34:522589검색

본 글에서는 PHP Redis의 메모리 사용량을 줄이는 방법을 주로 소개합니다. 매우 좋은 참조 값을 가지고 있습니다. 아래 에디터로 살펴보겠습니다

1. Redis 메모리 사용량 절감의 장점

1. 스냅샷 생성 및 로딩 시간 단축에 도움이 됩니다

2. AOF 파일 로드 및 재작성 효율성 향상

3. 서버에서 동기화하는 데 필요한 시간 단축

4. 추가 하드웨어 추가 필요 없음 이를 통해 Redis는 더 많은 데이터를 저장할 수 있습니다.

2. 짧은 구조

Redis는 목록, 세트, ​​해시 및 정렬된 세트에 대한 구성 옵션 세트를 제공합니다. 이러한 옵션을 사용하면 Redis가 더 짧은 구조를 더 경제적인 방법.

2.1, zip목록압축 목록(목록, 해시, 속편 포함)

일반적으로 사용되는 저장 방법

list, hash, ordered set의 길이가 짧거나 용량이 작은 경우 redis에서는 ziplist라는 컴팩트한 저장 방법을 사용합니다. 이러한 구조를 저장하는 데 사용됩니다.

Ziplist는 목록, 해시 및 순서가 지정된 집합이라는 세 가지 유형의 객체를 구조화하지 않은 방식으로 표현한 것입니다. 이러한 직렬화된 데이터는 매번 읽어야 합니다. 기록될 때마다 검색되고 인코딩됩니다.

양방향 목록과 압축 목록의 차이점:

목록의 압축을 풀고 다른 데이터보다 더 많은 메모리를 절약하기 위해 구조에 대해서는 목록 구조를 예로 들어 심층적인 연구를 수행합니다.

일반적인 양방향 목록

일반적인 양방향 목록에서는 각 값이 노드로 표시됩니다. 각 노드에는 연결 목록의 이전 노드와 다음 노드에 대한 포인터와 노드에 포함된 string 값에 대한 포인터가 있습니다.

각 노드에 포함된 문자열 값은 세 부분으로 나누어 저장됩니다. 문자열 길이, 문자열 값에 남아 있는 사용 가능한 바이트 수 및 Null 종료 문자열 자체를 포함합니다.

예:

특정 노드가 'abc' 문자열을 저장하는 경우 보수적으로 21바이트의 추가 오버헤드가 필요할 것으로 추정됩니다(포인터 3개 + int+null 문자 2개: 3*4). +2*4+1=21)

예제를 보면 3바이트 문자열을 저장하려면 최소 21바이트의 추가 오버헤드가 필요하다는 것을 알 수 있습니다.

ziplist

ziplist는 각각 두 개의 길이와 하나의 문자열을 포함하는 일련의 노드입니다. 첫 번째 길이는 이전 노드의 길이를 기록합니다(압축된 목록을 뒤에서 앞으로 이동하는 데 사용됨). 두 번째 길이는 저장된 문자열의 현재 지점의 길이입니다.

예:

은 'abc'라는 문자열을 저장합니다. 두 길이 모두 1바이트로 저장할 수 있으므로 추가 오버헤드는 2바이트입니다(두 길이는 1 +1=2입니다)

결론:

목록을 압축하면 추가 포인터 및 메타데이터 저장을 방지하여 추가 오버헤드가 줄어듭니다.

구성:

#list
list-max-ziplist-entries 512 #表示允许包含的最大元素数量
list-max-ziplist-value 64  #表示压缩节点允许存储的最大体积
#hash         #当超过任一限制后,将不会使用ziplist方式进行存储
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
#zset
zset-max-ziplist-entries 128
zset-max-ziplist-value 64

테스트 목록:

1. test.php 파일 생성

#test.php
<?php
$redis=new Redis();
$redis->connect(&#39;192.168.95.11&#39;,&#39;6379&#39;);
for ($i=0; $i<512 ; $i++) 
{ 
  $redis->lpush(&#39;test-list&#39;,$i.&#39;-test-list&#39;); #往test-list推入512条数据
}
?>

현재 테스트 목록에는 512개의 데이터가 포함되어 있으며 이는 구성 파일

의 한도를 초과하지 않습니다. 2. 테스트에 푸시합니다. -list 다시 데이터 입력

이 때 test-list에는 구성 파일에 제한되어 있는 512개보다 많은 513개의 데이터가 포함되어 있습니다. 🎜>Index는 ziplist 저장 방식을 포기합니다. 원래의 linkedlist 저장 방식인

해싱은 주문된 세트와 동일합니다.

2.2, intsetintegerset(set)

전제 조건, set에 포함된 모든 멤버는 다음과 같이 구문 분석될 수 있습니다. 십진수.

컬렉션을 순서대로

배열 저장하면 메모리 소비를 줄일 수 있을 뿐만 아니라 컬렉션 작업의 실행 속도도 향상시킬 수 있습니다.

구성:

set-max-intset-entries  512   #限制集合中member个数,超出则不采取intset存储

테스트:

test.php 파일 생성

#test.php
<?php
$redis=new Redis();
$redis->connect(&#39;192.168.95.11&#39;,&#39;6379&#39;);
for ($i=0; $i<512 ; $i++) 
{ 
  $redis->sadd(&#39;test-set&#39;,$i);  #给集合test-set插入512个member
}
?>

2.3、性能问题

不管列表、散列、有序集合、集合,当超出限制的条件后,就会转换为更为典型的底层结构类型。因为随着紧凑结构的体积不断变大,操作这些结构的速度将会变得越来越慢。

测试:

#将采用list进行代表性测试

测试思路:

1、在默认配置下往test-list推入50000条数据,查看所需时间;接着在使用rpoplpush将test-list数据全部推入到新列表list-new中,查看所需时间

2、修改配置,list-max-ziplist-entries 100000,再执行上面的同样操作

3、对比时间,得出结论

默认配置下测试:

1、插入数据,查看时间

#test1.php
<?php
header("content-type: text/html;charset=utf8;");
$redis=new Redis();
$redis->connect(&#39;192.168.95.11&#39;,&#39;6379&#39;);
$start=time();
for ($i=0; $i<50000 ; $i++) 
{ 
  $redis->lpush(&#39;test-list&#39;,$i.&#39;-aaaassssssddddddkkk&#39;);
}
$end=time();
echo "插入耗时为:".($end-$start).&#39;s&#39;;
?>

结果耗时4秒

2、执行相应命令,查看耗时

#test2.php
<?php
header("content-type: text/html;charset=utf8;");
$redis=new Redis();
$redis->connect(&#39;192.168.95.11&#39;,&#39;6379&#39;);
$start=time();
$num=0;
while($redis->rpoplpush(&#39;test-list&#39;,&#39;test-new&#39;))
{
  $num+=1;
}
echo &#39;执行次数为:&#39;.$num."<br/>";
$end=time();
echo "耗时为:".($end-$start).&#39;s&#39;;
?>

更改配置文件下测试  

1、先修改配置文件

list-max-ziplist-entries 100000  #将这个值修改大一点,可以更好的凸显对性能的影响

list-max-ziplist-value 64    #此值可不做修改

2、插入数据

执行test1.php

结果为:耗时12s

3、执行相应命令,查看耗时

执行test2.php

结果为:执行次数:50000,耗时12s

结论:

在本机中执行测试50000条数据就相差8s,若在高并发下,长压缩列表和大整数集合将起不到任何的优化,反而使得性能降低。

3、片结构

分片的本质就是基于简单的规则将数据划分为更小的部分,然后根据数据所属的部分来决定将数据发送到哪个位置上。很多数据库使用这种技术来扩展存储空间,并提高自己所能处理的负载量。

结合前面讲到的,我们不难发现分片结构对于redis的重要意义。因此我们需要在配置文件中关于ziplist以及intset的相关配置做出适当的调整。

3.1、分片式散列

#ShardHash.class.php

<?php
class ShardHash
{
  private $redis=&#39;&#39;; #存储redis对象
  /**
  * @desc 构造函数
  * 
  * @param $host string | redis主机
  * @param $port int  | 端口
  */
  public function construct($host,$port=6379)
  {
    $this->redis=new Redis();
    $this->redis->connect($host,$port);
  } 
  /**
  * @desc 计算某key的分片ID
  *
  * @param $base string | 基础散列
  * @param $key  string | 要存储到分片散列里的键名
  * @param $total int  | 预计非数字分片总数
  * 
  * @return string | 返回分片键key
  */
  public function shardKey ($base,$key,$total)
  {
    if(is_numeric($key))
    {
      $shard_id=decbin(substr(bindec($key),0,5)); #取$key二进制高五位的十进制值
    }
    else
    {
      $shard_id=crc32($key)%$shards; #求余取模
    }
    return $base.&#39;_&#39;.$shard_id;
  }
  /**
  * @desc 分片式散列hset操作
  *
  * @param $base string | 基础散列
  * @param $key  string | 要存储到分片散列里的键名
  * @param $total int  | 预计元素总数
  * @param $value string/int | 值
  *
  * @return bool | 是否hset成功
  */
  public function shardHset($base,$key,$total,$value)
  {
    $shardKey=$this->shardKey($base,$key,$total);
    return $this->redis->hset($shardKey,$key,$value);
  }
  /**
  * @desc 分片式散列hget操作
  *
  * @param $base string | 基础散列
  * @param $key  string | 要存储到分片散列里的键名
  * @param $total int  | 预计元素总数
  *
  * @return string/false | 成功返回value
  */
  public function shardHget($base,$key,$total)
  {
    $shardKey=$this->shardKey($base,$key,$total);
    return $this->redis->hget($shardKey,$key);
  }
} 
$obj=new ShardHash(&#39;192.168.95.11&#39;);
echo $obj->shardHget(&#39;hash-&#39;,&#39;key&#39;,500);
?>

散列分片主要是根据基础键以及散列包含的键计算出分片键ID,然后再与基础键拼接成一个完整的分片键。在执行hset与hget以及大部分hash命令时,都需要先将key(field)通过shardKey方法处理,得到分片键才能够进行下一步操作。

3.2、分片式集合

如何构造分片式集合才能够让它更节省内存,性能更加强大呢?主要的思路就是,将集合里面的存储的数据尽量在不改变其原有功能的情况下转换成可以被解析为十进制的数据。根据前面所讲到的,当集合中的所有成员都能够被解析为十进制数据时,将会采用intset存储方式,这不仅能够节省内存,而且还可以提高响应的性能。

例子:

假若要某个大型网站需要存储每一天的唯一用户访问量。那么就可以使用将用户的唯一标识符转化成十进制数字,再存入分片式set中。

#ShardSet.class.php

<?php
class ShardSet
{
  private $redis=&#39;&#39;; #存储redis对象
  /**
  * @desc 构造函数
  * 
  * @param $host string | redis主机
  * @param $port int  | 端口
  */
  public function construct($host,$port=6379)
  {
    $this->redis=new Redis();
    $this->redis->connect($host,$port);
  } 
  /**
  * @desc 根据基础键以及散列包含的键计算出分片键
  *
  * @param $base string | 基础散列
  * @param $key  string | 要存储到分片散列里的键名
  * @param $total int  | 预计分片总数
  * 
  * @return string | 返回分片键key
  */
  public function shardKey ($base,$member,$total=512)
  {
    $shard_id=crc32($member)%$shards; #求余取模
    return $base.&#39;_&#39;.$shard_id;
  }
  /**
  * @desc 计算唯一用户日访问量
  * 
  * @param $member int | 用户唯一标识符
  *
  * @return string | ok表示count加1 false表示用户今天已经访问过不加1
  */
  public function count($member)
  {
    $shardKey=$this->shardKey(&#39;count&#39;,$member,$total=10); #$totla调小一点用于测试
    $exists=$this->redis->sismember($shardKey,$member); 
    if(!$exists)  #判断member今天是否访问过
    {
      $this->redis->sadd($shardKey,$member);
      $this->redis->incr(&#39;count&#39;);
      $ttl1=$this->redis->ttl(&#39;count&#39;);
      if($ttl1===-1)
        $this->redis->expireat(&#39;count&#39;,strtotime(date(&#39;Y-m-d 23:59:59&#39;))); #设置过期时间
      $ttl2=$this->redis->ttl($shardKey);
      if($ttl2===-1)
      {
        $this->redis->expireat("$shardKey",strtotime(date(&#39;Y-m-d 23:59:59&#39;))); #设置过期时间
        #echo $shardKey; #测试使用
      }
      #echo $shardKey;  #测试使用
      return &#39;ok&#39;;
    }
    return &#39;false&#39;;
  }
}
$str=substr(md5(uniqid()), 0, 8);  #取出前八位
#将$str作为客户的唯一标识符
$str=hexdec($str);   #将16进制转换为十进制
$obj=new ShardSet(&#39;192.168.95.11&#39;);
$obj->count($str);
?>

4、将信息打包转换成存储字节

结合前面所讲的分片技术,采用string分片结构为大量连续的ID用户存储信息。

使用定长字符串,为每一个ID分配n个字节进行存储相应的信息。

接下来我们将采用存储用户国家、省份的例子进行讲解:

假若某个用户需要存储中国、广东省这两个信息,采用utf8字符集,那么至少需要消耗5*3=15个字节。如果网站的用户量大的话,这样的做法将会占用很多资源。接下来我们采用的方法每个用户仅仅只需要占用两个字节就可以完成存储信息。

具体思路步骤:

1、首先我们为国家、以及各国家的省份信息建立相应的'信息表格'

2、将'信息表格'建好后,也意味着每个国家,省份都有相应的索引号

3、看到这里大家应该都想到了吧,对就是使用两个索引作为用户存储的信息,不过需要注意的是我们还需要对这两个索引进行相应的处理

4、将索引当做ASCII码,将其转换为对应ASCII(0~255)所指定的字符

5、使用前面所讲的分片技术,定长分片string结构,将用户的存储位置找出来(redis中一个string不能超过512M)

6、实现信息的写入以及取出(getrange、setrange)

实现代码:

#PackBytes.class.php

<?php
#打包存储字节
#存储用户国家、省份信息
class PackBytes
{
  private $redis=&#39;&#39;; #存储redis对象
  /**
  * @desc 构造函数
  * 
  * @param $host string | redis主机
  * @param $port int  | 端口
  */
  public function construct($host,$port=6379)
  {
    $this->redis=new Redis();
    $this->redis->connect($host,$port);
  } 
  /**
  * @desc 处理并缓存国家省份数据
  * @param $countries string | 第一类数据,国家字符串
  * @param $provinces 二维array | 第二类数据,各国省份数组
  * @param $cache 1/0  | 是否使用缓存,默认0不使用
  *
  * @return array | 返回总数据
  */
  public function dealData($countries,$provinces,$cache=0)
  {
    if($cache)
    {
      $result=$this->redis->get(&#39;cache_data&#39;);
      if($result)
        return unserialize($result);
    }
    $arr=explode(&#39; &#39;,$countries);
    $areaArr[]=$arr;
    $areaArr[]=$provinces;
    $cache_data=serialize($areaArr);
    $this->redis->set(&#39;cache_data&#39;,$cache_data);
    return $areaArr;
  }
  /**
  * @desc 将具体信息按表索引转换成编码信息
  * 
  * @param $countries,$provinces,$cache| 参考dealData方法
  * @param $country string       | 具体信息--国家
  * @param $province  string      | 具体信息--省份
  *
  * @return string | 返回转换的编码信息
  */
  public function getCode($countries,$provinces,$country,$province,$cache=0)
  {
    $dataArr=$this->dealData($countries,$provinces,$cache=0);
    $result=array_search($country, $dataArr[0]); #查找数组中是否含有data1
    if($result===false)     #判断是否存在
      return chr(0).chr(0);  #不存在则返回初始值
    $code=chr($result);
    $result=array_search($province, $dataArr[1][$country]); #查找数组中是否含有data2
    if($result===false)
      return $code.chr(0);
    return $code.chr($result);   #返回对应ASCII(0~255)所指定的字符 
  }
  /**
  * @desc 计算用户存储编码数据的相关位置信息
  * 
  * @param $userID int | 用户的ID
  *
  * @return array | 返回一个数组 包含数据存储时的分片ID、以及属于用户的存储位置(偏移量)
  */
  public function savePosition($userID)
  {
    $shardSize=pow(2, 3);   #每个分片的大小
    $position=$userID*2;    #user的排位
    $arr[&#39;shardID&#39;]=floor($position/$shardSize);  #分片ID
    $arr[&#39;offset&#39;]=$position%$shardSize;   #偏移量
    return $arr;
  }
  /**
  * @desc | 整合方法,将编码信息存入redis中string相应的位置
  *
  * @param $userID int      | 用户ID
  * @param $countries string   | 第一类数据,国家字符串
  * @param $provinces 二维array | 第二类数据,各国省份数组
  * @param $country string       | 具体信息--国家
  * @param $province  string      | 具体信息--省份
  * @param $cache 1/0      | 是否使用缓存,默认0不使用
  *
  * @return 成功返回写入位置/失败false
  */
  public function saveCode($userID,$countries,$provinces,$country,$province,$cache=0)
  {
    $code=$this->getCode($countries,$provinces,$country,$province,$cache=0);
    $arr=$this->savePosition($userID); #存储相关位置信息
    return $this->redis->setrange(&#39;save_code_&#39;.$arr[&#39;shardID&#39;],$arr[&#39;offset&#39;],$code);
  }
  /**
  * @desc 获取用户的具体国家与省份信息
  *
  * @param $userID int | 用户ID
  *
  * @return array | 返回包含国家和省份信息的数组
  */
  public function getMessage($userID)
  {
    $position=$this->savePosition($userID);
    $code=$this->redis->getrange(&#39;save_code_&#39;.$position[&#39;shardID&#39;],$position[&#39;offset&#39;],$position[&#39;offset&#39;]+1);
    $arr=str_split($code);
    $areaArr=$this->dealData(&#39;&#39;, &#39;&#39;,$cache=1); #使用缓存数据
    $message[&#39;country&#39;]=$areaArr[0][ord($arr[0])];
    $message[&#39;province&#39;]=$areaArr[1][$message[&#39;country&#39;]][ord($arr[1])];
    return $message;
  }
}
header("content-type: text/html;charset=utf8;");
$countries="无 中国 日本 越南 朝鲜 俄罗斯 巴基斯坦 美国";
$provinces=array(
    &#39;无&#39;=>array(&#39;无&#39;),
    &#39;中国&#39;=>array(&#39;无&#39;,&#39;广东&#39;,&#39;湖南&#39;,&#39;湖北&#39;,&#39;广西&#39;,&#39;云南&#39;,&#39;湖南&#39;,&#39;河北&#39;),
    &#39;日本&#39;=>array(&#39;无&#39;,&#39;龟孙子区&#39;,&#39;王八区&#39;,&#39;倭国鬼区&#39;,&#39;鬼子区&#39;,&#39;萝卜头区&#39;),
  );
$obj=new PackBytes(&#39;192.168.95.11&#39;);
/*
#数据处理,并将其缓存到redis中
$b=$obj->dealData($countries,$provinces);
echo "<pre class="brush:php;toolbar:false">";
print_r($b);
echo "
";die; */ /* #存储用户国家省份信息 $country='中国'; $province='广东'; $result=$obj->saveCode(0,$countries,$provinces,$country,$province); echo "
";
print_r($result);
echo "
"; */ /* #取出用户国家省份信息 $a=$obj->getMessage(15); echo "
";
print_r($a);
echo "
";die; */ ?>

测试:

1、dealData处理后的信息,即为'信息表表格'

2、saveCode()

userID 国家 省份
0 中国 广东
13 日本 龟孙子区
15 日本 王八区

3、getMessage()

위 내용은 PHP Redis의 메모리 사용량을 줄이는 방법(그림 및 텍스트)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.