Memcached:高性能分布式内存对象缓存系统
一、引言
Memcached 是一款开源的高性能分布式内存缓存系统,旨在通过将数据缓存在内存中来减少后端数据库访问次数,从而提升 Web 应用的响应速度与并发能力。自 2003 年由 Brad Fitzpatrick 开发以来,Memcached 已广泛应用于各大互联网公司,是构建可扩展、高可用架构的重要组成部分。
本文将从以下几个方面介绍 Memcached:
- 核心原理与架构
- 部署与集群拓扑
- 客户端应用:常见语言示例
- 一致性哈希与扩缩容策略
- 缓存失效与淘汰策略
- 性能优化与运维注意事项
二、核心原理与架构
2.1 基本原理
- 内存存储
Memcached 将数据以<key, value>
形式缓存到 RAM 中,读取非常迅速。所有数据存储在进程内存中,没有磁盘落盘操作,因此延迟极低。 纯 KV 接口
Memcached 提供简单的文本协议与二进制协议(Binary Protocol),客户端可通过set
/get
/delete
等命令进行操作。示例如下:set user:123 0 60 24\r\n {"name": "Alice", "age": 30}\r\n get user:123\r\n
以上示例将 key=
user:123
的值设置为一段 JSON 字符串,有效期 60 秒,长度 24 字节。
2.2 内部数据结构
- Slab Allocator
为避免频繁的内存碎片,Memcached 使用 slab 分配器将内存划分为不同大小的 slab class(例如 64B、128B、256B、512B……)。当存储某个对象时,Memcached 会根据 object size 选择最合适的 slab class,从而减少碎片化并提高内存利用率。 - Hash Table
Memcached 在每个实例内部维护一个哈希表,以便O(1) 时间完成 key 到内存地址的映射。哈希表使用拉链法解决冲突,同时配合 slab allocator 管理对象内存。
2.3 分布式架构
- Memcached 本身并不支持多活或主从复制,每个实例是独立的。分布式是通过客户端一致性哈希或Ketama等算法,将 key 映射到不同实例上,形成一个逻辑上的集群。如“图1”所示,ClientA/B/C 根据哈希后,分别将请求发送到最合适的服务器(Server1/Server2/Server3)。
- 无中心节点:整个体系中没有集中式的 Coordinator,客户端直接均衡请求到集群中各节点,易于水平扩展。
三、部署与集群拓扑
3.1 单机部署
以 Linux 环境为例,快速安装与启动 Memcached:
# 安装(以 Ubuntu 为例)
sudo apt-get update
sudo apt-get install memcached
# 启动,并指定监听端口(默认 11211)与最大内存尺寸
sudo memcached -d -m 1024 -p 11211 -u memcache
# 参数说明:
# -m 1024 : 最大使用 1024MB 内存
# -p 11211 : 监听 TCP 端口为 11211
# -u memcache : 以 memcache 用户运行
启动后,可通过以下命令验证:
# 查看进程
ps aux | grep memcached
# 测试客户端连通性
echo "stats" | nc localhost 11211
3.2 集群部署(多实例)
在生产环境通常需要多台服务器运行 Memcached 实例,以分担负载。常见做法:
多结点分布式
将 N 台 Memcached 服务器节点部署在不同机器或容器上,并通过客户端的一致性哈希算法决定将每个 key 存储到哪个节点。如下:- 节点列表:
["10.0.0.1:11211", "10.0.0.2:11211", "10.0.0.3:11211"]
- 客户端根据 Ketama 哈希环,将 key 映射到相应节点。
- 节点列表:
- 多进程多端口
在同一台机器上,可同时运行多个 memcached 实例,分别绑定不同的端口或 IP。适用于资源隔离或多租户场景。
图1:Memcached 分布式集群架构示意图
上方示例图展示了 3 台服务器(Server1、Server2、Server3),及若干客户端(ClientA、ClientB、ClientC)通过一致性哈希或环状哈希机制将请求发送到相应节点。
四、客户端应用:常见语言示例
4.1 Python 客户端示例(使用 pymemcache
)
from pymemcache.client.hash import HashClient
# 假设有三个 Memcached 节点
servers = [("10.0.0.1", 11211), ("10.0.0.2", 11211), ("10.0.0.3", 11211)]
client = HashClient(servers)
# 设置数据
key = "user:1001"
value = {"name": "Bob", "age": 25}
client.set(key, str(value), expire=120) # 将 dict 转为字符串并缓存 120 秒
# 获取数据
result = client.get(key)
if result:
print("缓存命中,值:", result.decode())
# 删除数据
client.delete(key)
说明:
HashClient
会自动根据 key 值做一致性哈希映射到对应节点。expire
为过期时间(秒),默认为 0 表示永不过期。
4.2 Java 客户端示例(使用 spymemcached
)
import net.spy.memcached.MemcachedClient;
import java.net.InetSocketAddress;
public class MemcachedJavaExample {
public static void main(String[] args) throws Exception {
// 定义集群节点
MemcachedClient client = new MemcachedClient(
new InetSocketAddress("10.0.0.1", 11211),
new InetSocketAddress("10.0.0.2", 11211),
new InetSocketAddress("10.0.0.3", 11211)
);
// 写入缓存
String key = "session:abcd1234";
String value = "user=Bob;role=admin";
client.set(key, 300, value); // 缓存 300 秒
// 读取缓存
Object cached = client.get(key);
if (cached != null) {
System.out.println("缓存获取: " + cached.toString());
} else {
System.out.println("未命中");
}
// 删除缓存
client.delete(key);
client.shutdown();
}
}
说明:
MemcachedClient
构造时传入多个节点,会自动使用一致性哈希算法分布数据。
4.3 PHP 客户端示例(使用 Memcached
扩展)
<?php
// 初始化 Memcached 客户端
$m = new Memcached();
$m->addServer('10.0.0.1', 11211);
$m->addServer('10.0.0.2', 11211);
$m->addServer('10.0.0.3', 11211);
// 设置缓存
$m->set('page:home', file_get_contents('home.html'), 3600);
// 获取缓存
$html = $m->get('page:home');
if ($html) {
echo "从缓存加载首页内容";
echo $html;
} else {
echo "缓存未命中,重新生成并设置";
// ... 重新生成 ...
}
// 删除缓存
$m->delete('page:home');
?>
说明:
- PHP 内置
Memcached
扩展支持一致性哈希,addServer()
多次调用即可添加多个节点。
五、一致性哈希与扩缩容策略
5.1 一致性哈希原理
- 传统哈希(如
hash(key) % N
)在节点上下线或扩容时会导致大量 key 重新映射,缓存命中率骤降。 - 一致性哈希(Consistent Hashing) 将整个哈希空间想象成一个环(0\~2³²-1),每个服务器(包括虚拟节点)在环上占据一个或多个位置。Key 通过相同哈希映射到环上的某个点,然后顺时针找到第一个服务器节点来存储。
- 当某台服务器加入或离开,只会影响其相邻区域的少量 key,不会造成全局大量失效。
5.2 虚拟节点(Virtual Node)
- 为了避免服务器节点分布不均,一般会为每台真实服务器创建多个虚拟节点(例如 100\~200 个),将它们做哈希后分布到环上。
- 客户端在环上找到的第一个虚拟节点对应一个真实服务器,即可减少节点数量变化带来的数据迁移。
5.3 扩容与缩容示例
添加服务器
- 新服务器加入后,客户端会在一致性哈希环上插入对应的虚拟节点,环上受影响的 key 只需迁移给新服务器。
示例流程(概念):
- 在环上计算新服务器的每个虚拟节点位置。
- 客户端更新哈希环映射表。
- 新服务器接管部分 key(旧服务器负责将这些 key 迁移到新服务器)。
删除服务器
- 移除服务器对应的虚拟节点,环上相邻节点接管其负责的 key。
- 只需将原本属于该服务器的 key 重新写入相邻节点,其他 key 不受影响。
六、缓存失效与淘汰策略
6.1 过期(TTL)与显式删除
- 当通过
set
命令设置expire
参数时,Memcached 会在后台检查并自动清理已过期的数据。 - 客户端也可以显式调用
delete key
删除某个缓存项。
6.2 LRU 淘汰机制
- Memcached 在单实例内部使用LRU (Least Recently Used) 策略管理各 slab class 中存储的对象:当某个 slab class 内存空间用尽,且无法分配新对象时,会淘汰该 slab class 中最久未被访问的 key。
- 各 slab class 独立维护 LRU 列表,避免不同大小对象相互挤占空间。
6.3 高阶淘汰策略:LRU / LFU / 带样本的 LRU
- 虽然 Memcached 默认仅支持 LRU,但可以结合外部模块或客户端策略实现如 LFU (Least Frequently Used) 等更复杂的淘汰算法。
- 例如:将部分热点 key 在客户端层面持续刷新过期时间,使得热点 key 不被淘汰。
七、性能优化与运维注意事项
7.1 配置调优
内存与 slab 配置
- 通过
-m
参数设置合适的内存总量。 - 使用
stats items
与stats slabs
命令监控各 slab class 的命中率与被淘汰次数,根据实际情况调整 slab 分配。
- 通过
网络参数
- 对高并发场景,应调整系统
ulimit -n
打开文件描述符数。 - 根据网络带宽计算最大并发客户端连接数,避免出现 TCP 队头阻塞问题。
- 对高并发场景,应调整系统
多核优化
- Memcached 默认使用多线程架构,可通过
-t
参数指定线程数,例如memcached -m 2048 -p 11211 -t 4
。线程数可设置为 CPU 核心数或更高,但要注意锁竞争。
- Memcached 默认使用多线程架构,可通过
7.2 监控与告警
关键指标:
- Cache Hit Ratio:
get_hits / get_misses
,命中率过低时需检查 key 设计或容量是否不足; - Evictions(被淘汰次数):若快速递增,说明 memory 不足或某些 slab class 项过大;
- Connection Stats:
curr_connections
,total_connections
; - Bytes Read/Written,
cmd_get
,cmd_set
:表示负载情况。
- Cache Hit Ratio:
- 推荐通过 Prometheus + Grafana 或 InfluxDB + Grafana 监控 Memcached 指标,并设置阈值告警,如命中率低于 80% 或被淘汰次数猛增时触发报警。
7.3 数据一致性与回源策略
缓存穿透:若缓存不存在时直接到后端 DB 查询,可能造成高并发下产生大量 DB 访问(击穿)。
解决方案:
- 在缓存中写入空对象或 Bloom Filter 检测,避免不存在 key 大量打到 DB。
缓存雪崩:多条缓存同时过期,导致瞬间大量请求到后端。
解决方案:
- 使用随机过期时间(TTL 增减少量随机值);
- 在热点数据点使用永不过期 + 定时更新策略。
数据不一致:当后端数据更新,未及时更新或删除缓存,导致脏数据。
解决方案:
- 双写策略:更新数据库的同时清除或更新缓存;
- 异步 Cache Invalidation:使用消息队列通知其他节点清除缓存。
八、总结
Memcached 作为一款成熟、简洁的分布式内存缓存系统,具有低延迟、高吞吐、易扩展等特点。通过合理的部署、客户端一致性哈希、有效的淘汰策略和运维监控,可以显著提升应用性能,减轻后端数据库压力。
- 核心优势:秒级响应、极低延迟、横向扩展简单。
- 适用场景:Session 缓存、热点数据缓存、页面缓存、API 响应缓存等。
- 注意事项:需要设计合理的 key 规范、过期策略和缓存更新机制,以防止缓存击穿/雪崩/污染。