强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Memcached 传输协议精讲 / 第01章 Memcached 协议概述

第01章 Memcached 协议概述

理解 Memcached 的协议设计哲学,是掌握其全部能力的起点。


1.1 Memcached 简介

Memcached 由 Brad Fitzpatrick 于 2003 年为 LiveJournal 开发,如今已成为互联网基础设施中最广泛使用的缓存系统之一。其核心设计理念可以概括为:

设计原则说明
简单性协议基于文本,易于实现和调试
高性能单线程事件驱动模型(libevent),避免锁竞争
分布式客户端决定数据分布,服务端无中心协调
易失性仅用于缓存,不做持久化存储

核心定位

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   客户端    │────▶│  Memcached  │────▶│   数据库    │
│  (应用层)   │◀────│   (缓存层)  │◀────│  (持久层)   │
└─────────────┘     └─────────────┘     └─────────────┘
   先查缓存            缓存命中则返回         缓存未命中时查询
   未命中回源          未命中则穿透           查询后回写缓存

1.2 协议历史演进

Memcached 的协议经历了三个阶段的发展:

版本时间线

时间协议版本特征
2003文本协议(初版)最早的纯文本命令集
2009二进制协议(草案)Facebook 提出,更高效
2018Meta 协议(1.6+)扩展元数据,减少往返次数

各版本的动机

文本协议 诞生于 Memcached 的早期开发阶段,目标是"能在 telnet 中手动调试"。命令格式可读性极高,例如:

set mykey 0 3600 5
hello
STORED

二进制协议 的出现是为了解决文本协议的两个缺陷:

  1. 解析开销:文本解析需要字符串处理
  2. 功能局限:无法高效传递元数据

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 淘汰链表

连接处理流程

  1. 客户端 建立 TCP 连接(默认端口 11211)
  2. 主线程 接受连接,Round-Robin 分配给 Worker 线程
  3. Worker 线程 通过 libevent 事件循环处理请求
  4. 命令解析器 解析协议消息,执行对应操作
  5. 哈希表 查找或存储 item
  6. 响应 按协议格式返回结果

关键启动参数

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服务管理
Metameta 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
会话存储存储用户 Sessionset / 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 注意事项

  1. 不要将 Memcached 当作数据库使用:缓存数据随时可能丢失
  2. 单个 Value 最大 1MB:超出需自行分片或使用其他方案
  3. Key 长度限制 250 字节:且不能包含空格和控制字符
  4. 线程安全:每个 TCP 连接绑定一个 Worker 线程,同一连接的命令串行执行
  5. UDP 模式:适用于对可靠性要求不高的场景(如 Stats 协议),不建议用于存储操作

1.10 扩展阅读


下一章: 第02章 文本协议详解 — 深入理解文本协议的格式规范与命令语法。