Memcached — 分布式内存缓存
架构特点
Memcached 是一个高性能、分布式内存对象缓存系统,设计极简:只支持 String 类型,无持久化,无主从复制,客户端负责分片。
Client(一致性哈希)
├──► Memcached Node1(192.168.1.10:11211)
├──► Memcached Node2(192.168.1.11:11211)
└──► Memcached Node3(192.168.1.12:11211)
特点:
- 多线程(区别于 Redis 单线程)
- 纯内存,无持久化
- 客户端分片(无服务端集群概念)
- 内存利用率高(Slab 分配器)Slab 内存分配
Memcached 使用 Slab 分配器管理内存,避免内存碎片:
Slab Class 1: 96 bytes → 存储小对象
Slab Class 2: 120 bytes
Slab Class 3: 152 bytes
...
Slab Class N: 1MB → 存储大对象
每个 Slab Class 包含多个 Page(默认 1MB)
每个 Page 切分为固定大小的 Chunk内存浪费问题:存储 100 bytes 的对象会占用 120 bytes 的 Chunk(Slab Class 2),浪费 20 bytes。
基本操作
bash
# 连接 Memcached
telnet localhost 11211
# 存储命令
set key 0 3600 5 # key, flags, exptime(s), bytes
hello # value
# 读取
get key
# 原子操作
incr counter 1 # 原子递增
decr counter 1 # 原子递减
# CAS(Compare And Swap,防并发覆盖)
gets key # 返回 cas_token
cas key 0 3600 5 <cas_token>
world
# 统计
stats
stats items
stats slabsJava 客户端
Spymemcached
java
MemcachedClient client = new MemcachedClient(
new BinaryConnectionFactory(),
AddrUtil.getAddresses("node1:11211 node2:11211 node3:11211")
);
// 存储(异步)
Future<Boolean> future = client.set("user:123", 3600, userJson);
future.get(1, TimeUnit.SECONDS);
// 读取
String value = (String) client.get("user:123");
// 批量读取
Map<String, Object> bulk = client.getBulk("key1", "key2", "key3");
// 原子递增
long count = client.incr("page:views", 1, 0, 3600);
// 关闭
client.shutdown();XMemcached(推荐)
java
MemcachedClientBuilder builder = new XMemcachedClientBuilder(
AddrUtil.getAddresses("node1:11211 node2:11211 node3:11211")
);
builder.setConnectionPoolSize(2); // 每个节点连接池大小
builder.setCommandFactory(new BinaryCommandFactory()); // 二进制协议
MemcachedClient client = builder.build();
// 存储
client.set("key", 3600, value);
// 读取
String result = client.get("key");
// 带超时的操作
String result = client.get("key", 1000); // 1s 超时一致性哈希
客户端使用一致性哈希将 Key 路由到对应节点,节点增减时只影响少量 Key:
普通哈希:hash(key) % N
问题:N 变化时,几乎所有 Key 都需要重新映射
一致性哈希:
1. 将节点映射到哈希环(0 ~ 2^32)
2. Key 顺时针找到第一个节点
3. 增减节点只影响相邻区间的 Keyjava
// XMemcached 默认使用一致性哈希
builder.setSessionLocator(new KetamaMemcachedSessionLocator());与 Redis 对比
| 维度 | Memcached | Redis |
|---|---|---|
| 数据结构 | 仅 String | 多种(String/Hash/List/Set/ZSet 等) |
| 持久化 | 无 | RDB + AOF |
| 集群 | 客户端分片 | 原生 Cluster / Sentinel |
| 多线程 | 是(多核利用好) | 6.0+ 多线程 IO |
| 内存效率 | 略高(Slab 分配) | 略低(数据结构开销) |
| 事务 | 无 | MULTI/EXEC |
| Lua 脚本 | 无 | 支持 |
| 发布订阅 | 无 | 支持 |
| 适用场景 | 简单 KV 缓存,极致内存效率 | 通用缓存,复杂数据结构 |
适用场景
Memcached 在以下场景仍有优势:
- 大量小对象缓存:Slab 分配器内存利用率高
- 多核服务器:多线程充分利用 CPU
- 纯缓存场景:不需要持久化和复杂数据结构
- 已有 Memcached 基础设施:迁移成本高时保持现状
故障处理案例
案例一:节点宕机导致缓存失效
现象:某 Memcached 节点宕机,该节点上的所有缓存失效,大量请求打到数据库。
处理:
- 立即重启 Memcached 节点(内存清空,缓存需重建)
- 数据库开启慢查询监控,防止被压垮
- 考虑使用 Redis Cluster 替代(原生高可用)
预防:
- 使用 Ketama 一致性哈希,节点宕机只影响该节点的 Key
- 配置合理的连接超时,快速失败
案例二:内存碎片严重
现象:stats 显示 bytes 远小于 limit_maxbytes,但仍出现内存不足。
排查:
bash
# 查看 Slab 使用情况
stats slabs
# 关注 mem_requested vs chunk_size * chunks_per_page * total_pages解决:
bash
# 重启 Memcached(清空内存,重新分配)
# 或调整 Slab 增长因子
memcached -f 1.25 # 默认 1.25,减小可减少碎片但增加 Slab 数量案例三:热点 Key 导致单节点过热
现象:某个 Memcached 节点 CPU 100%,其他节点空闲。
原因:热点 Key 集中在同一节点。
解决:
java
// 热点 Key 复制到多个节点(客户端随机读取)
String[] hotKeys = {"hot:product:123#1", "hot:product:123#2", "hot:product:123#3"};
String key = hotKeys[ThreadLocalRandom.current().nextInt(hotKeys.length)];
String value = client.get(key);
if (value == null) {
value = db.getProduct(123);
for (String k : hotKeys) {
client.set(k, 300, value);
}
}