Skip to content

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 slabs

Java 客户端

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. 增减节点只影响相邻区间的 Key
java
// XMemcached 默认使用一致性哈希
builder.setSessionLocator(new KetamaMemcachedSessionLocator());

与 Redis 对比

维度MemcachedRedis
数据结构仅 String多种(String/Hash/List/Set/ZSet 等)
持久化RDB + AOF
集群客户端分片原生 Cluster / Sentinel
多线程是(多核利用好)6.0+ 多线程 IO
内存效率略高(Slab 分配)略低(数据结构开销)
事务MULTI/EXEC
Lua 脚本支持
发布订阅支持
适用场景简单 KV 缓存,极致内存效率通用缓存,复杂数据结构

适用场景

Memcached 在以下场景仍有优势:

  1. 大量小对象缓存:Slab 分配器内存利用率高
  2. 多核服务器:多线程充分利用 CPU
  3. 纯缓存场景:不需要持久化和复杂数据结构
  4. 已有 Memcached 基础设施:迁移成本高时保持现状

故障处理案例

案例一:节点宕机导致缓存失效

现象:某 Memcached 节点宕机,该节点上的所有缓存失效,大量请求打到数据库。

处理

  1. 立即重启 Memcached 节点(内存清空,缓存需重建)
  2. 数据库开启慢查询监控,防止被压垮
  3. 考虑使用 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);
    }
}

PaaS 中间件生态系统深度学习文档