Memcached 介绍
Memcached 是什么?
Free & open source, high-performance, distributed memory object caching system(自由 & 开放源码, 高性能, 分布式的内存对象缓存系统). 由 LiveJournal 旗下的 danga 公司开发的老牌 nosql 应用.
什么是 NoSQL?
NoSQL, 指的是非关系型的数据库.
相对于传统关系型数据库的 "行与列",NoSQL 的鲜明特点为 key-value 存储(memcache,Redis), 或基于文档存储(MongoDB).
注: nosql --not only sql, 不仅仅是关系型数据库
Memcached 安装
Linux 下编译 Memcached
准备编译环境
再 Linux 下编译, 需要 gcc,make,cmake,autoconf,libtool 等工具, 这几件工具, 以后还要编译 Redis 等使用, 所以需要先安装. 在 Linux 系统联网后, 用如下命令安装
yum install gcc gcc-c++ make cmake autoconf libtool
编译 Memcached
Memcached 依赖于 libevent 库, 因此我们需要先安装 libevent. 分别到 libevent.org 和 Memcached.org 下载最新的 stable 版本(稳定版).
先编译 libevent, 再编译 Memcached.
编译 Memcached 时要指定 libevent 的路径.
过程如下: 假设源码在 / root/package 下, 安装在 / usr/local 下
- tar zxvf libevent-2.1.8-stable.tar.gz
- cd libevent-2.1.8-stable
- ./configure --prefix=/usr/local/libevent
- make && make install
安装 Memcached
- tar zxvf Memcached-1.5.1.tar.gz
- cd Memcached-1.5.1
- ./configure --prefix=/usr/local/Memcached --with-libevent=/usr/local/libevent/
- make && make install
配置环境变量
- vi /etc/profile
- export PATH="$PATH:/usr/local/memcached/bin"
- source /etc/profile
创建 Memcached 用户
useradd Memcached
设置开机自动启动:
vi /etc/rc.local
增加
/usr/local/Memcached/bin/Memcached -u Memcached -m 64 &
PHP 安装 Memcached 扩展
http://pecl.php.net/package/memcache 下载扩展包
- wget https://pecl.php.net/get/memcache-2.2.7.tgz
- tar zxvf memcache-2.2.7.tgz
- cd memcache-2.2.7
- phpize // 执行 phpize 命令, phpize 是 PHP 的工具, 用来将 PHP 的扩展与 PHP 程序建立关联
配置编译安装
./configure && make && make install
修改 PHP.INI
vi /usr/local/PHP/lib/PHP.INI
在大约 928 行左右加上扩展配置
- ;Linux extension load
- extension=memcache.so
重启 Apache
apachectl -k restart
在 phpinfo 里可以查找到 memcache 说明安装成功
Memcached 的启动
Memcached -m 64 -p 11211 -u nobody -d //-d 表示后台运行(也可以用 &)
可以使用 Memcached -h 查看帮助来了解各个参数的意义.
Memcached 基本使用
PHP 操作 Memcache
方法 | 方法说明 |
---|---|
connect() | 打开一个 memcached 服务端连接 |
add() | 增加一个条目到缓存服务器 |
addServer() | 向连接池中添加一个 memcache 服务器 |
increment() | 增加一个元素的值 |
decrement() | 减小一个元素的值 |
delete() | 从服务端删除一个元素 |
flush() | 清洗 (删除) 已经存储的所有的元素 |
get() | 从服务端检回一个元素 |
set() | 保存数据到缓存服务器 |
replace() | 替换已经存在的元素的值 |
pconnect() | 打开一个到服务器的持久化连接 |
close() | 关闭 memcache 连接 |
PHP 连接 memcache 服务
- $memcache = new Memcache();
- $memcache->connect("192.168.20.131", 11211);
- bool Memcache::set (string $key , mixed $var [, int $flag [, int $expire ]])
$key: 要设置值的 key
$var: 要存储的值, 字符串和数值直接存储, 其他类型序列化后存储. 数据的最大长度为 1M.
$flag: 使用 MEMCACHE_COMPRESSED 指定对值进行压缩(使用 zlib). 通常传入 0 即可, 表示不需要压缩.
$expire: 当前写入缓存的数据的失效时间. 如果此值设置为 0 表明此数据永不过期. 当时间小于 30 天时表示的是时间间隔, 当时间大于 30 天表示时间戳
bool Memcache::add (string $key , mixed $var [, int $flag [, int $expire ]])
说明: 与 set 类似, 仅仅可以执行添加操作, 不能执行修改操作, 当 key 已经存在时, 则 add 失败
bool Memcache::replace (string $key , mixed $var [, int $flag [, int $expire ]])
说明: 与 set 类似, 仅仅可以执行替换操作, 仅仅在 key 存在时才可以执行, 当 key 不存在时, 替换是失败的
get($key [, $flag]) --- 获取
获取时, 有时需要设置第二个参数, flag 标志!
比如如果存储时设置了第二个参数为压缩存储, 那么获取时也需要传递压缩存储的参数.
increment($key, $num) --- 递增
在原有值得基础上增加, 第二个参数不写默认是 1
decrement($key, $num) --- 递减
在原有值得基础减少,
delete($key) --- 删除
fulsh() --- 清空 / 刷新
close() --- 关闭连接
PHP
Memcached 的内存管理与删除机制
内存的碎片化
如果用 c 语言直接 malloc, free 来向操作系统申请和释放内存时, 在不断的申请和释放过程中, 形成了一些很小的内存片断, 无法再利用. 这种空闲, 但无法利用内存的现象,-- 称为内存的碎片化.
slab allocator 缓解内存碎片化
Memcached 用 slab allocator 机制来管理内存.
Slab allocator 原理: 预告把内存划分成数个 slab cl ass 仓库.(每个 slab class 大小 1 M)各仓库, 切分成不同尺时的小块(chunk).(图 3.2)
需要存内容时, 判断内容的大小, 为其选取合理的仓库.
系统如何选择合适的 chunk?
Memcached 根据收到的数据的大小, 选择最适合数据大小的 chunk 组(slab class)(图 3.3).Memcached 中保存着 slab class 内空闲 chunk 的列表, 根据该列表选择空的 chunk, 然后将数据缓存于其中.
警告:
如果有 100byte 的内存要存, 但 122 大小的仓库的 chunk 满了
并不会寻找更大的, 如 144 的仓库来存储,
而是把 122 仓库的旧数据踢掉! 详见过期与删除机制
固定大小的 chunk 带来的内存浪费
由于 slab allocator 机制中, 分配的 chunk 的大小是 "固定" 的, 因此, 对于特定的 item, 可能造成内存空间的浪费.
比如, 将 100 字节的数据缓存到 122 字节的 chunk 中, 剩余的 22 字节就浪费了图 3.4)
对于 chunk 空间的浪费问题, 无法彻底解决, 只能缓解该问题.
开发者可以对网站中缓存中的 item 的长度进行统计, 并制定合理的 slab class 中的 chunk 的大小.
可惜的是, 我们目前还不能自定义 chunk 的大小, 但可以通过参数来调整各 slab class 中 chunk 大小的增长速度. 即增长因子, grow factor!
growfactor 调优
Memcached 在启动时可以通过 - f 选项指定 Growth Factor 因子, 并在某种程度上控制 slab 之间的差异. 默认值为 1.25. 但是, 在该选项出现之前, 这个因子曾经固定为 2, 称为 "powers of2" 策略.
对比可知, 当 f=2 时, 各 slab 中的 chunk size 增长很快, 有些情况下就相当浪费内存. 因此, 我们应细心统计缓存的大小, 制定合理的增长因子.
注意:
当 f=1.25 时, 从输出结果来看, 某些相邻的 slab class 的大小比值并非为 1.25, 可能会觉得有些 计算误差, 这些误差是为了保持字节数的对齐而故意设置的.
Memcached 的过期数据惰性删除
1: 当某个值过期后, 并没有从内存删除, 因此, stats 统计时, curr_item 有其信息
2: 当某个新值去占用他的位置时, 当成空 chunk 来占用.
3: 当 get 值时, 判断是否过期, 如果过期, 返回空, 并且清空, curr_item 就减少了.
这个过期, 只是让用户看不到这个数据而已, 并没有在过期的瞬间立即从内存删除. 这个称为 lazy expiration, 惰性失效.
好处: 节省了 CPU 时间和检测的成本
Memcached 的 LRU 删除机制
如果以 122byte 大小的 chunk 举例, 122 的 chunk 都满了, 又有新的值 (长度为 120) 要加入, 要 挤掉谁?
Memcached 此处用的 lru 删除机制.
(操作系统的内存管理, 常用 fifo,lru 删除)
lru: least recently used 最近最少使用
fifo: first in ,first out
原理: 当某个单元被请求时, 维护一个计数器, 通过计数器来判断最近谁最少被使用. 就把谁 t 出.
注意: 即使某个 key 是设置的永久有效期, 也一样会被踢出来! 即 - 永久数据被踢现象
Memcached 的一些参数限制
key 的长度: 250 字节, (二进制协议支持 65536 个字节)
value 的限制: 1m, 一般都是存储一些文本, 如新闻列表等等, 这个值足够了.
内存的限制: 32 位下最大设置到 2G.
如果有 30g 数据要缓存, 一般也不会单实例装 30G, (不要把鸡蛋装在一个篮子里), 一般建议 开启多个实例(可以在不同的机器, 或同台机器上的不同端口)
分布式集群算法
Memcached 如何实现分布式
Memcached 并不像 MongoDB 那样, 允许配置多个节点, 且节点之间 "自动分配数据". 也就是说, Memcached 节点之间是不互相通信的
因此, Memcached 的分布式, 要靠用户去设计算法, 把数据分布在多个 Memcached 节点中.
求余 / 取模算法
代码:
- interface hasher {
- public function hash($str);
- }
- interface distribution {
- public function lookup($key);
- }
- /**
- * Class Moder
- * 求余算法
- */
- class Moder implements hasher, distribution {
- protected $server = array();
- protected $num = 0;
- // 计算一个字符串对应的 32 位循环冗余校验码多项式
- public function hash($str) {
- return sprintf("%u", crc32($str));
- }
- // 查询数据应存放的节点服务器
- public function lookup($key) {
- $index = $this->hash($key) % $this->num;
- return $this->server[$index];
- }
- // 模拟增加一台 Memcached 服务器
- public function addNode($s) {
- $this->server[] = $s;
- $this->num++;
- }
- // 模拟 Memcached 服务器宕机
- public function delNode($s) {
- foreach ($this->server as $key => $value) {
- if($s == $value) {
- unset($this->server[$key]);
- }
- }
- $this->num--;
- $this->server = array_merge($this->server); // 重新整理 server 的键, 使其按照 0->1->2 递增
- }
- }
- $moder = new Moder();
- $moder->addNode('a');
- $moder->addNode('b');
- $moder->addNode('c');
- $moder->addNode('d');
- for($i = 0; $i <100; $i++) {
- $key = 'key_'.$i;
- echo $key, '---->', $moder->lookup($key), '<br>';
- }
一致性哈希算法
通俗理解一致性哈希: 把各服务器节点映射放在钟表的各个时刻上, 把 key 也映射到钟表的某个时刻上, 该 key 沿着钟表顺时针走, 碰到的第一个节点即为该 key 的存储节点. 如下图所示:
一致性哈希 + 虚拟节点算法代码
- interface hasher {
- public function hash($str);
- }
- interface distribution {
- public function lookup($key);
- }
- class Consistency implements hasher, distribution {
- protected $nodes = array();
- protected $points = array();
- protected $multi = 64; // 每个 Memcached 服务器对应的虚拟节点数量
- // 计算一个字符串对应的 32 位循环冗余校验码多项式
- public function hash($str) {
- return sprintf("%u", crc32($str));
- }
- // 查询数据应存放的节点服务器
- public function lookup($key) {
- $position = $this->hash($key);
- reset($this->points); // 重置指针 (因为 foreach 遍历时会数组指针后移, 等到下次遍历数组时, key() 方法获取的就不是第一个元素的键了)
- $needle = key($this->points); // 默认会落在第一个节点
- foreach ($this->points as $key => $value) {
- if($position <= $key) {
- $needle = $key;
- break;
- }
- }
- return $this->points[$needle];
- }
- // 添加节点
- public function addNode($node) {
- $this->nodes[$node] = array();
- for($i = 0; $i <$this->multi; $i++) {
- $point = $node . '_' . $i;
- $point = $this->hash($point); // 虚拟节点转为数字
- $this->points[$point] = $node;
- $this->nodes[$node][] = $point;
- $this->resort();
- }
- }
- public function delNode($node) {
- foreach ($this->nodes[$node] as $p) { // 清除 64 个虚拟节点
- unset($this->points[$p]);
- }
- unset($this->nodes[$node]); // 去掉节点
- }
- // 对虚拟几点进行排序
- protected function resort() {
- ksort($this->points);
- }
- }
- $consistency = new Consistency();
- $consistency->addNode('A');
- $consistency->addNode('B');
- $consistency->addNode('C');
- $consistency->addNode('D');
- echo $consistency->hash('name');
- echo $consistency->lookup('name');
并发处理 - 乐观锁
在新版的 Memcached 中, 增加了对并发的控制, 处理方案是: 乐观锁
并发: 多个进程(连接), 同时操作一个 key. 就是并发操作.
乐观锁:
进程 A, 先操作了缓存项
在进程 A 第二次操作缓存项前, 进程 B 操作了缓存项.
之后, 进程 A 第二次操作缓存项. 检查, 在进程 A 第一次操作后, 是否有其他进程操作过需要的缓存项. 如果有, 则放弃第二次操作. 采取乐观的处理态度.(乐观锁定)
支持乐观锁的操作:
gets() 获取
cas() 设置
注意: Memcache 扩展还不支持这两个操作, 在 telnet 上可以演示
Memcached 经典问题或现象
缓存雪崩现象及真实案例
缓存雪崩一般是由某个缓存节点失效, 导致其他节点的缓存命中率下降, 缓存中缺失的数据去数据库查询. 短时间内, 造成数据库服务器崩溃.
重启 DB, 短期又被压垮, 但缓存数据也多一些.
DB 反复多次启动, 缓存重建完毕, DB 才稳定运行.
或者, 是由于缓存周期性的失效, 比如每 6 个小时失效一次, 那么每 6 小时, 将有一个请求 "峰值", 严重者甚至会令 DB 崩溃.
解决办法 / 方案: 把缓存设置为随机 3-9 个小时的生命周期, 这样不同时失效, 把工作分担到各个时间点.
缓存的无底洞现象 multiget-hole
永久数据被踢现象
网上有人反馈为 "memcached 数据丢失", 明明设为永久有效, 却莫名其妙的丢失了.
其实, 这要从 2 个方面来找原因:
即前面介绍的 惰性删除, 与 LRU 最近最少使用记录删除.
分析(如下图)
1: 如果 slab 里的很多 chunk, 已经过期, 但过期后没有被 get 过, 系统不知他们已经过期.
2: 永久数据很久没 get 了, 不活跃, 如果新增 item, 则永久数据被踢了.
3: 当然, 如果那些非永久数据被 get, 也会被标识为 expire, 从而不会再踢掉永久数据
来源: https://www.cnblogs.com/itbsl/p/9993301.html