第01章 Memcached 协议概述
第01章 Memcached 协议概述
理解 Memcached 的协议设计哲学,是掌握其全部能力的起点。
1.1 Memcached 简介
Memcached 由 Brad Fitzpatrick 于 2003 年为 LiveJournal 开发,如今已成为互联网基础设施中最广泛使用的缓存系统之一。其核心设计理念可以概括为:
| 设计原则 | 说明 |
|---|---|
| 简单性 | 协议基于文本,易于实现和调试 |
| 高性能 | 单线程事件驱动模型(libevent),避免锁竞争 |
| 分布式 | 客户端决定数据分布,服务端无中心协调 |
| 易失性 | 仅用于缓存,不做持久化存储 |
核心定位
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 客户端 │────▶│ Memcached │────▶│ 数据库 │
│ (应用层) │◀────│ (缓存层) │◀────│ (持久层) │
└─────────────┘ └─────────────┘ └─────────────┘
先查缓存 缓存命中则返回 缓存未命中时查询
未命中回源 未命中则穿透 查询后回写缓存
1.2 协议历史演进
Memcached 的协议经历了三个阶段的发展:
版本时间线
| 时间 | 协议版本 | 特征 |
|---|---|---|
| 2003 | 文本协议(初版) | 最早的纯文本命令集 |
| 2009 | 二进制协议(草案) | Facebook 提出,更高效 |
| 2018 | Meta 协议(1.6+) | 扩展元数据,减少往返次数 |
各版本的动机
文本协议 诞生于 Memcached 的早期开发阶段,目标是"能在 telnet 中手动调试"。命令格式可读性极高,例如:
set mykey 0 3600 5
hello
STORED
二进制协议 的出现是为了解决文本协议的两个缺陷:
- 解析开销:文本解析需要字符串处理
- 功能局限:无法高效传递元数据
Meta 协议 在二进制协议基础上进一步演进,用紧凑的文本格式传递丰富的元数据,兼顾可读性与性能。
1.3 文本协议 vs 二进制协议
对比总览
| 维度 | 文本协议 | 二进制协议 | Meta 协议 |
|---|---|---|---|
| 格式 | 纯文本(ASCII) | 固定头 + 可变体 | 文本 + Flag 后缀 |
| 可读性 | ★★★★★ | ★★☆☆☆ | ★★★★☆ |
| 解析性能 | 中等 | 最高 | 较高 |
| 功能丰富度 | 基础 | 完整 | 最丰富 |
| 版本要求 | 所有版本 | 1.3+ | 1.6+ |
| 调试难度 | 低 | 高 | 中 |
| 生产使用 | 最广泛 | 少 | 逐渐增长 |
性能差异分析
文本协议解析时需要:
# 文本协议解析伪代码
line = socket.readline() # 读取一行
parts = line.split(" ") # 按空格分割
command = parts[0] # 提取命令
key = parts[1] # 提取 key
flags = int(parts[2]) # 转换 flags
exptime = int(parts[3]) # 转换过期时间
length = int(parts[4]) # 提取数据长度
data = socket.read(length) # 读取数据体
二进制协议解析时只需:
# 二进制协议解析伪代码
header = socket.read(24) # 固定 24 字节头
magic = header[0] # 直接字节读取
opcode = header[1]
key_length = struct.unpack(">H", header[2:4])[0]
body_length = struct.unpack(">I", header[8:12])[0]
body = socket.read(body_length) # 读取完整 body
注意: 在大多数实际场景中,网络延迟远大于协议解析开销,因此文本协议的性能劣势并不显著。选择协议时应更多考虑功能需求和可维护性。
1.4 Memcached 架构解析
进程模型
Memcached 进程
├── 主线程(Main Thread)
│ └── 监听端口,接受连接
├── 工作线程(Worker Threads)[默认4个]
│ ├── Worker 1 ── 处理客户端连接
│ ├── Worker 2 ── 处理客户端连接
│ ├── Worker 3 ── 处理客户端连接
│ └── Worker 4 ── 处理客户端连接
└── 内存管理器
├── Slab Allocator(分片分配器)
│ ├── Slab Class 1 (64B items)
│ ├── Slab Class 2 (128B items)
│ ├── ...
│ └── Slab Class N (1MB items)
└── LRU 淘汰链表
连接处理流程
- 客户端 建立 TCP 连接(默认端口 11211)
- 主线程 接受连接,Round-Robin 分配给 Worker 线程
- Worker 线程 通过 libevent 事件循环处理请求
- 命令解析器 解析协议消息,执行对应操作
- 哈希表 查找或存储 item
- 响应 按协议格式返回结果
关键启动参数
memcached \
-p 11211 \ # 监听端口
-m 1024 \ # 最大内存(MB)
-c 10240 \ # 最大连接数
-t 4 \ # 工作线程数
-l 127.0.0.1 \ # 监听地址
-U 0 \ # 禁用 UDP(0=禁用)
-o modern \ # 启用现代特性(1.6+)
-v # 详细日志(-vv 更详细)
1.5 协议通信模型
TCP 通信基础
Memcached 默认使用 TCP 长连接,协议遵循简单的请求-响应模式:
客户端 服务端
│ │
│──── 请求(Request)────▶ │
│ │──── 解析 → 执行
│◀── 响应(Response)──── │
│ │
│──── 请求(Request)────▶ │
│ │
│◀── 响应(Response)──── │
│ │
│──── ... │
管道化(Pipelining)
文本协议支持管道化:客户端可以在收到前一个响应之前发送后续请求,但响应必须按顺序返回。
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 11211))
# 管道化发送多个命令
sock.sendall(b"set key1 0 0 5\r\nhello\r\n")
sock.sendall(b"set key2 0 0 5\r\nworld\r\n")
sock.sendall(b"get key1\r\n")
# 按顺序接收响应
print(sock.recv(4096).decode())
# STORED\r\nSTORED\r\nVALUE key1 0 5\r\nhello\r\nEND\r\n
注意: 虽然可以管道化发送,但不要对有因果依赖的命令使用管道化(例如 set 之后立即 gets 进行 CAS 操作)。
1.6 命令分类
Memcached 命令按功能可以分为以下几类:
| 类别 | 文本协议命令 | 说明 |
|---|---|---|
| 存储 | set, add, replace, append, prepend, cas | 写入/修改缓存 |
| 检索 | get, gets | 读取缓存 |
| 删除 | delete | 删除缓存 |
| 原子操作 | incr, decr | 数值递增递减 |
| 统计 | stats, stats items, stats slabs | 获取服务状态 |
| 管理 | flush_all, version, quit, verbosity | 服务管理 |
| Meta | meta get, meta set, meta delete, meta debug | 高级操作 |
1.7 快速体验
使用 telnet 手动调试
# 连接到 Memcached
telnet 127.0.0.1 11211
# 设置一个键值
set greeting 0 60 5
hello
STORED
# 读取该键值
get greeting
VALUE greeting 0 5
hello
END
# 查看版本
version
VERSION 1.6.22
# 退出
quit
使用 netcat 快速测试
# 设置
echo -e "set testkey 0 300 4\r\ntest\r\n" | nc 127.0.0.1 11211
# 获取
echo "get testkey" | nc 127.0.0.1 11211
# 统计
echo "stats" | nc 127.0.0.1 11211
使用 Python 连接
#!/usr/bin/env python3
"""memcached_demo.py — Memcached 文本协议快速演示"""
import socket
def send_command(sock, command: str, data: bytes = b"") -> str:
"""发送命令并接收响应"""
sock.sendall(command.encode() + b"\r\n")
if data:
sock.sendall(data + b"\r\n")
response = b""
while True:
chunk = sock.recv(4096)
response += chunk
if b"END\r\n" in response or b"STORED\r\n" in response \
or b"NOT_STORED\r\n" in response or b"DELETED\r\n" in response \
or b"NOT_FOUND\r\n" in response:
break
return response.decode().strip()
def main():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(('127.0.0.1', 11211))
# 1. 查看版本
print("=== Version ===")
sock.sendall(b"version\r\n")
print(sock.recv(1024).decode().strip())
# 2. 存储数据
print("\n=== Set ===")
result = send_command(sock, "set user:1001 0 3600 13", b'{"name":"Bob"}')
print(result)
# 3. 读取数据
print("\n=== Get ===")
result = send_command(sock, "get user:1001")
print(result)
# 4. 删除数据
print("\n=== Delete ===")
result = send_command(sock, "delete user:1001")
print(result)
# 5. 查看统计
print("\n=== Stats (摘要) ===")
sock.sendall(b"stats\r\n")
data = sock.recv(8192).decode()
for line in data.split("\r\n"):
if line.startswith("STAT") and any(
k in line for k in ["curr_items", "total_items", "bytes", "cmd_get", "cmd_set"]
):
print(line)
sock.sendall(b"quit\r\n")
sock.close()
if __name__ == "__main__":
main()
运行结果示例:
=== Version ===
VERSION 1.6.22
=== Set ===
STORED
=== Get ===
VALUE user:1001 0 13
{"name":"Bob"}
END
=== Delete ===
DELETED
=== Stats (摘要) ===
STAT cmd_get 1
STAT cmd_set 1
STAT curr_items 0
STAT total_items 1
STAT bytes 0
1.8 业务场景
典型应用场景
| 场景 | 说明 | 关键命令 |
|---|---|---|
| 数据库查询缓存 | 缓存 SQL 查询结果 | set / get |
| 会话存储 | 存储用户 Session | set / get / delete |
| 页面片段缓存 | 缓存 HTML 片段 | set / get |
| 计数器 | 文章浏览量、点赞数 | incr / decr |
| 分布式锁 | 简单的互斥锁 | add / delete |
| 限流器 | API 调用频率限制 | incr + TTL |
实际案例:电商商品缓存
import json
import socket
from typing import Optional, Dict, Any
class SimpleMemcachedClient:
def __init__(self, host: str = '127.0.0.1', port: int = 11211):
self.host = host
self.port = port
self.sock = None
self._connect()
def _connect(self):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
def set(self, key: str, value: Any, ttl: int = 300) -> bool:
data = json.dumps(value).encode()
cmd = f"set {key} 0 {ttl} {len(data)}\r\n"
self.sock.sendall(cmd.encode() + data + b"\r\n")
resp = self.sock.recv(1024).decode().strip()
return resp == "STORED"
def get(self, key: str) -> Optional[Dict]:
self.sock.sendall(f"get {key}\r\n".encode())
resp = self.sock.recv(65536).decode()
lines = resp.strip().split("\r\n")
if len(lines) >= 2 and lines[0].startswith("VALUE"):
return json.loads(lines[1])
return None
def close(self):
self.sock.sendall(b"quit\r\n")
self.sock.close()
# 使用示例
cache = SimpleMemcachedClient()
# 缓存商品信息
product = {
"id": 10001,
"name": "机械键盘",
"price": 599.0,
"stock": 100
}
cache.set("product:10001", product, ttl=600)
# 查询商品
cached = cache.get("product:10001")
if cached:
print(f"命中缓存: {cached['name']} ¥{cached['price']}")
else:
print("缓存未命中,查询数据库...")
1.9 注意事项
- 不要将 Memcached 当作数据库使用:缓存数据随时可能丢失
- 单个 Value 最大 1MB:超出需自行分片或使用其他方案
- Key 长度限制 250 字节:且不能包含空格和控制字符
- 线程安全:每个 TCP 连接绑定一个 Worker 线程,同一连接的命令串行执行
- UDP 模式:适用于对可靠性要求不高的场景(如 Stats 协议),不建议用于存储操作
1.10 扩展阅读
- Memcached 协议规范(GitHub)
- Memcached Architecture — HighScalability
- Memcached Internals — codeproject
- libevent 官方文档
下一章: 第02章 文本协议详解 — 深入理解文本协议的格式规范与命令语法。