入门
连接与基本命令
Memcached 在 11211 端口使用简单的文本协议。set 命令接收 flags(对服务器不透明的 32 位整数)、exptime(过期秒数,0 = 永不过期)和字节数。值以原始字节存储——服务器从不检查或修改它们。memcached-tool 随服务器附带,便于快速查看统计信息。
# connect to a memcached server
telnet localhost 11211
# or use the memcached-tool (ships with the server)
memcached-tool localhost:11211 stats
# set a key (flags, exptime, bytes)
set mykey 0 3600 5
hello
# get a key
get mykey
# delete a key
delete mykey协议概览
Memcached 同时支持 ASCII 文本协议(便于用 telnet/netcat 操作)和二进制协议(更高效,支持 SASL 认证和原子操作)。二进制协议使用固定 24 字节头部,大多数生产客户端优先采用。两种协议操作同一个底层键值存储。
# Two protocols available:
# 1) ASCII text protocol (human-readable, telnet-friendly)
# 2) Binary protocol (lower overhead, SASL auth, request pipelining)
# text protocol example
set foo 0 0 3
bar
# -> STORED
# binary protocol needs a client (e.g. python binary protocol):
# 0x80 (request) | opcode | keylen | extras | datatype | reserved
# bodylen | opaque | cas | extras | key | value
# both share the same in-memory data store
# check server version (text)
version
# -> VERSION 1.6.24键规则与限制
键最长 250 字节,文本协议下不能包含空格或控制字符。默认最大值大小为 1MB(用 -I 标志调大,如 -I 4m)。键不会被复制——Memcached 内联存储键,因此较长的键会消耗更多内存。使用简短、结构化的键(如 user:1001),而非冗长的描述性名称。
# Keys: max 250 bytes, no spaces or newlines (text protocol)
# Controls: max 1MB default value (configurable via -I)
# valid keys
set user:1001 0 0 5
alice
set product:sku-42 0 0 3
xyz
# INVALID keys (text protocol)
# set "with spaces" ... -> spaces not allowed
# set mykey\n ... -> newlines not allowed
# large value exceeds 1MB default
set bigkey 0 0 1048577
# -> SERVER_ERROR object too large for cache
# increase max item size at startup (-I in MB)
# memcached -m 256 -I 4m常见用例
Memcached 最适合缓存临时的、派生的数据:数据库查询结果、会话、渲染的页面片段、限流计数器、以及昂贵的计算结果。它有意保持简单——无持久化、无复制、无复杂数据类型。把它当作易失的 LRU 缓存:绝不存储无法从权威系统中重建的数据。
# 1) Database query cache
set user:1001:profile 0 600 48
{"id":1001,"name":"Alice","email":"[email protected]"}
# 2) Session storage (with TTL)
set sess:abc123 0 1800 64
{"userId":1001,"loginAt":1700000000,"role":"admin"}
# 3) Rendered HTML fragment cache
set page:home:en 0 300 128
<html><body>Hello</body></html>
# 4) Rate-limit counters
incr ratelimit:ip:1.2.3.4 1
# 5) Computed result cache (expensive aggregation)
set report:daily:20240101 0 3600 256
{"visits":1234,"signups":56}架构概览
Memcached 是由独立、无协调的服务器组成的分布式缓存。服务器本身没有集群、复制或故障转移——所有分发逻辑都位于客户端(一致性哈希)。每个节点拥有键空间的一段不相交切片;若节点宕机,其数据丢失,客户端会重新路由。这让服务器极其简单快速,代价是牺牲了持久性。
# Memcached is a pool of independent servers (no clustering built-in)
#
# +---------+ +---------+ +---------+
# | mc:11211| | mc:11211| | mc:11211|
# +---------+ +---------+ +---------+
# ^ ^ ^
# | | |
# +----+------------+-------------+----+
# | CLIENT (consistent hashing) |
# +------------------------------------+
#
# - Each server owns a disjoint slice of the keyspace
# - Sharding is done by the CLIENT (consistent hashing)
# - Servers do NOT talk to each other (no replication, no failover)
# - A server down = its keys are lost; clients reroute to the ring
# horizontal scale = add more servers to the ring
# vertical scale = bigger -m memory per server基本命令(set / get / delete)
set — 存储值
set 无条件存储值,覆盖任何已存在的键。flags 是服务器存储但从不解释的 32 位整数——客户端用它编码序列化格式(如 1 = JSON,2 = 压缩)。exptime 为 0 表示永不过期;大于 30 天的 Unix 时间戳被视为绝对时间。noreply 跳过响应,适合即发即弃写入(更快,但无错误反馈)。
# syntax: set <key> <flags> <exptime> <bytes> [noreply]\r\n<data>\r\n
# response: STORED | NOT_STORED
set greeting 0 0 5
hello
# -> STORED
# with flags (app-defined 32-bit int, e.g. mark as JSON=1)
set config 1 0 14
{"a":1,"b":2}
# -> STORED
# with expiration (3600 seconds) and noreply
set token 0 3600 8 noreply
s3cr3t00
# -> (no response)
# overwrite an existing key
set greeting 0 0 7
goodbye
# -> STOREDget — 获取值
get 在一次往返中获取一个或多个键——批量(multi-get)对性能至关重要。每个返回的 VALUE 行回显 flags,让客户端知道如何反序列化。缺失的键被静默省略(无错误)。响应以 END 结束。大型 multi-get 应分块,避免阻塞单个服务器线程。
# single key
get greeting
# VALUE greeting 0 7
# goodbye
# END
# multiple keys in one request (batched)
get user:1 user:2 user:3
# VALUE user:1 0 5
# alice
# VALUE user:3 0 3
# bob
# END (user:2 missing -> simply omitted)
# response format: VALUE <key> <flags> <bytes>\r\n<data>\r\n ... END
# flags lets the client decode (e.g. decompress / parse JSON)
# non-existent key
get nope
# END (no VALUE line at all)gets — 获取带 CAS 令牌
gets(get-extended)与 get 相同,但在每个 VALUE 行后追加 cas_unique 令牌。这个 64 位整数在每次写入时变化,是 Memcached 乐观并发(CAS)的基础。配合 cas 一起使用以避免多客户端修改同一键时的更新丢失。令牌是不透明的——切勿假设其值或跨键的单调性。
# gets returns a CAS unique identifier for optimistic concurrency
gets greeting
# VALUE greeting 0 7 12
# goodbye
# END
# ^ cas_unique = 12
# the cas_unique changes every time the key is written
set greeting 0 0 5
hello
gets greeting
# VALUE greeting 0 5 13
# hello
# END (cas_unique is now 13)
# use cas_unique with the cas command for safe updates
cas greeting 0 0 5 13
world
# -> STOREDdelete — 删除键
delete 移除单个键,返回 DELETED 或 NOT_FOUND。没有通配符或模式删除——Memcached 根本没有键枚举命令,因此批量删除必须由应用自行跟踪(在其他地方维护键集合,或使用命名空间的 flush 模式)。添加 noreply 可跳过响应。被删除的内存归还到其 slab 并被复用。
# delete a single key
delete greeting
# -> DELETED
# delete a non-existent key
delete nope
# -> NOT_FOUND
# with noreply (fire-and-forget)
delete stalekey 0 noreply
# -> (no response)
# delete does NOT support patterns or wildcards
# delete user:* <- INVALID, only deletes a key literally named "user:*"
# to bulk-delete, list keys yourself then delete each
# (memcached has no KEYS/SCAN command — track keys in your app)touch & gat — 更新过期时间
touch(1.4.8+)在不传输值的情况下更新键的过期时间——比 get+set 更省开销,适合会话刷新。gat 在一次往返中原子地获取值并更新 TTL,非常适合应延长生命周期的会话读取。gats 还返回 CAS 令牌。用它们替代 get+set,以避免两次操作之间值变化产生的竞态。
# touch: change a key's expiration without fetching the value
touch session 3600
# -> TOUCHED
touch session 0 # make it never expire
# -> TOUCHED
# gat: get-and-touch — fetch value AND update expiration in one op
gat 3600 session
# VALUE session 0 64
# {...}
# END
# gats: get-and-touch with CAS token
gats 3600 session
# VALUE session 0 64 42
# {...}
# END
# touch a missing key
touch nope 60
# -> NOT_FOUND存储命令(add / replace / append / prepend)
add — 仅在不存在时存储
add 仅当键不存在时存储值,否则返回 NOT_STORED。它是无竞态地惰性初始化键(计数器、锁)的规范方式。与 incr/decr 结合可形成安全的计数器模式:用 add 创建 为 0,再 incr。与 Redis 的 SETNX 不同,add 没有 TTL 快捷方式——在命令中设置 exptime。
# add fails if the key already exists
add newkey 0 0 5
hello
# -> STORED
add newkey 0 0 5
world
# -> NOT_STORED (key already exists)
# common pattern: initialize a counter only once
add counter:daily 0 0 1
0
# -> STORED on first run, NOT_STORED after
# then use incr to bump it
incr counter:daily 1
# -> 1replace — 仅在已存在时存储
replace 仅当键已存在时存储值——add 的镜像。它防止从过时的写入路径意外创建缓存条目。键不存在时返回 NOT_STORED。当写入应刷新现有缓存但绝不应填充新条目时(如后台刷新不应复活已被驱逐的键)使用它。
# replace fails if the key does NOT exist
replace missing 0 0 5
hello
# -> NOT_STORED (key doesn't exist)
set existing 0 0 3
foo
# -> STORED
replace existing 0 0 3
bar
# -> STORED (key existed, value updated)
# useful for updating cached data only if it was already cached
# (don't accidentally create a cache entry from a stale write)
replace cached:user:1 0 600 64
{"name":"Alice"}append — 追加到现有值
append 将数据拼接到现有值的末尾,键不存在时返回 NOT_STORED。flags 和 exptime 参数被接受但被忽略——值保留原始元数据。这非常适合就地构建日志行或缓冲区,无需读-改-写。注意:结果值仍须符合 1MB 条目限制。
# append adds data to the END of an existing value
set logline 0 0 5
hello
# -> STORED
append logline 0 0 6
world
# -> STORED
get logline
# VALUE logline 0 11
# hello world
# END
# append to a non-existent key
append nolog 0 0 3
abc
# -> NOT_STORED (key must exist)
# flags and exptime are IGNORED on append/prepend (value keeps old metadata)prepend — 前置到现有值
prepend 是 append 的镜像——它将数据拼接到现有值的开头。与 append 一样,要求键已存在并忽略 flags/exptime。适用于在缓冲值上叠加头部/前缀。append 和 prepend 不返回结果,跨客户端并非原子读-改-写,但它们是服务器端原子操作。
# prepend adds data to the START of an existing value
set greeting 0 0 5
world
# -> STORED
prepend greeting 0 0 6
hello
# -> STORED
get greeting
# VALUE greeting 0 11
# hello world
# END
# prepend to non-existent key
prepend nope 0 0 3
abc
# -> NOT_STORED
# flags/exptime ignored — only the value bytes change存储命令对比
Memcached 的六个存储命令(set、add、replace、append、prepend、cas)仅在存储前提条件上不同。set 是无条件的;add/replace 受存在性限制;append/prepend 就地修改并忽略元数据;cas 受 gets 返回的 CAS 令牌限制。选对命令可将读-改-写竞态转化为单个原子服务器操作。
# When does each command succeed?
#
# set -> ALWAYS (create or overwrite)
# add -> only if key does NOT exist
# replace -> only if key ALREADY exists
# append -> only if key exists (adds to end, ignores flags/exptime)
# prepend -> only if key exists (adds to start, ignores flags/exptime)
#
# All return STORED / NOT_STORED / EXISTS / NOT_FOUND as appropriate.
# set: overwrite unconditionally
# add: lazy init (counters, locks)
# replace: refresh only if already cached
# append: grow a buffer / log
# prepend: stack headers
# all accept [noreply] to skip the responseCAS(Compare-And-Swap)
gets — 获取 CAS 令牌
gets(get-extended)是 CAS 的读取半部分。它返回值加上 cas_unique 64 位令牌,服务器保证该令牌在键的每次修改时变化。你捕获此令牌并交给 cas;若令牌仍匹配,写入成功。没有 gets 就无法做安全的 CAS——普通 get 省略了令牌。
# gets is get + a 64-bit cas_unique token
gets counter
# VALUE counter 0 1 7
# 5
# END
# ^ cas_unique = 7
# the token is opaque and changes on EVERY write to the key
set counter 0 0 1
5
gets counter
# VALUE counter 0 1 8 <- token changed to 8
# fetch multiple keys with tokens in one call
gets a b c
# VALUE a 0 3 11
# ...
# VALUE c 0 3 19
# ...cas — 条件写入
cas 仅当 cas_unique 令牌仍与 gets 返回的匹配时写入值。STORED 表示写入已应用;EXISTS 表示另一个客户端先修改了键(令牌不匹配);NOT_FOUND 表示键已被删除。这是乐观锁——在 EXISTS 时重试整个 gets→修改→cas 循环。它避免了服务器端加锁,同时防止更新丢失。
# syntax: cas <key> <flags> <exptime> <bytes> <cas_unique> [noreply]
gets counter
# VALUE counter 0 1 8 <- token is 8
# 5
cas counter 0 0 1 8
6
# -> STORED (token matched, write applied)
# a concurrent write changed the token in the meantime
cas counter 0 0 1 8 # stale token 8, but key is now on token 9
6
# -> EXISTS (token mismatch, someone else wrote first)CAS 用例:安全计数器
当你需要读-改-写语义(如更新缓存条目内的 JSON 块)时,CAS 解决了更新丢失问题。在 EXISTS 时必须用 gets 重新读取、重新应用转换并重试 cas——通常限制重试次数以避免活锁。对于纯整数计数器,incr/decr 更简单且原子;仅当值是结构化时才用 CAS。
# read-modify-write with get+set is RACY:
# get counter -> 5 (client A)
# get counter -> 5 (client B)
# set counter 6 (A writes 6)
# set counter 6 (B writes 6, LOST A's increment!)
# safe pattern with gets/cas:
gets counter # token = 8, value = 5
newval = 5 + 1
cas counter 0 0 1 8 # 8 -> STORED, value = 6
# on EXISTS, retry the whole loop:
gets counter # token = 9, value = 6
cas counter 0 0 1 9 # -> STORED, value = 7
# for plain counters prefer incr/decr (atomic, no CAS needed)CAS 失败与重试
健壮的 CAS 循环必须处理三种结果:键缺失(用 add,在 NOT_STORED 时回退重试)、令牌匹配(STORED,完成)、令牌不匹配(EXISTS,重试)。限制重试次数以避免在高争用下活锁。如果键确实缺失,优先用 add 而非 set,这样两个竞相创建它的客户端不会互相覆盖。
# pseudocode for a CAS retry loop
max_retries = 5
for i in range(max_retries):
res = client.gets(key) # value + cas token
if res is None:
# key missing -> use add to create it race-free
if client.add(key, new_value, ttl):
break
else:
continue # someone else created it; retry
old_val, cas_token = res
new_val = transform(old_val) # your read-modify logic
if client.cas(key, new_value, ttl, cas_token):
break # STORED
# else EXISTS -> loop and retry
else:
raise Exception("CAS failed after retries")CAS 的限制
CAS 仅支持单键——Memcached 没有 Redis MULTI/EXEC 那样的多键事务。它是乐观的而非悲观的:在高争用下许多写入会命中 EXISTS 并重试,因此要对热点键分片。cas_unique 是每键且不透明的;切勿跨键比较或假设排序。若键被驱逐并重新创建,令牌会重置,因此把 NOT_FOUND 当作全新开始而非失败。
# 1) CAS only protects a SINGLE key — no multi-key transactions
# (memcached has no MULTI/EXEC like Redis)
#
# 2) CAS tokens are per-key and opaque — never compare across keys
#
# 3) CAS does NOT lock the key; it's optimistic — high contention
# means many retries (consider sharding the hot key)
#
# 4) The binary protocol exposes cas more cleanly; some ASCII quirks
# exist in older servers
#
# 5) cas_unique may RESET if a key is evicted and re-created —
# treat absence (NOT_FOUND) as a fresh start, not a token failure
# for multi-key atomicity, use app-level locks (e.g. add-based lock)计数器(incr / decr)
incr — 原子自增
incr 原子地将一个正整数加到值为十进制字符串的键上,返回新值。该操作在并发客户端间完全原子——无更新丢失。值必须是 64 位无符号数字 ASCII(无符号、无小数)。incr 对缺失键返回 NOT_FOUND;对非数字数据报错。没有自动创建——先用 add。
# syntax: incr <key> <value> [noreply]
# the key's value MUST be a numeric string (ASCII digits)
set views 0 0 1
5
# -> STORED
incr views 1
# -> 6
incr views 10
# -> 16
# returns the NEW value as a decimal string
# incrementing a non-numeric value
set name 0 0 5
alice
incr name 1
# -> CLIENT_ERROR cannot increment or decrement non-numeric value
# incr on a missing key
incr missing 1
# -> NOT_FOUNDdecr — 原子自减
decr 原子地从数字值中减去并返回结果。它在 0 处向下取整截断——永远不会产生负数,这对库存计数器很方便,但也意味着无法用 decr 跟踪净负余额。与 incr 一样,要求值为 ASCII 数字且键已存在(否则 NOT_FOUND)。
# syntax: decr <key> <value> [noreply]
set views 0 0 2
16
# -> STORED
decr views 1
# -> 15
decr views 10
# -> 5
# decr NEVER goes negative — stops at 0
decr views 100
# -> 0
# decr a missing key
decr missing 1
# -> NOT_FOUND
# decr a non-numeric value -> CLIENT_ERROR初始化计数器
由于 incr/decr 对缺失键返回 NOT_FOUND,计数器必须用 add 初始化(无竞态:第一个调用者创建为 0,其他得到 NOT_STORED 并直接跳到 incr)。切勿用 set 初始化你还要 incr 的计数器——set 无条件覆盖,会与并发自增产生竞态。按日期为计数器命名空间并设置 TTL,可让它们自动过期。
# incr/decr do NOT create keys — initialize with add first
# race-free counter init: add creates at "0" only if absent
add counter:daily 0 86400 1
0
# -> STORED (first caller wins) or NOT_STORED (already exists)
# now safe to increment from any client
incr counter:daily 1
# -> 1
# alternative: set then incr (but set+incr is racy for resets)
# For a daily counter, key by date so it auto-expires:
# counter:2024-01-01
# counter:2024-01-02计数器用例
计数器适用于限流(按 IP+分钟为键,设 60 秒 TTL)、浏览/下载统计、日活计数、库存、上线指标等。它们原子且廉价,可扩展到极高写入速率。对于限流,选择与窗口匹配的键粒度(按用户、按 IP)和 TTL——并记住 decr 在 0 截断,因此库存不会变负。
# rate limiting (per IP per minute)
add ratelimit:1.2.3.4:2024010112 0 60 1
0
incr ratelimit:1.2.3.4:2024010112 1
# if result > 100 -> reject request
# view/download counters
incr article:42:views 1
# daily active users (with a SET of user ids tracked elsewhere)
incr dau:20240101 1
# inventory / stock (decrement on purchase)
decr product:42:stock 1
# if result == 0 -> out of stock
# feature-flag rollout counters
incr flag:newui:shown 1计数器的限制
计数器仅支持无符号 64 位整数——无浮点、无负数、无符号。incr/decr 从不自动创建键。decr 在 0 处截断。因为值只是字符串,一次多余的 set/replace 会覆盖它并打乱并发自增——用 add 初始化,其余只用 incr/decr。对于浮点或多字段计数器,改用结构化值上的 CAS。
# 1) values must be unsigned 64-bit integers (no decimals, no signs)
# incr/decr on "3.14" or "-5" -> CLIENT_ERROR
#
# 2) no auto-create — must add/set first; incr on missing = NOT_FOUND
#
# 3) decr floors at 0 — cannot represent negative balances
#
# 4) the counter is just a string; set/add/replace/cas OVERWRITE it,
# which can clobber concurrent incr/decr (use add only to init)
#
# 5) for float counters or complex updates, use CAS on a JSON blob
# to RESET a counter, use set (not decr-to-zero) — but watch for races过期与 TTL
set 设置过期时间
exptime 以秒为单位:0 表示永不过期,最大到 2592000(30 天)的值是相对 TTL,更大的值被解释为绝对 Unix 时间戳。过期条目不会在精确秒数被主动回收——Memcached 使用惰性过期(访问时检查)加上后台 LRU crawler,因此过期键可能残留直到被触碰或驱逐。
# exptime is the 4th arg to all storage commands (seconds)
# 0 = no expiration (live until evicted)
set session 0 0 5 # never expire
alice
set session 0 1800 5 # expire in 1800 seconds (30 min)
alice
# exptime > 30 days (2592000 s) is treated as a UNIX timestamp
set archive 0 1735689600 5 # expires at 2025-01-01 00:00 UTC
alice
# common TTLs
# 60 -> 1 minute (rate limit windows)
# 3600 -> 1 hour (short cache)
# 86400 -> 1 day (daily cache)
# 0 -> forever (until LRU evicts)touch — 更新 TTL
touch(1.4.8+)在不传输值的情况下就地更新键的 TTL——比 get+set 远为省开销,适合滑动窗口会话。将 exptime 设为 1 可 软删除(立即过期)。同样的 30 天相对/绝对规则适用。touch 对缺失键返回 NOT_FOUND,调用者应将其视为'未缓存'。
# touch: change a key's TTL without fetching the value
# syntax: touch <key> <exptime> [noreply]
set session 0 3600 5
alice
# extend the session by another hour
touch session 3600
# -> TOUCHED
# make it never expire
touch session 0
# -> TOUCHED
# expire it immediately (poor man's delete via TTL)
touch session 1
# -> TOUCHED (effectively deleted on next access)
touch missing 60
# -> NOT_FOUNDgat & gats — 获取并更新
gat(1.6+)在一次往返中原子地组合 get 和 touch——非常适合应将过期时间向前滑动的会话读取。gats 还返回 CAS 令牌。单独使用 get+touch 会产生 TTL 未刷新的窗口并使往返翻倍;gat 同时解决这两个问题。对于多键滑动读取,gat 接受多个键。
# gat: fetch value AND update TTL in one round-trip
# syntax: gat <exptime> <key>
gat 3600 session
# VALUE session 0 5
# alice
# END
# gats: get-and-touch with CAS token
gats 3600 session
# VALUE session 0 5 42
# alice
# END
# ideal for session reads that should refresh the TTL:
# - one network round-trip instead of get + touch
# - atomic: no window where TTL isn't refreshed
# multiple keys: gat <exptime> <key1> <key2> ...
gat 3600 a b c过期行为
Memcached 的过期默认是惰性的——键仅在被访问时回收,这意味着过期数据可能占用内存直到被驱逐。LRU crawler(1.4.24+)增加了后台扫描,逐 slab 主动释放过期条目,防止内存浪费。在生产环境启用它:lru_crawler enable。即便如此,切勿依赖精确的过期时间来保证正确性。
# Memcached does NOT actively scan for expired keys every second.
# Expiration is LAZY: a key is checked when accessed (get/gets/touch).
#
# A background "LRU crawler" (1.4.24+) periodically sweeps slabs to
# free expired items so memory is reclaimed even if never accessed.
#
# stats lru_crawler # is the crawler enabled?
# lru_crawler enable # turn it on
# lru_crawler sleep 100 # microseconds between slab visits
# lru_crawler tocrawl 1000 # max items to inspect per slab per pass
# an expired-but-not-yet-reaped key:
get expiredkey
# -> END (treated as missing, memory freed for its slab)TTL 最佳实践
始终设置 TTL——无界缓存条目会导致无界内存增长和过时数据 bug。给 TTL 加随机抖动(±5–10%),避免相关过期同时发生踩踏源头。按日期为易失数据命名空间,让旧条目自动过期。在未命中时,在同一代 码路径中重新计算并存储以保持缓存温热。用 delete 而非 TTL=1 做显式失效。
# 1) ALWAYS set a TTL on cache entries — never store forever (0)
# unless you have an explicit invalidation path.
#
# 2) add jitter to avoid thundering herds:
ttl = base_ttl + random(0, 300) # +/- 5 minutes
#
# 3) namespace by time so old keys auto-expire:
set report:2024-01-01 0 86400 N # yesterday's report, 1-day TTL
#
# 4) use longer TTLs for stable data, shorter for volatile data
#
# 5) on a cache miss, recompute AND set with TTL in ONE path
#
# 6) don't use exptime=1 as a "delete" in production — use delete统计命令
stats — 概览
stats 输出核心服务器指标:cmd_get/cmd_set(总操作数)、get_hits/get_misses(命中率 = hits/cmd_get)、evictions(因内存压力丢弃的条目)、curr_items、bytes 与 limit_maxbytes、以及连接数。命中率和 evictions 是首先要看的两个数字。这是任何 Memcached 监控的基础。
# general server statistics
stats
# STAT pid 1234
# STAT uptime 3600
# STAT curr_connections 10
# STAT total_connections 1024
# STAT cmd_get 50000
# STAT cmd_set 12000
# STAT get_hits 45000
# STAT get_misses 5000
# STAT evictions 12
# STAT bytes 8388608
# STAT limit_maxbytes 67108864
# STAT curr_items 4321
# ...
# END
# hit rate = get_hits / cmd_get (here 90%)
# watch evictions — rising = memory pressurestats items — 按 slab 统计条目数
stats items 报告每个 slab 类持有的条目数(number)和其最久未存储条目的年龄(age)。number 高但 age 低的 slab 正在被频繁更替;age 高的 slab 持有长生命周期数据。这是调优 chunk 大小或诊断某个尺寸范围为何被驱逐时首先要看的地方。
# item counts and age per slab class
stats items
# STAT items:1:number 100
# STAT items:1:age 1200
# STAT items:2:number 250
# STAT items:2:age 3600
# STAT items:5:number 3
# STAT items:5:age 60
# ...
# END
# items:<slabid>:number -> how many items live in that slab class
# items:<slabid>:age -> age of the LRU item (seconds since stored)
# use this to see which size classes are full / hotstats slabs — Slab 分配器状态
stats slabs 揭示 slab 分配器的布局:每个类有 chunk_size(条目槽向上取整到它)、chunks_per_page、total_pages 和已用/空闲 chunk。分配给某 slab 类的内存留在那里——不能归还给其他类。如果一个类满了且在驱逐而另一个空闲,你遇到了 slab 钙化,可通过调优 -f(增长因子)解决。
# memory layout of each slab class
stats slabs
# STAT 1:chunk_size 96
# STAT 1:chunks_per_page 10922
# STAT 1:total_pages 1
# STAT 1:total_chunks 10922
# STAT 1:used_chunks 100
# STAT 1:free_chunks 10822
# STAT 2:chunk_size 120
# ...
# STAT active_slabs 5
# STAT total_malloced 16777216
# END
# chunk_size -> bytes per item slot in this class
# total_chunks -> total slots
# used_chunks -> slots holding data
# free_chunks -> empty slotsstats sizes — 条目大小直方图
stats sizes 构建真实条目大小(键+值+开销)的直方图,让你看到有多少内存浪费在 chunk 大小取整上。它执行全量扫描并历史上会锁住服务器——现代版本在安全模式检查后启用;传 --disable-safe-mode 强制运行。仅在低流量时运行。输出有助于选择正确的 -f 增长因子。
# histogram of actual item sizes (key + value + overhead)
# WARNING: this command locks the cache while scanning — do NOT run
# in production at peak. Use stats sizes --disable-safe-mode only
# if you accept the risk.
stats sizes
# STAT 96 100 # 100 items rounded up to 96 bytes
# STAT 120 250
# STAT 192 50
# ...
# END
# compare to stats slabs to see how much memory is wasted to
# chunk-size rounding (internal fragmentation)stats cachedump — 检查 slab
stats cachedump 列出给定 slab 类中持有的键(限制 N 条),适合临时调试。它遍历 slab 的 LRU,可能很慢或在某些构建中完全禁用——切勿用于生产键枚举。Memcached 刻意不提供高效列出所有键的方法;把它当作由应用驱动的黑盒。
# peek at keys in a specific slab class
# syntax: stats cachedump <slab_id> <limit> [noreply]
stats cachedump 1 10
# ITEM user:1 [5 b; 1700000000 s]
# ITEM user:2 [5 b; 1700000000 s]
# ...
# END
# WARNING: for debugging only — it walks the LRU and can be slow.
# Not all builds ship cachedump; it is disabled in some distros.
# Each ITEM shows: key, [value size in bytes; storage time s]
# do NOT use this for key enumeration in production code —
# memcached has no efficient SCAN commandstats reset 与其他子命令
stats reset 将累计计数器(操作、命中、未命中)清零而不丢数据——便于在部署后测量全新时间间隔。其他有用子命令:stats settings(生效配置)、stats conns(较新构建的每连接详情)、stats lru_crawler(crawler 状态)。计数器否则自进程启动累计,因此基准测试前先 reset。
# zero the cumulative counters (cmd_get, hits, misses, etc.)
stats reset
# -> RESET
# useful stats subcommands:
stats settings # current effective configuration
stats items # per-slab item counts/age
stats slabs # slab allocator layout
stats sizes # item size histogram (slow!)
stats conns # per-connection info (newer builds)
stats lru_crawler # crawler state
# reset counters after a deploy to measure only current run
# (counters are since process start otherwise)设置与配置
stats settings — 生效配置
stats settings 显示运行配置:maxbytes(-m)、maxconns(-c)、growth_factor(-f)、item_size_max(-I)、是否启用 evictions 和 CAS、以及 LRU crawler 状态。大多数在启动时固定;只有少数可在运行时更改。这是确认服务器是否拾取了你认为的标志的首要地方。
# view the server's current effective settings
stats settings
# STAT maxbytes 67108864
# STAT maxconns 1024
# STAT tcpport 11211
# STAT udpport 0
# STAT inter NULL
# STAT evictions on
# STAT cas_enabled on
# STAT chunk_size 48
# STAT growth_factor 1.25
# STAT item_size_max 1048576
# STAT lru_crawler on
# ...
# END
# this reflects flags passed at startup (-m, -c, -f, -I, ...)启动参数
大多数配置通过启动标志完成。关键的:-m(内存)、-l(始终绑定到 127.0.0.1 或私有网络——切勿公开暴露 Memcached)、-c(连接上限)、-I(最大条目大小)、-f(slab 增长因子)。-M 将驱逐行为翻转为满时出错而非 LRU 驱逐(缓存很少需要)。将 -t 调到 CPU 数以提升吞吐。
# common memcached startup flags
memcached -d -m 256 -p 11211 -u memcache -c 2048 -f 1.25 -I 1m
# -d run as a daemon
# -m <MB> max memory to use for items (default 64)
# -M return error instead of evicting when memory full
# -c <conn> max simultaneous connections (default 1024)
# -p <port> TCP port (default 11211)
# -U <port> UDP port (0 disables; default 11211)
# -l <addr> listen address (default all — bind to 127.0.0.1!)
# -u <user> run as this user (when started as root)
# -f <factor> slab growth factor (default 1.25)
# -n <bytes> min item allocation (default 48)
# -I <size> max item size (default 1m, can be 1m-1g)
# -t <threads> worker threads (default 4)
# -v / -vv / -vvv verbosity运行时配置
Memcached 仅暴露少数运行时旋钮:cache_memlimit(实时调整内存上限)、lru_crawler 系列(启用/禁用/调优后台回收器)、verbosity(日志级别)、flush_all(使所有条目失效,可延迟)。大多数设置在启动时固定——要改 -m、-c 或 -I 需重启进程。flush_all 是控制面操作,不是设置。
# a small number of settings can change at runtime:
# change the memory limit (bytes) live
cache_memlimit 536870912 # 512 MB
# -> OK
# LRU crawler controls
lru_crawler enable # turn the crawler on
lru_crawler disable # turn it off
lru_crawler sleep 100 # us between slab visits
lru_crawler tocrawl 1000 # max items per slab per pass
lru_crawler metadump all # dump key metadata (not values)
# log verbosity at runtime
verbosity 2
# flush_all (invalidate everything, not a config change)
flush_all
flush_all 900 # invalidate in 900 seconds重要设置详解
-m 是头条设置:按工作集加余量来设定,因为过小会导致驱逐和缓存更替。-f 在 slab 类粒度与内部碎片间权衡——当有大量尺寸相近的条目时调低(向 1.1)。-I 限制条目大小;仅在必须缓存大 blob 时调高。-M 很少用于缓存——它把 OOM 变成错误而非驱逐。
# -m memory budget
# the single most important knob. Size to fit your working set
# with headroom; undersizing = evictions = cache churn.
#
# -f slab growth factor (default 1.25)
# smaller (1.1) = more slab classes, less internal fragmentation
# but more per-class memory stuck. Larger (1.5) = fewer classes,
# more waste per chunk.
#
# -I max item size (default 1m)
# raise to cache bigger blobs (e.g. -I 4m) at the cost of larger
# worst-case allocations.
#
# -M disable LRU eviction (error instead) — only for strict caches
#
# -t worker threads (default 4) — scale to CPU count for throughput连接限制与线程
-c 限制并发连接——设得舒适地高于(池大小 × 应用实例),否则客户端会被拒(在 stats 中看 listen_disabled_num)。-t 设置工作线程;Memcached 每线程事件驱动,因此 -t 调到 CPU 核数,通常 4–8。超过后锁争用限制收益。每个线程拥有哈希表的一片,因此更多线程对单个热点键无帮助。
# -c max client connections (default 1024)
memcached -c 4096
# each connection costs a bit of memory; set -c above your expected
# peak concurrent clients (pool size * app instances + headroom)
# -t worker threads (default 4)
memcached -t 8
# memcached is event-driven per thread; each thread runs its own
# event loop and owns a slice of the hash table. Scale -t to CPU
# cores. Beyond ~8 threads, contention often limits gains.
# check connection pressure:
stats
# STAT curr_connections 412
# STAT total_connections 10230
# STAT listen_disabled_num 0 # >0 means you hit -c and refused conns缓存策略
Cache-Aside(懒加载)
Cache-aside 是最简单也最流行的模式:应用先读缓存,未命中时从数据库加载并回填缓存。它具弹性(缓存故障只是意味着更多数据库负载),但允许冷键踩踏和直到显式失效前的过时。即使在写时失效也要设置 TTL,作为漏掉失效的安全网。
# the most common pattern — app manages the cache explicitly
def get_user(id):
key = "user:" + str(id)
val = mc.get(key) # 1) try cache
if val is not None:
return val # hit
val = db.query(id) # 2) miss -> load from DB
mc.set(key, val, ttl=3600) # 3) back-fill the cache
return val
# on write:
def update_user(id, data):
db.update(id, data)
mc.delete("user:" + str(id)) # invalidate (or set fresh value)
# pros: simple, resilient to cache failure
# cons: stampede on miss; stale until invalidatedRead-Through(读穿透)
在 read-through 中,缓存层本身在未命中时调用加载器——应用只调用 get 并得到值。这集中了缓存填充逻辑并保证跨调用方一致,代价是在缓存调用内有同步的数据库读取。大多数 Memcached 客户端不内置 read-through,因此通常实现为 cache-aside 的薄封装。
# the cache itself is responsible for loading from the DB
# (the client library or a cache layer provides a loader callback)
mc = memcached_with_loader(load_fn=db.query_user)
def get_user(id):
# the cache calls load_fn(id) automatically on a miss
return mc.get_or_load("user:" + str(id))
# pros:
# - app code is simpler (no miss-handling logic)
# - cache population is consistent across callers
# cons:
# - the cache library must support a loader (or you wrap it)
# - a cache miss is slower (synchronous DB read inside get)Write-Through(写穿透)
Write-through 在每次写入时更新缓存,保持新鲜且无需单独失效路径。代价是写入延迟(缓存+数据库)和失败顺序风险:若先写缓存而数据库写入失败,缓存持有非持久数据;先写数据库则有过时窗口。大多数应用对热键用 write-through,其余用 cache-aside。
# every write goes to the cache AND the DB synchronously
def update_user(id, data):
mc.set("user:" + str(id), data, ttl=3600) # cache first
db.update(id, data) # then DB
# or DB first, then cache — pick an order and stick with it
# pros:
# - cache stays fresh after writes (no stale reads)
# - no invalidation logic needed
# cons:
# - write latency = cache write + DB write
# - if the DB write fails after the cache write, cache is inconsistent
# - cache holds data even if never read (wastes memory)Write-Behind(写回)
Write-behind 立即写入缓存并通过队列异步持久化到数据库——提供亚毫秒写入延迟和尖峰吸收。代价是一致性(数据库滞后于缓存)和持久性(worker 排空前崩溃会丢数据)。Memcached 单独做不到;你需要队列+worker。保留给写密集、容忍丢失的工作负载,如计数器和遥测。
# writes go to the cache first; DB is updated ASYNCHRONOUSLY later
# (memcached alone can't do this — it's a cache, not a queue.
# this pattern needs a worker/queue alongside it.)
def update_user(id, data):
mc.set("user:" + str(id), data, ttl=3600)
queue.push({"op":"update","id":id,"data":data}) # async DB write
# a background worker drains the queue to the DB
# pros:
# - very fast writes (cache-only latency)
# - absorbs write spikes
# cons:
# - DB lags behind cache -> reads may see newer data than DB
# - data loss if cache evicts/evacuates before the worker runs
# - complex (queue + worker + reconciliation)缓存失效
失效是缓存最难的部分。写时显式 delete 是基线;TTL 是安全网(始终设一个)。对于无法枚举的集合,使用版本化键(user:1:v42),在模式/数据变化时提升版本,让旧条目通过 TTL 老化。按时间分桶的键(report:2024-01-01)按日期自动过期。切勿尝试通配失效——Memcached 无法列出键。
# invalidation is HARD — "there are only two hard problems in CS"
# 1) explicit invalidation on write
def update_user(id, data):
db.update(id, data)
mc.delete("user:" + str(id)) # drop stale entry
# 2) versioned keys to avoid invalidation entirely
key = "user:" + str(id) + ":v" + str(version)
# bump version on update -> old keys auto-expire via TTL
# 3) TTL as a safety net — even with explicit invalidation, set a TTL
# so a missed invalidation self-heals within (e.g.) an hour
# 4) namespacing by time/date for time-bucketed data
key = "report:" + today_date() # yesterday auto-expires
# anti-pattern: trying to invalidate "all user:* keys" —
# memcached can't enumerate keys, so use a versioned namespace insteadTTL 与驱逐策略
按数据易变性匹配 TTL:参考数据用小时级,用户数据用分钟级,限流计数器用窗口长度。始终加抖动(±5–10%)避免同步过期踩踏。Memcached 在内存压力下按 slab 做 LRU 驱逐,无每条目固定——唯一杠杆是 TTL 和总量规划。对热键,在未命中时用基于 add 的锁,让仅一个客户端重算。
# choose TTL by data volatility:
# reference data (rarely changes) -> 1-24 hours
# user data (changes on action) -> 5-60 minutes
# computed reports -> matches refresh cadence
# sessions -> matches idle timeout
# rate-limit counters -> matches the window (60s)
# add jitter to prevent synchronized expiry / stampedes:
ttl = base + random.randint(-60, 60)
# on high memory pressure, Memcached evicts LRU items per slab.
# to AVOID eviction of critical data, give it a longer TTL OR
# store it under a key pattern you can pin (memcached has no
# explicit "no-evict" flag — TTL + sizing are your tools).
# stampede protection: lock-and-recompute on miss
if mc.add("lock:user:1", "1", ttl=30):
val = db.query(1); mc.set("user:1", val, 3600); mc.delete("lock:user:1")
else:
val = mc.get("user:1") # another client is refreshing; retry shortly客户端库
Python(pymemcache)
pymemcache 是纯 Python、快速且显式的。单服务器用普通 Client,集群用 HashClient(一致性哈希)。set/get 是基础;get_many 批量 multi-get。计数器用 add+incr,乐观更新用 gets/cas。始终传显式超时——死亡节点应快速失败,而非挂住你的请求线程。
# pip install pymemcache
from pymemcache.client.base import Client
mc = Client(("127.0.0.1", 11211), connect_timeout=2, timeout=1)
mc.set("user:1", "alice", expire=3600)
print(mc.get("user:1")) # b"alice"
# multi-get (batched)
for k, v in mc.get_many(["user:1", "user:2", "user:3"]).items():
print(k, v)
# counter
mc.add("views", 0)
mc.incr("views", 1)
# CAS loop
while True:
val, cas = mc.gets("counter")
if mc.cas("counter", int(val) + 1, cas, expire=0):
break
# use a consistent-hashing cluster
from pymemcache.client.hash import HashClient
cluster = HashClient([("mc1", 11211), ("mc2", 11211), ("mc3", 11211)])Node.js(memcached)
memcached npm 包支持单服务器和多服务器(一致性哈希)设置、连接池和重试。set/get/getMulti(批量 multi-get)覆盖常见路径。按并发配置 poolSize 并始终设超时,让死亡节点快速失败。该库是回调风格;用 Promise 封装或 util.promisify 获得async/await 体验。
// npm install memcached
const Memcached = require("memcached");
const mc = new Memcached("127.0.0.1:11211", {
timeout: 1000,
retries: 1,
poolSize: 10,
});
mc.set("user:1", "alice", 3600, (err) => {
if (err) console.error(err);
});
mc.get("user:1", (err, data) => {
console.log(data); // "alice"
});
// multi-get
mc.getMulti(["user:1", "user:2"], (err, data) => {
console.log(data["user:1"]);
});
// counter
mc.incr("views", 1, (err, val) => console.log(val));
// cluster: pass a comma-separated list, client does consistent hashing
const cluster = new Memcached("mc1:11211,mc2:11211,mc3:11211");
mc.end(); // close connections on shutdownPHP(memcached 扩展)
PHP 的 memcached 扩展(基于 libmemcached)是生产之选——设 OPT_LIBKETAMA_COMPATIBLE 实现跨服务器一致性哈希,用 addServers 做集群。getMulti 批量读取。cas() 接受 fetch() 结果数组中的令牌。避免旧的 memcache 扩展(无 'd');它功能少且维护差。始终设 TTL 并干净处理 Memcached::RES_NOTFOUND。
<?php
// requires the memcached extension (libmemcached-based)
$mc = new Memcached();
$mc->addServer("127.0.0.1", 11211);
$mc->set("user:1", "alice", 3600);
echo $mc->get("user:1"); // alice
// multi-get returns associative array
$rows = $mc->getMulti(["user:1", "user:2", "user:3"]);
// counter
$mc->set("views", 0);
$mc->increment("views", 1);
// CAS
$mc->getDelayed(["counter"], false, null);
while ($item = $mc->fetch()) {
$cas = $item["cas"];
$mc->cas($cas, "counter", intval($item["value"]) + 1);
}
// cluster with consistent hashing
$mc->setOption(Memcached::OPT_LIBKETAMA_COMPATIBLE, true);
$mc->addServers([
["mc1", 11211],
["mc2", 11211],
["mc3", 11211],
]);Ruby(dalli)
Dalli 是 Ruby 的标准 Memcached 客户端——纯 Ruby、线程安全,是 Rails 默认缓存存储。传服务器数组做一致性哈希(ketama)集群。它的 cas 块形式在冲突时自动重读并重试,比手动循环友好得多。全局配置 expires_in 并按调用覆盖以获得更紧的 TTL。
# gem install dalli
require "dalli"
# single server
mc = Dalli::Client.new("127.0.0.1:11211", { expires_in: 3600 })
mc.set("user:1", "alice")
mc.get("user:1") # => "alice"
# multi-get (returns a hash)
mc.get_multi(["user:1", "user:2", "user:3"])
# counter
mc.incr("views", 1)
mc.decr("views", 1)
# CAS
mc.get("counter") do |val, cas|
mc.cas("counter", cas) { val.to_i + 1 }
end
# cluster with consistent hashing (ketama)
cluster = Dalli::Client.new(["mc1:11211", "mc2:11211", "mc3:11211"],
{ expires_in: 3600 })Java(spymemcached)
spymemcached 是高性能、异步、基于 NIO 的 Java 客户端。其 API 基于 Future;需要确认时 get() 阻塞。传空格分隔的服务器列表给 AddrUtil 做一致性哈希。gets()/cas() 提供乐观并发,getBulk() 做批量 multi-get。始终在应用关闭时 shutdown() 以释放 IO 线程。
// build.gradle: implementation 'net.spy:spymemcached:2.12.3'
import net.spy.memcached.MemcachedClient;
import net.spy.memcached.AddrUtil;
MemcachedClient mc = new MemcachedClient(
AddrUtil.getAddresses("mc1:11211 mc2:11211 mc3:11211"));
// async API returns java.util.concurrent.Future
Future<Boolean> f = mc.set("user:1", 3600, "alice");
f.get(); // block for confirmation
Object val = mc.get("user:1"); // "alice"
// counter
mc.incr("views", 1);
// CAS
CASValue<Object> cv = mc.gets("counter");
CASResponse r = mc.cas("counter", cv.getCas(), newVal);
if (r == CASResponse.OK) { /* success */ }
// bulk get
Map<String, Object> rows = mc.getBulk("user:1", "user:2", "user:3");
mc.shutdown();Go(gomemcache)
gomemcache(bradfitz)是事实上的 Go 客户端——简单、快速,支持跨多服务器一致性哈希。Set/Get 接受一个捆绑键、值、TTL 和 CAS 令牌的 Item 结构体;CompareAndSwap 做乐观更新。GetMulti 返回按名称键控的 map(缺失键是缺失而非错误)。它是并发安全的;跨 goroutine 复用同一客户端。
// go get github.com/bradfitz/gomemcache/memcache
import memcache "github.com/bradfitz/gomemcache/memcache"
mc := memcache.New("mc1:11211", "mc2:11211", "mc3:11211")
// set
err := mc.Set(&memcache.Item{
Key: "user:1", Value: []byte("alice"), Expiration: 3600,
})
// get
it, err := mc.Get("user:1")
// it.Value -> []byte("alice")
// multi-get (returns map, missing keys absent)
rows, _ := mc.GetMulti([]string{"user:1", "user:2"})
// counter
mc.Increment("views", 1)
mc.Decrement("views", 1)
// CAS
it, _ = mc.Get("counter")
it.Value = []byte("6")
it.Cas = it.Cas // use existing token
mc.CompareAndSwap(it)
// delete
mc.Delete("user:1")分布式架构
一致性哈希
一致性哈希将服务器和键都映射到环上;键由顺时针方向的下一个服务器拥有。添加或移除服务器只移动该服务器的键切片(约 1/N),而非整个键空间——远优于 hash(key) % N(后者在成员变更时几乎全部重映射)。大多数客户端使用 ketama 变体,每服务器多个虚拟节点以均衡分布。
# consistent hashing spreads keys across servers with minimal
# remapping when the pool changes.
#
# 1) hash each server onto a ring (multiple virtual nodes each)
# 2) hash the key, walk the ring clockwise to the next server
#
# ring: s1---s2---s3---s1
# ^ ^
# keyA keyB -> goes to s3
#
# adding s4 only moves keys between s3 and s4 (not the whole ring)
# removing s1 only moves keys between s1's predecessor and s2
#
# result: |keys moved| ~= keys/N when adding/removing one server
# (vs. ~all keys remapped with hash(key) % N)
# most clients implement ketama consistent hashing automatically
# when you pass a list of servers客户端分片
所有分片都在客户端:它将每个键哈希到一个服务器,并并行地跨服务器展开 multi-get。每个键恰好存在于一个节点——没有复制,因此死亡节点丢失其键空间切片。没有代理或协调者;每个客户端必须共享相同的服务器列表和哈希配置(ketama),否则键会落到不同节点并导致大量未命中。
# memcached servers do NOT coordinate — sharding is 100% client-side.
#
# the client picks ONE server per key:
# server = ring[hash(key)]
#
# a multi-get is split across servers in parallel:
# getMulti(["a","b","c"])
# -> get "a" from mc1
# -> get "b" from mc2 (concurrent, fan-out)
# -> get "c" from mc1
#
# this means:
# - each key lives on exactly ONE server (no replication)
# - a multi-get touches multiple servers but returns one result set
# - the client must know the full server list (no proxy)
# never put a replica behind the same client as a primary for the
# same key range — memcached clients do not read-replicate哈希算法(Ketama)
Ketama 是 Memcached 事实上一致性哈希方案:每台物理服务器作为多个虚拟节点(约 160 个)映射到环上以均衡负载。定位键时哈希它并顺时针走到下一个节点。关键陷阱:每种语言的每个客户端必须使用相同算法和一致的服务器标签字符串,否则它们会对所有权不一致并在部署后导致大量缓存未命中。
# ketama is the de-facto consistent-hashing algorithm for memcached.
#
# for each server, create N virtual nodes (default ~160-200):
# for i in range(vnodes):
# point = hash("server:port-" + i)
# ring[point] = server
#
# to locate a key:
# point = hash(key)
# server = first node clockwise from point on the ring
#
# benefits of many virtual nodes:
# - balanced load (no single server gets a huge arc)
# - smooth redistribution on add/remove
#
# IMPORTANT: all clients must use the SAME algorithm + server labels,
# or they will disagree on which server owns a key.
# enable in clients:
# PHP: Memcached::OPT_LIBKETAMA_COMPATIBLE
# Ruby: default in dalli
# Python: HashClient (pymemcache)添加与移除服务器
使用一致性哈希时,添加或移除服务器只重映射约 1/N 的键——远优于模运算哈希(几乎全部重映射)。但服务器崩溃仍永久丢失其键(无复制),导致源头负载瞬时飙升。同时向所有客户端推出成员变更,并考虑在正式上线前通过影子读取预热新节点。
# adding a server to the ring:
# - only ~1/N of keys remap (the slice the new server now owns)
# - those keys are cold misses until repopulated -> brief spike
#
# removing a server (e.g. crash):
# - that server's keys are GONE (no replication)
# - its slice of the ring is reassigned to neighbors
# - all reads to those keys miss until repopulated
#
# best practices for membership changes:
# 1) deploy new server list to all clients simultaneously
# 2) warm a new server with a shadow-read phase if possible
# 3) expect a transient increase in DB load (cache misses)
# 4) use weighted virtual nodes to add capacity gradually
# anti-pattern: hash(key) % N -> changes ALL key placements on
# every membership change (catastrophic for cache hit rate)复制与故障转移
Memcached 没有内置复制或故障转移——死亡节点丢失其数据,客户端重新路由。要增加冗余,要么在客户端复制(写 N 个节点,成本更高),要么运行 mcrouter/twemproxy 之类的代理,要么接受丢失并从源头重建。惯用选择是最后一种:把缓存当作一次性的,把池规模设到一个节点丢失可承受,让未命中从数据库回填。
# memcached has NO built-in replication or failover.
# if a node dies, its data is gone — clients reroute to the ring.
#
# to ADD replication, options:
#
# 1) client-side replication: write to N servers per key
# - the client writes to primary + replica(s) on each set
# - reads try primary, fall back to replica
# - trade-off: N x write traffic + memory, weaker consistency
#
# 2) repcached: a memcached fork with master/replica replication
# - largely unmaintained today; avoid for new projects
#
# 3) use a layer above: twemproxy / mcrouter / mc-router
# - proxy that handles sharding, pooling, failover
#
# 4) accept the loss: cache is volatile by design — rebuild from DB
# the "memcached way" is usually option 4: don't replicate, just
# size the pool so a single node loss is survivable内存管理(Slabs / LRU)
Slab 分配器
Memcached 使用 slab 分配器:内存被划分为固定大小 chunk 的类,条目存储在能容纳它的最小 chunk 中(向上取整)。这避免了 malloc/free 碎片并给出 O(1) 分配,代价是内部碎片(chunk 内浪费空间)和类钙化(内存不能在类间移动)。这就是为什么值尺寸的均匀混合对效率很重要。
# memcached does NOT use malloc/free per item (that fragments).
# instead it pre-allocates fixed-size "slab classes" of chunks.
#
# slab class 1: chunks of 96 bytes
# slab class 2: chunks of 120 bytes
# slab class 3: chunks of 152 bytes (grow by -f factor)
# ...
#
# a 50-byte value -> rounded up to 96-byte chunk (class 1)
# a 100-byte value -> rounded up to 120-byte chunk (class 2)
#
# trade-offs:
# + no per-item malloc overhead, no heap fragmentation
# + O(1) alloc/free
# - internal fragmentation (wasted bytes inside a chunk)
# - memory is stuck per class (can't move between classes)
# inspect with:
stats slabsSlab 类与增长因子
Slab 类尺寸从 -n 最小值(默认 48 字节)开始,按 -f 因子(默认 1.25)增长到 -I 最大条目大小。较低的 -f 产生更多更细粒度的类(浪费少,钙化风险高);较高的 -f 产生更少更粗的类(浪费多)。用 stats sizes 检查真实值尺寸直方图,调 -f 让常见尺寸落到填充良好的类上。
# slab class sizes start at -n (default 48 bytes) and grow by -f:
#
# class 1: 48 bytes (base, -n)
# class 2: 48 * 1.25 = 60
# class 3: 60 * 1.25 = 75 -> 80
# class 4: 80 * 1.25 = 100
# ...
# up to the max item size (-I, default 1MB)
#
# -f default 1.25 balances class count vs. waste.
# lower (1.1) -> more classes, less waste, but more stuck memory
# higher (1.5) -> fewer classes, more waste per chunk
#
# tune -f to your value-size distribution (check stats sizes)
# example: many small JSON values -> -f 1.1 -n 80
memcached -m 512 -f 1.1 -n 80LRU 驱逐
驱逐按 slab 类进行,在该类内严格 LRU:当类满时,该类中最久未使用的条目被丢弃以腾出空间。有空闲 chunk 的类从不驱逐,因此不平衡的尺寸分布可能导致一个类重度驱逐而另一个空闲——经典的 slab 钙化问题。在 stats 中观察 evictions 和 evicted_active 以发现它。
# when a slab class is full and a new item arrives in that class,
# memcached evicts the LEAST RECENTLY USED item in that class.
#
# eviction is PER-SLAB-CLASS, not global:
# - a full class-1 (96B) evicts class-1 items only
# - a class with free chunks never evicts
#
# this means an unbalanced workload can evict from one class
# while another class sits idle with free chunks.
#
# check eviction pressure:
stats
# STAT evictions 1234 # total items ever evicted
# STAT evicted_active 12 # evicted even though recently used (1.6+)
# STAT evicted_unfetched 50 # evicted before ever read
stats items
# STAT items:1:evicted 500 # evictions in slab class 1
# STAT items:1:outofmemory 0内存限制(-m)
-m 以 MB 限制条目内存;一旦达到,Memcached 停止添加 slab 页并在类内驱逐 LRU。-M 将策略改为满时出错而非驱逐(缓存很少需要)。cache_memlimit 实时调整上限。按工作集加余量设定 -m——100% 运行意味着持续驱逐和低命中率的更替缓存。
# -m sets the max memory for items (in MB)
memcached -m 64 # default: 64 MB
memcached -m 4096 # 4 GB
# once -m is reached, memcached stops allocating new slab pages and
# relies on LRU eviction within each class.
#
# -M flips behavior: instead of evicting, return an ERROR on writes
# when memory is full (rarely what you want for a cache).
memcached -m 512 -M
# resize the memory cap at runtime (bytes):
cache_memlimit 2147483648 # 2 GB
# -> OK (only grows/shrinks the page pool; doesn't drop items
# unless shrinking forces eviction)
# monitor:
stats
# STAT bytes 424463360 # current bytes in use
# STAT limit_maxbytes 536870912 # the -m limit驱逐策略与调优
Memcached 唯一的驱逐策略是按 slab 的 LRU——没有 LFU 或随机选项。要减少驱逐:调大 -m、缩短 TTL、重平衡值尺寸、或调 -f。现代构建增加了 LRU crawler 和重新分配 slab 页的能力(slabs reassign / slabs automove)以缓解钙化。用 stats items 的 evicted/outofmemory 计数器检测热点类。
# memcached's only eviction policy is LRU (per slab class).
# (Redis offers LRU/LFU/random/TTL; memcached does not.)
#
# 1.6+ adds an LRU crawler + "temporary" vs "permanent" LRU tiers:
# lru_crawler enable
# lru_crawler tocrawl 1000
#
# to reduce evictions:
# - increase -m (more memory)
# - shorten TTLs so items self-expire before LRU drops them
# - rebalance value sizes (avoid one hot slab class)
# - tune -f so popular sizes map to well-populated classes
#
# detect a hot/evicting class:
stats items
# STAT items:5:evicted 9999 <- class 5 is under pressure
# STAT items:5:outofmemory 0
# rebalance slab memory automatically (1.5+):
slabs reassign 5 1 # move a page from class 5 to class 1
slabs automove 1 # let the server auto-rebalance (cautious mode)Slab 自动迁移与重分配
slabs reassign(手动)和 slabs automove(1.5+,自动)让你把 slab 页从有空闲 chunk 的类重平衡到驱逐压力下的类——slab 钙化的解药。automove=1 是谨慎的后台搬运器,生产安全;=2 激进且实验性。始终在前后用 stats slabs 验证——错误的 reassign 可能饿死一个类。
# 1.5+ lets you move a slab PAGE from a class with free chunks
# to a class under memory pressure.
#
# manual one-shot move:
slabs reassign 1 5 # take a page from class 1, give to class 5
# -> DONE (or BUSY if the page is being freed)
# automatic rebalancing (runs in the background):
slabs automove 0 # off (default)
slabs automove 1 # cautious auto-move (slow, safe)
slabs automove 2 # aggressive (experimental; watch carefully)
# the automover watches evictions/free chunks and slowly shifts
# pages to relieve pressure. It's conservative by design.
# check current slab state:
stats slabs
# STAT 1:total_pages 4
# STAT 1:free_chunks 0 <- class 1 is full
# STAT 5:total_pages 12
# STAT 5:free_chunks 8000 <- class 5 has room to give性能优化
noreply 与管道
noreply 砍掉每次写入的响应,管道将多个命令批量到一次网络往返——两者合起来是批量写入最大的吞吐杠杆。multi-get 对读取做同样的事。noreply 的代价是丢失每命令错误反馈,因此保留给非关键、高容量写入,在必须暴露失败时保留响应。
# noreply skips the server's response for write commands.
# use it for fire-and-forget writes to cut latency & network:
set k1 0 0 5 noreply
hello
set k2 0 0 5 noreply
world
# (no STORED echoed back)
# pipelining: send many commands in one batch without waiting for
# each response (binary protocol and most clients support this).
# ideal for bulk inserts/refreshes.
# trade-off:
# + fewer round-trips, higher throughput
# - no error feedback per command (use sparingly for critical writes)
# multi-get is the read-side equivalent: one request, N keys
get user:1 user:2 user:3 user:4二进制协议
二进制协议比 ASCII 更高效——固定 24 字节头部而非文本解析、原生 quiet(noreply 式)操作用于管道、以及 SASL 支持认证。对小值和高吞吐批量写入明显更快。服务器在同一端口说两种协议,因此启用它纯粹是客户端选择。生产客户端优先使用它。
# the binary protocol (vs ASCII text) is more efficient:
# - fixed 24-byte header (no string parsing)
# - supports SASL authentication
# - supports pipelining and quiet (noreply-equivalent) ops natively
# - smaller wire footprint for small values
#
# enable on the client side (the server speaks both on the same port):
# pymemcache: Client(..., allow_unicode_keys=True, default_noreply=True)
# spymemcached: use the binary connection factory
# libmemcached: MEMCACHED_BINARY_PROTOCOL
# binary quiet ops (Noop/SetQ/AddQ/...):
# pipelined writes that only reply on error — perfect for bulk loads
# the server listens for both protocols on 11211 by default;
# no server-side flag is required连接池
连接池化至关重要——每次请求打开 TCP 连接会烧光临时端口并增加延迟。保持每服务器每进程一小池持久连接,按并发设定大小。在 stats 中观察 total_connections 与 curr_connections:若 total 随流量增长而 curr 平坦,你在池化;若 total 爆炸则没有。大多数客户端配置后内部池化。
# opening a TCP connection per request is extremely wasteful.
# reuse connections via a pool:
#
# - keep N persistent connections per server per app process
# - checkout -> use -> checkin (don't close)
# - size the pool to your concurrency, not your request rate
#
# pymemcache: HashClient manages a pool internally
# Node.js memcached: set poolSize
# PHP: persistent connections via memcached::OPT_CONNECT_TIMEOUT
# Java spymemcached: one MemcachedClient (thread-safe, NIO)
#
# signs of a missing pool:
# - high total_connections in stats (grows with traffic)
# - TIME_WAIT exhaustion on the server (ephemeral ports)
# - request latency dominated by connect() not get()/set()
stats
# STAT total_connections 1000000 <- too high; pool your conns
# STAT curr_connections 50 <- healthy pooled count批量操作(Multi-Get)
multi-get 在一次往返中获取 N 个键——可用的最大读取优化。客户端跨服务器并行展开键,因此延迟受最慢服务器而非键数限制。分块超大 multi-get(如 100 个键)以免单个服务器线程被独占,并记住缺失键在结果 map 中静默缺失。
# fetching N keys in one get is far cheaper than N single gets:
# 1 network round-trip instead of N
# server parses 1 command instead of N
# text protocol: space-separated keys
get user:1 user:2 user:3 user:4 user:5
# clients wrap this:
# pymemcache: mc.get_many([...])
# Node.js: mc.getMulti([...])
# PHP: $mc->getMulti([...])
# Go: mc.GetMulti([]string{...})
# best practices:
# - chunk very large multi-gets (e.g. 100 keys at a time)
# so no single server thread is monopolized
# - keys in a multi-get fan out to multiple servers — the client
# parallelizes, so a multi-get is bound by the SLOWEST server
# - missing keys are simply absent from the result合理调整值大小
因为条目向上取整到下一个 chunk 大小,值尺寸直接驱动内部碎片——96 字节 chunk 中的 60 字节值浪费 37%。合理调整值:修剪 JSON 空白,压缩超过 ~1KB 的任何内容(用 flags 位标记),缓存投影而非整个 blob。用 stats sizes 检查尺寸直方图,调 -f 让常见尺寸落到 chunk 边界附近。