Skip to content

Memcached 速查表

高性能分布式内存对象缓存系统。

01

入门

连接与基本命令

Memcached 在 11211 端口使用简单的文本协议。set 命令接收 flags(对服务器不透明的 32 位整数)、exptime(过期秒数,0 = 永不过期)和字节数。值以原始字节存储——服务器从不检查或修改它们。memcached-tool 随服务器附带,便于快速查看统计信息。

memcached
# 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 字节头部,大多数生产客户端优先采用。两种协议操作同一个底层键值存储。

memcached
# 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),而非冗长的描述性名称。

memcached
# 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 缓存:绝不存储无法从权威系统中重建的数据。

memcached
# 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
# 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
02

基本命令(set / get / delete)

set — 存储值

set 无条件存储值,覆盖任何已存在的键。flags 是服务器存储但从不解释的 32 位整数——客户端用它编码序列化格式(如 1 = JSON,2 = 压缩)。exptime 为 0 表示永不过期;大于 30 天的 Unix 时间戳被视为绝对时间。noreply 跳过响应,适合即发即弃写入(更快,但无错误反馈)。

memcached
# 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
# -> STORED

get — 获取值

get 在一次往返中获取一个或多个键——批量(multi-get)对性能至关重要。每个返回的 VALUE 行回显 flags,让客户端知道如何反序列化。缺失的键被静默省略(无错误)。响应以 END 结束。大型 multi-get 应分块,避免阻塞单个服务器线程。

memcached
# 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 一起使用以避免多客户端修改同一键时的更新丢失。令牌是不透明的——切勿假设其值或跨键的单调性。

memcached
# 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
# -> STORED

delete — 删除键

delete 移除单个键,返回 DELETED 或 NOT_FOUND。没有通配符或模式删除——Memcached 根本没有键枚举命令,因此批量删除必须由应用自行跟踪(在其他地方维护键集合,或使用命名空间的 flush 模式)。添加 noreply 可跳过响应。被删除的内存归还到其 slab 并被复用。

memcached
# 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,以避免两次操作之间值变化产生的竞态。

memcached
# 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
03

存储命令(add / replace / append / prepend)

add — 仅在不存在时存储

add 仅当键不存在时存储值,否则返回 NOT_STORED。它是无竞态地惰性初始化键(计数器、锁)的规范方式。与 incr/decr 结合可形成安全的计数器模式:用 add 创建为 0,再 incr。与 Redis 的 SETNX 不同,add 没有 TTL 快捷方式——在命令中设置 exptime。

memcached
# 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
# -> 1

replace — 仅在已存在时存储

replace 仅当键已存在时存储值——add 的镜像。它防止从过时的写入路径意外创建缓存条目。键不存在时返回 NOT_STORED。当写入应刷新现有缓存但绝不应填充新条目时(如后台刷新不应复活已被驱逐的键)使用它。

memcached
# 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 条目限制。

memcached
# 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 不返回结果,跨客户端并非原子读-改-写,但它们是服务器端原子操作。

memcached
# 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 令牌限制。选对命令可将读-改-写竞态转化为单个原子服务器操作。

memcached
# 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 response
04

CAS(Compare-And-Swap)

gets — 获取 CAS 令牌

gets(get-extended)是 CAS 的读取半部分。它返回值加上 cas_unique 64 位令牌,服务器保证该令牌在键的每次修改时变化。你捕获此令牌并交给 cas;若令牌仍匹配,写入成功。没有 gets 就无法做安全的 CAS——普通 get 省略了令牌。

memcached
# 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 循环。它避免了服务器端加锁,同时防止更新丢失。

memcached
# 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。

memcached
# 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,这样两个竞相创建它的客户端不会互相覆盖。

memcached
# 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 当作全新开始而非失败。

memcached
# 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)
05

计数器(incr / decr)

incr — 原子自增

incr 原子地将一个正整数加到值为十进制字符串的键上,返回新值。该操作在并发客户端间完全原子——无更新丢失。值必须是 64 位无符号数字 ASCII(无符号、无小数)。incr 对缺失键返回 NOT_FOUND;对非数字数据报错。没有自动创建——先用 add。

memcached
# 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_FOUND

decr — 原子自减

decr 原子地从数字值中减去并返回结果。它在 0 处向下取整截断——永远不会产生负数,这对库存计数器很方便,但也意味着无法用 decr 跟踪净负余额。与 incr 一样,要求值为 ASCII 数字且键已存在(否则 NOT_FOUND)。

memcached
# 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,可让它们自动过期。

memcached
# 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 截断,因此库存不会变负。

memcached
# 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。

memcached
# 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
06

过期与 TTL

set 设置过期时间

exptime 以秒为单位:0 表示永不过期,最大到 2592000(30 天)的值是相对 TTL,更大的值被解释为绝对 Unix 时间戳。过期条目不会在精确秒数被主动回收——Memcached 使用惰性过期(访问时检查)加上后台 LRU crawler,因此过期键可能残留直到被触碰或驱逐。

memcached
# 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,调用者应将其视为'未缓存'。

memcached
# 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_FOUND

gat & gats — 获取并更新

gat(1.6+)在一次往返中原子地组合 get 和 touch——非常适合应将过期时间向前滑动的会话读取。gats 还返回 CAS 令牌。单独使用 get+touch 会产生 TTL 未刷新的窗口并使往返翻倍;gat 同时解决这两个问题。对于多键滑动读取,gat 接受多个键。

memcached
# 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
# 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 做显式失效。

memcached
# 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
07

统计命令

stats — 概览

stats 输出核心服务器指标:cmd_get/cmd_set(总操作数)、get_hits/get_misses(命中率 = hits/cmd_get)、evictions(因内存压力丢弃的条目)、curr_items、bytes 与 limit_maxbytes、以及连接数。命中率和 evictions 是首先要看的两个数字。这是任何 Memcached 监控的基础。

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 pressure

stats items — 按 slab 统计条目数

stats items 报告每个 slab 类持有的条目数(number)和其最久未存储条目的年龄(age)。number 高但 age 低的 slab 正在被频繁更替;age 高的 slab 持有长生命周期数据。这是调优 chunk 大小或诊断某个尺寸范围为何被驱逐时首先要看的地方。

memcached
# 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 / hot

stats slabs — Slab 分配器状态

stats slabs 揭示 slab 分配器的布局:每个类有 chunk_size(条目槽向上取整到它)、chunks_per_page、total_pages 和已用/空闲 chunk。分配给某 slab 类的内存留在那里——不能归还给其他类。如果一个类满了且在驱逐而另一个空闲,你遇到了 slab 钙化,可通过调优 -f(增长因子)解决。

memcached
# 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 slots

stats sizes — 条目大小直方图

stats sizes 构建真实条目大小(键+值+开销)的直方图,让你看到有多少内存浪费在 chunk 大小取整上。它执行全量扫描并历史上会锁住服务器——现代版本在安全模式检查后启用;传 --disable-safe-mode 强制运行。仅在低流量时运行。输出有助于选择正确的 -f 增长因子。

memcached
# 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 刻意不提供高效列出所有键的方法;把它当作由应用驱动的黑盒。

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 command

stats reset 与其他子命令

stats reset 将累计计数器(操作、命中、未命中)清零而不丢数据——便于在部署后测量全新时间间隔。其他有用子命令:stats settings(生效配置)、stats conns(较新构建的每连接详情)、stats lru_crawler(crawler 状态)。计数器否则自进程启动累计,因此基准测试前先 reset。

memcached
# 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)
08

设置与配置

stats settings — 生效配置

stats settings 显示运行配置:maxbytes(-m)、maxconns(-c)、growth_factor(-f)、item_size_max(-I)、是否启用 evictions 和 CAS、以及 LRU crawler 状态。大多数在启动时固定;只有少数可在运行时更改。这是确认服务器是否拾取了你认为的标志的首要地方。

memcached
# 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 数以提升吞吐。

memcached
# 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 是控制面操作,不是设置。

memcached
# 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 变成错误而非驱逐。

memcached
# -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。超过后锁争用限制收益。每个线程拥有哈希表的一片,因此更多线程对单个热点键无帮助。

memcached
# -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
09

缓存策略

Cache-Aside(懒加载)

Cache-aside 是最简单也最流行的模式:应用先读缓存,未命中时从数据库加载并回填缓存。它具弹性(缓存故障只是意味着更多数据库负载),但允许冷键踩踏和直到显式失效前的过时。即使在写时失效也要设置 TTL,作为漏掉失效的安全网。

memcached
# 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 invalidated

Read-Through(读穿透)

在 read-through 中,缓存层本身在未命中时调用加载器——应用只调用 get 并得到值。这集中了缓存填充逻辑并保证跨调用方一致,代价是在缓存调用内有同步的数据库读取。大多数 Memcached 客户端不内置 read-through,因此通常实现为 cache-aside 的薄封装。

memcached
# 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。

memcached
# 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。保留给写密集、容忍丢失的工作负载,如计数器和遥测。

memcached
# 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 无法列出键。

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 instead

TTL 与驱逐策略

按数据易变性匹配 TTL:参考数据用小时级,用户数据用分钟级,限流计数器用窗口长度。始终加抖动(±5–10%)避免同步过期踩踏。Memcached 在内存压力下按 slab 做 LRU 驱逐,无每条目固定——唯一杠杆是 TTL 和总量规划。对热键,在未命中时用基于 add 的锁,让仅一个客户端重算。

memcached
# 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
10

客户端库

Python(pymemcache)

pymemcache 是纯 Python、快速且显式的。单服务器用普通 Client,集群用 HashClient(一致性哈希)。set/get 是基础;get_many 批量 multi-get。计数器用 add+incr,乐观更新用 gets/cas。始终传显式超时——死亡节点应快速失败,而非挂住你的请求线程。

memcached
# 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 体验。

memcached
// 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 shutdown

PHP(memcached 扩展)

PHP 的 memcached 扩展(基于 libmemcached)是生产之选——设 OPT_LIBKETAMA_COMPATIBLE 实现跨服务器一致性哈希,用 addServers 做集群。getMulti 批量读取。cas() 接受 fetch() 结果数组中的令牌。避免旧的 memcache 扩展(无 'd');它功能少且维护差。始终设 TTL 并干净处理 Memcached::RES_NOTFOUND。

memcached
<?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。

memcached
# 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 线程。

memcached
// 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 复用同一客户端。

memcached
// 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")
11

分布式架构

一致性哈希

一致性哈希将服务器和键都映射到环上;键由顺时针方向的下一个服务器拥有。添加或移除服务器只移动该服务器的键切片(约 1/N),而非整个键空间——远优于 hash(key) % N(后者在成员变更时几乎全部重映射)。大多数客户端使用 ketama 变体,每服务器多个虚拟节点以均衡分布。

memcached
# 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
# 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 个)映射到环上以均衡负载。定位键时哈希它并顺时针走到下一个节点。关键陷阱:每种语言的每个客户端必须使用相同算法和一致的服务器标签字符串,否则它们会对所有权不一致并在部署后导致大量缓存未命中。

memcached
# 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 的键——远优于模运算哈希(几乎全部重映射)。但服务器崩溃仍永久丢失其键(无复制),导致源头负载瞬时飙升。同时向所有客户端推出成员变更,并考虑在正式上线前通过影子读取预热新节点。

memcached
# 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
# 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
12

内存管理(Slabs / LRU)

Slab 分配器

Memcached 使用 slab 分配器:内存被划分为固定大小 chunk 的类,条目存储在能容纳它的最小 chunk 中(向上取整)。这避免了 malloc/free 碎片并给出 O(1) 分配,代价是内部碎片(chunk 内浪费空间)和类钙化(内存不能在类间移动)。这就是为什么值尺寸的均匀混合对效率很重要。

memcached
# 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 slabs

Slab 类与增长因子

Slab 类尺寸从 -n 最小值(默认 48 字节)开始,按 -f 因子(默认 1.25)增长到 -I 最大条目大小。较低的 -f 产生更多更细粒度的类(浪费少,钙化风险高);较高的 -f 产生更少更粗的类(浪费多)。用 stats sizes 检查真实值尺寸直方图,调 -f 让常见尺寸落到填充良好的类上。

memcached
# 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 80

LRU 驱逐

驱逐按 slab 类进行,在该类内严格 LRU:当类满时,该类中最久未使用的条目被丢弃以腾出空间。有空闲 chunk 的类从不驱逐,因此不平衡的尺寸分布可能导致一个类重度驱逐而另一个空闲——经典的 slab 钙化问题。在 stats 中观察 evictions 和 evicted_active 以发现它。

memcached
# 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% 运行意味着持续驱逐和低命中率的更替缓存。

memcached
# -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
# 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 可能饿死一个类。

memcached
# 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
13

性能优化

noreply 与管道

noreply 砍掉每次写入的响应,管道将多个命令批量到一次网络往返——两者合起来是批量写入最大的吞吐杠杆。multi-get 对读取做同样的事。noreply 的代价是丢失每命令错误反馈,因此保留给非关键、高容量写入,在必须暴露失败时保留响应。

memcached
# 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 支持认证。对小值和高吞吐批量写入明显更快。服务器在同一端口说两种协议,因此启用它纯粹是客户端选择。生产客户端优先使用它。

memcached
# 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 爆炸则没有。大多数客户端配置后内部池化。

memcached
# 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 中静默缺失。

memcached
# 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 边界附近。

memcached
# because of the slab allocator, value size drives memory waste:
#
#   a 60-byte value -> 96-byte chunk -> 36 bytes wasted (37%!)
#   a 97-byte value -> 120-byte chunk -> 23 bytes wasted (19%)
#
# right-size values to fit chunk boundaries:
#   - trim/normalize JSON before caching (drop whitespace, short keys)
#   - compress large values (>1KB) with gzip/snappy; set a flags bit
#   - avoid caching huge blobs; cache projections instead
#
# check your size distribution:
stats sizes      # (off-peak; it locks)

# pick -f so common sizes land near chunk boundaries:
#   if most values are 100-200 bytes, -f 1.25 is fine
#   if values cluster at a few sizes, tune -f to match

# flags bit to mark compression:
set bigval 2 0 4096   # flags=2 means "gzip compressed" (your convention)

避免热点键

热点键是单服务器、单线程瓶颈——其读取在一个节点上串行化而其他空闲。将其分片为 N 个桶(读随机桶,更新时写全部),加 1 秒客户端 TTL 合并重载,或移入进程内 LRU。通过每服务器 CPU 不平衡或在 1.6+ 上用 lru_crawler metadump 查看访问频率来检测热点键。

memcached
# a single hot key creates a bottleneck: it lives on ONE server
# and ONE worker thread, so concurrent reads serialize.
#
# symptoms: one memcached server at 100% CPU while others idle;
#           one slab class churning while others are calm.
#
# fixes:
#   1) shard the hot key into N buckets:
#        key = "hot:" + str(random.randint(0, 9))   # 10 buckets
#      write to all N on update; read from a random one
#   2) add a short client-side TTL (e.g. 1s) to coalesce reloads
#   3) precompute and rotate (cache the rendered page, not the query)
#   4) move the hot value closer to the app (in-process LRU)
#
# detect with:
#   - per-server CPU/stats imbalance
#   - lru_crawler metadump all  (peek at access frequency, 1.6+)
14

监控

需关注的核心指标

四个头条指标是命中率(get_hits/cmd_get,目标 >95%)、evictions(内存压力,稳态目标 0)、内存利用率(bytes/limit_maxbytes,目标 <90%)、以及 listen_disabled_num(连接上限命中,目标 0)。按时间跟踪为速率,而非仅计数器。命中率下降或 evictions 上升是不健康缓存的最早迹象。

memcached
# the four numbers to monitor continuously:
stats
# STAT cmd_get, cmd_set        # throughput (ops/sec deltas)
# STAT get_hits, get_misses    # hit rate = hits / cmd_get
# STAT evictions               # memory pressure (rising = bad)
# STAT bytes / limit_maxbytes  # memory utilization %

# derived metrics:
#   hit_rate    = get_hits / cmd_get            # target > 95%
#   eviction_r  = evictions_delta / cmd_set_delta
#   mem_usage   = bytes / limit_maxbytes        # target < 90%

# connection health:
# STAT curr_connections         # active conns
# STAT listen_disabled_num      # >0 = hit -c limit (refused conns)

# alert thresholds (suggestions):
#   hit_rate < 90%        -> investigate cold keys / undersizing
#   evictions > 0 (steady)-> add memory or shorten TTLs
#   listen_disabled_num > 0 -> raise -c

memcached-tool

memcached-tool 是内置 CLI,用于快速临时检查:stats 显示每 slab 表、display 实时刷新视图、dump 查看键(仅调试——它遍历 LRU)。它适合 SSH 一次性检查但不适合持续监控;配合 Prometheus exporter 和 Grafana 做生产仪表盘和告警。

memcached
# memcached-tool ships with the server — quick CLI stats:
memcached-tool host:port stats
memcached-tool host:port display      # live-refreshing table
memcached-tool host:port dump         # dump keys (debug only!)

# sample output of "stats":
#   #  Item_Size  Max_age  Pages  Count  Full?  Evicted Evict_Time
#   1     96B        3600s    1    100    no        0       0
#   2    120B        7200s    2    250    no        0       0
#   ...

# great for a one-off SSH check; not for continuous monitoring.
# for the latter, use a Prometheus exporter + Grafana.

Telnet 监控

通过 telnet 或 netcat 到 11211 端口可即时交互访问文本协议做临时检查:stats、stats settings、stats items、stats slabs、version。用 nc 单行命令做脚本(echo 'stats' | nc -q 1 host 11211)。记住 flush_all 是破坏性的——切勿随意别名。这是检查节点最低摩擦的方式。

memcached
# quick interactive check via telnet/nc:
telnet localhost 11211
stats
stats settings
stats items
stats slabs
version
quit

# or one-liners with nc:
echo "stats" | nc -q 1 localhost 11211
echo "stats slabs" | nc -q 1 localhost 11211

# flush all keys (DESTRUCTIVE — use with care):
echo "flush_all" | nc -q 1 localhost 11211

# tip: pipe commands from a file for repeatable checks
nc localhost 11211 < checks.txt

Prometheus Exporter 与 Grafana

memcached_exporter 是标准 Prometheus 集成——在每个节点旁运行一个并抓取 :9150。Grafana 仪表盘随后跟踪命中率、驱逐率、内存使用和每 slab 填充。在命中率跌破 90%、任何持续 evictions、内存超 90%、以及 listen_disabled_num > 0 时告警。这提供临时 telnet 无法提供的持续历史可见性。

memcached
# popular exporters scrape memcached stats into Prometheus:
#
# 1) memcached_exporter (official):
#    https://github.com/prometheus/memcached_exporter
#    run alongside each memcached instance:
memcached_exporter --memcached.address=localhost:11211 \
  --web.listen-address=:9150

# 2) scrape config in prometheus.yml:
#    - job_name: 'memcached'
#      static_configs:
#        - targets: ['mc1:9150','mc2:9150','mc3:9150']

# 3) Grafana dashboard (e.g. community id 37 or custom):
#    - hit rate (get_hits / cmd_get)
#    - eviction rate (rate(evictions[5m]))
#    - memory usage (bytes / limit_maxbytes)
#    - connections (curr_connections, listen_disabled_num)
#    - per-slab fill / evictions

# alert on: hit rate < 90%, evictions > 0 steady, mem > 90%

关键指标与告警

对命中率退化(警告 <90%,严重 <75%)、任何持续驱逐、内存超 90%、以及任何拒绝连接告警。始终将缓存指标与源头指标关联——命中率下降配数据库 CPU 飙升是缓存未命中风暴的标志,驱逐上升配源头延迟上升指向缓存过小。跟踪 uptime 重置以捕获静默重启。

memcached
# metric                  alert when                     severity
# ------------------------------------------------------------------
# hit_rate                < 90% sustained                 warning
# hit_rate                < 75% sustained                 critical
# evictions (rate)        > 0 steady                      warning
# evictions (rate)        > 100/min                       critical
# mem_usage               > 90%                           warning
# mem_usage               > 98%                           critical
# listen_disabled_num     > 0 (refused conns)             critical
# curr_connections        > 80% of -c                     warning
# uptime                  resets (process restarted)      info
# cmd_get/cmd_set rates   sudden drop (app -> cache down) critical

# pair cache metrics with origin metrics:
#   if cache hit rate drops AND DB CPU spikes -> cache miss storm
#   if cache evictions rise AND origin latency rises -> undersized
15

安全

网络隔离

Memcached 的文本协议无认证,因此绝不可公开暴露——用 -l 绑定到 127.0.0.1 或私有网卡,不用时用 -U 0 禁用 UDP,并将 11211 防火墙限制到应用子网。公开可达的 Memcached 既是开放数据缓存又是 UDP 放大 DDoS 向量(CVE-2018-1000115)。默认监听地址是所有接口——始终覆盖它。

memcached
# memcached has NO authentication in the text protocol.
# it MUST NOT be exposed to the public internet.
#
# bind to loopback or a private network only:
memcached -l 127.0.0.1                  # local only
memcached -l 10.0.0.5                   # private NIC
memcached -l 10.0.0.5 -U 0              # also disable UDP

# firewall: deny 11211 from outside, allow only app subnets
# iptables example:
#   iptables -A INPUT -p tcp --dport 11211 -s 10.0.0.0/24 -j ACCEPT
#   iptables -A INPUT -p tcp --dport 11211 -j DROP

# CRITICAL: the default -l is ALL interfaces.
# A public memcached is an open cache AND a UDP amplification
# vector (CVE-2018-1000115 etc.) — disable UDP if unused.

SASL 认证(二进制协议)

SASL 认证仅在二进制协议上可用(用 -S 启动服务器)。它在连接建立阶段使用 PLAIN 凭据,增加一次可被池化摊销的往返。在仅网络隔离不够的共享或多租户网络上使用 SASL。文本协议无法认证——若需认证,必须端到端使用二进制协议。

memcached
# the BINARY protocol supports SASL (PLAIN) authentication.
# the text protocol does NOT.
#
# server: start with SASL enabled (built-in in most distros)
memcached -S -l 10.0.0.5

# create a SASL user (e.g. via saslpasswd2):
echo -n "myuser" | saslpasswd2 -p -a memcached myuser

# client: provide credentials
#   pymemcache: Client(..., sasl_credentials=("myuser","pass"))
#   spymemcached: use the binary factory + auth descriptor
#   libmemcached: MEMCACHED_BEHAVIOR_SASL

# note: SASL adds a round-trip at connection setup; pooled
# connections amortize this. Use it on shared/multi-tenant networks
# where -l alone isn't enough.

加密与 TLS

Memcached 无原生 TLS——要加密流量,在每个节点前用 stunnel 或 HAProxy 等边车终结 TLS,然后在回环上运行纯 memcached。对大多数 LAN 部署,网络隔离加可选 SASL 足够;将 TLS 保留给跨网络或合规要求的环境。较新的分支(以及一些云托管服务)添加了原生 TLS。

memcached
# memcached itself does NOT support TLS/SSL natively.
# to encrypt traffic, terminate TLS in front of the server:
#
#   app --TLS--> stunnel/haproxy --plain--> memcached
#
# stunnel example (server side):
#   [memcached]
#   accept = 11443
#   connect = 127.0.0.1:11211
#   cert = /etc/stunnel/mc.pem
#
# the client also needs a stunnel/haproxy side, OR a client that
# speaks TLS (some newer forks/patches add native TLS).
#
# for most LAN deployments, network isolation + SASL is enough;
# reserve TLS for cross-network or compliance-required flows.

防火墙与访问控制

分层防御:绑定到私有网卡、防火墙限制到应用子网、禁用 UDP、在共享网络加 SASL、以非 root 用户运行、应用 systemd 加固(NoNewPrivileges、ProtectSystem、PrivateTmp)。关键:切勿在 Memcached 中存储原始机密——它是缓存而非保险库;加密后缓存或将机密保留在专用密钥管理器中,只缓存非敏感引用。

memcached
# defense in depth for memcached:
#
# 1) bind to a private interface (-l 10.0.0.5)
# 2) firewall 11211 to app subnets only
# 3) disable UDP if unused (-U 0) — UDP is an amplification vector
# 4) use SASL on shared networks
# 5) TLS via stunnel for cross-network
# 6) run as a non-root user (-u memcache)
# 7) drop privileges / use systemd hardening
# 8) never store secrets in memcached unencrypted — it's a cache,
#    not a vault (use a secrets manager + encrypt-then-cache)

# systemd hardening example:
# [Service]
#   User=memcache
#   NoNewPrivileges=true
#   ProtectSystem=strict
#   PrivateTmp=true

安全最佳实践

基本规则:绝不公开暴露 Memcached,始终绑定到私有接口,禁用 UDP,在共享网络上用 SASL。不要未加密存储机密或 PII;按租户命名空间键以防止跨用户泄露。保持服务器补丁更新,用 stats conns 审计连接,并把缓存泄露当作数据泄露——轮换任何可能已被缓存的令牌。

memcached
# 1) NEVER expose memcached to the internet (no auth in text proto)
# 2) ALWAYS set -l to loopback or a private address
# 3) disable UDP (-U 0) unless you specifically need it
# 4) use SASL on the binary protocol for multi-tenant networks
# 5) don't store secrets/PII in the cache unencrypted
# 6) namespace keys per-tenant to avoid cross-tenant leakage
#    (e.g. "tnt:42:user:1" — never shared global keys)
# 7) keep memcached updated (security CVEs do happen)
# 8) audit with stats conns to see who's connected
# 9) log and alert on unexpected connection sources
# 10) treat a cache breach as a data breach: rotate any tokens that
#     might have been cached
16

Memcached 与 Redis 对比

数据类型与功能

Memcached 是纯字符串键值缓存;Redis 是数据结构服务器,具备列表、哈希、集合、有序集合、流、Lua 脚本、事务、pub/sub 和模块。Redis 的丰富性在需要时是优势,但 Memcached 的极简对纯缓存是优点——表面积更小,更少误配置。按是否需要服务器端数据结构来选择。

memcached
# Memcached:
#   - strings only (key -> bytes), plus the numeric incr/decr trick
#   - no lists, hashes, sets, sorted sets, streams, pub/sub
#   - no server-side scripting (no Lua)
#   - no transactions (single-key CAS only)
#   - no keyspace notifications
#
# Redis:
#   - rich types: strings, lists, hashes, sets, zsets, streams, ...
#   - Lua scripting, MULTI/EXEC transactions, pub/sub
#   - modules (RedisJSON, RediSearch, RedisGraph, ...)
#   - per-key TTL on any type
#
# verdict: Redis is a data-structure server; memcached is a
# pure key-value cache. Pick memcached when you only need a cache.

持久化

Memcached 零持久化——重启即清空(设计如此),缓存总是可丢弃的。Redis 提供 RDB 快照和 AOF 持久化加复制,可作为主数据存储。若需缓存挺过重启或持有权威数据,Redis 胜出;若要纯易失缓存在未命中时重建,Memcached 的简单是美德。

memcached
# Memcached:
#   - NO persistence. A restart loses everything. By design.
#   - no snapshots, no AOF, no replication
#   - the cache is always disposable; rebuild from the DB on miss
#
# Redis:
#   - RDB snapshots (point-in-time) and AOF (append-only log)
#   - can act as a primary datastore with durability
#   - replication + sentinel/cluster for HA
#
# verdict: if you need the cache to survive restarts, use Redis.
# if you want a pure volatile cache (and rebuild on miss), memcached's
# lack of persistence is simpler and faster.

复制与集群

Memcached 无服务器端集群或复制——分发完全在客户端,死亡节点永久丢失其键。Redis 提供主/副本复制、Sentinel 做 HA、Redis Cluster 做分片加自动故障转移。Memcached 运维更简单(无 gossip、无故障转移逻辑)但不提供 HA;Redis 用运维复杂性换取真正弹性。

memcached
# Memcached:
#   - NO server-side clustering or replication
#   - sharding is 100% client-side (consistent hashing)
#   - a dead node loses its keys; clients reroute
#   - scale out = add nodes to the ring
#
# Redis:
#   - master/replica replication (async)
#   - Sentinel for HA/failover
#   - Redis Cluster for sharding + automatic failover
#   - more moving parts, but true HA
#
# verdict: memcached is simpler to operate (no cluster gossip) but
# has no failover — a dead node is data loss for its keys.
# Redis gives you replication + failover at the cost of complexity.

性能特征

两者都是内存型且亚毫秒。Memcached 默认多线程,纯 get/set 可跨核扩展;Redis 命令执行(大多)单线程——因此在大型多核只做 get/set 的机器上,Memcached 可达更高 QPS。对大多数真实应用差异可忽略;按功能和运维契合度选择,而非微基准。

memcached
# Both are in-memory and very fast (sub-millisecond, >100k ops/s).
#
# Memcached:
#   - multi-threaded by default (-t workers); scales across cores
#   - simple protocol, low overhead per op
#   - excellent for high-throughput pure get/set
#
# Redis:
#   - single-threaded command execution (Redis 6+ threads the I/O)
#   - one core does the work; more cores don't speed a single command
#   - richer ops can be slower, but core get/set is comparable
#
# verdict: for pure get/set at very high QPS on a big multi-core box,
# memcached's multi-threading can win. For most apps the difference
# is negligible; pick by features, not microbenchmarks.

内存效率

Memcached 的 slab 分配器对不透明 blob 精简但浪费空间在 chunk 取整;Redis 有每键/字段开销但为小结构选择紧凑编码(listpack)。两者并非普遍更小——Memcached 对均匀 blob 胜出,Redis 对小结构化数据可能胜出。在假设哪个更便宜前,始终用你自己的值形状测量。

memcached
# Memcached:
#   - slab allocator: fixed chunks, some internal fragmentation
#   - no per-field overhead (it's just bytes)
#   - very lean for small uniform values
#   - can waste memory if value sizes don't fit slab classes
#
# Redis:
#   - per-key/per-field overhead (complex internal structures)
#   - many encodings (listpack, hashtable, skiplist, ...) chosen by size
#   - richer structures use more memory than a flat string
#   - but more memory-efficient for small structured data via listpack
#
# verdict: memcached is leaner for opaque blobs; Redis can be leaner
# for small structured data (listpack). Measure with your own data.

如何选择

当需要纯、可丢弃的 get/set 缓存且运维简单性和多线程扩展重要时选 Memcached。当需要数据结构、持久化、复制、集群、Lua、pub/sub 或模块时选 Redis。两者不互斥——许多栈同时运行,Memcached 做原始页面/查询缓存,Redis 做会话、排行榜、限流和流。

memcached
# Choose Memcached when:
#   - you need a pure, disposable get/set cache
#   - you value operational simplicity (no clustering, no persistence)
#   - you want multi-threaded scaling on a big box
#   - your data is opaque blobs and you rebuild on miss
#
# Choose Redis when:
#   - you need data structures (lists, hashes, sets, zsets, streams)
#   - you need persistence or durability
#   - you need replication / failover / clustering
#   - you need server-side logic (Lua, pub/sub, modules)
#   - you need per-key TTL on rich types
#
# both can coexist: memcached for raw page/query cache, Redis for
# sessions, leaderboards, rate-limiting, streams.
17

故障排除

连接被拒绝

连接被拒绝通常意味着服务器未运行、未在预期地址监听(回环绑定的 -l 阻止远程客户端)、被防火墙拦截,或已达到 -c 连接上限(listen_disabled_num > 0)。走清单:ps/systemctl 查进程,ss/netstat 查监听,iptables 查防火墙,stats 查连接上限,journalctl 查崩溃日志。

memcached
# symptom: client reports "connection refused" to 11211
#
# 1) is memcached running?
ps aux | grep memcached
systemctl status memcached

# 2) is it listening on the expected address/port?
ss -tlnp | grep 11211
netstat -tlnp | grep 11211

# 3) check the -l bind address (default = ALL; you may have set 127.0.0.1)
#    a client on another host can't reach a loopback-bound server

# 4) firewall blocking?
iptables -L -n | grep 11211

# 5) hit the -c connection limit? (check listen_disabled_num)
echo "stats" | nc -q 1 host 11211 | grep listen_disabled

# 6) check memcached logs / journalctl
journalctl -u memcached -n 100

驱逐风暴

evictions 上升且缓存满意味着工作集超过 -m。用 stats(bytes == limit_maxbytes)和 stats items(哪个 slab 类在驱逐)诊断。修复:调大 -m、缩短 TTL 让条目自行过期、用 slabs reassign/automove 重平衡 slab 页、并追查制造过大值的失控写入。持续满的缓存需要更多内存或更小工作集。

memcached
# symptom: evictions counter rising fast, hit rate dropping
#
# diagnosis:
stats
# STAT evictions 999999     <- climbing
# STAT bytes 67108864
# STAT limit_maxbytes 67108864   <- 100% full

stats items
# STAT items:3:evicted 500000    <- slab class 3 is the victim
# STAT items:3:outofmemory 0

# fixes:
# 1) add memory: raise -m (or cache_memlimit at runtime)
# 2) shorten TTLs so items self-expire before LRU drops them
# 3) rebalance slab classes: slabs reassign <donor> <receiver>
#    or: slabs automove 1
# 4) check for a runaway writer creating oversized values
# 5) review your working set — maybe you're caching too much

命中率低

低命中率(<90%)配高数据库负载通常意味着 TTL 太短、工作集超过内存(驱逐)、键未复用(键中含时间戳等易变成分)、冷启动、或客户端对 ketama 配置不一致导致键落到不同服务器。验证所有客户端共享相同服务器列表和哈希,并检查应用日志的键复用模式。

memcached
# symptom: hit rate < 90%, DB load high
#
# diagnosis:
stats
# STAT cmd_get 100000
# STAT get_hits 70000          <- 70% hit rate, too low
# STAT get_misses 30000

# common causes:
# 1) TTL too short -> items expire before reuse (raise TTL)
# 2) working set > cache memory -> evictions (see eviction storm)
# 3) keys not reused -> each request computes a unique key
#    (e.g. including a timestamp; remove volatile components)
# 4) cold start -> warmup period; preheat the cache
# 5) multiple client configs -> keys hashed to different servers
#    (verify all clients use the same ketama config)
# 6) cache invalidated too eagerly on writes (delete too broad)

# check TTL distribution vs access patterns in your app logs

Slab 钙化

Slab 钙化是内存卡在不需要它的类中而另一个类在驱逐中更替——页无法自行迁移。用 stats slabs(free_chunks vs total_pages)和 stats items(每类 evicted)诊断。用 slabs reassign(一次性)或 slabs automove 1(后台)修复,并通过调 -f 匹配值尺寸分布来预防复发。

memcached
# symptom: one slab class keeps evicting while another has free chunks
#
# diagnosis:
stats slabs
# STAT 1:total_pages 4    STAT 1:free_chunks 0      <- class 1 full
# STAT 5:total_pages 12   STAT 5:free_chunks 8000   <- class 5 idle
#
# stats items
# STAT items:1:evicted 99999   <- class 1 churning

# cause: over time, all pages got assigned to popular classes;
# memory is stuck and can't move to where it's needed.

# fixes:
# 1) one-shot: slabs reassign 5 1    # move a page class5 -> class1
# 2) auto:     slabs automove 1      # cautious background rebalance
# 3) restart with a better -f growth factor that matches your data
# 4) enable lru_crawler for smarter eviction

# verify after:
stats slabs

内存占用过高

意外的内存使用通常是内部碎片(值不适配 chunk)、值膨胀(未压缩 JSON)、或单纯键数量(每条目开销)。用 stats(bytes vs limit_maxbytes)和 stats slabs(已用 vs 总 chunk)诊断。修复:压缩大值、修剪空白和键名、调 -f 减少浪费、并验证 -m 匹配你实际需要的工作集。

memcached
# symptom: memcached using more memory than expected
#
# diagnosis:
stats
# STAT bytes 600000000          # actual item bytes
# STAT limit_maxbytes 671088640 # the -m cap
# STAT bytes_read / bytes_written help gauge traffic

stats slabs
# look at used_chunks vs total_chunks per class — internal
# fragmentation = (total_chunks * chunk_size) - bytes

# common causes:
# 1) internal fragmentation (values don't fit chunks well)
# 2) bloated values (cache full JSON with whitespace; compress!)
# 3) too many keys (key+metadata overhead per item)
# 4) memory leak in the client holding dead connections
# 5) -m is just set high and the working set filled it

# fix: compress large values, trim keys, tune -f, lower -m

响应慢与超时

慢响应通常来自热点键在一个线程上串行化、超大 multi-get 阻塞服务器、巨大值饱和网络、应用到缓存网络延迟、-t 线程太少导致 CPU 饱和,或——关键——Memcached 交换到磁盘(切勿让它交换;-m 钉在物理 RAM 以下)。设激进客户端超时(50–100ms),让慢缓存优雅降级到源头而非拖住请求。

memcached
# symptom: get/set taking >10ms or timing out
#
# 1) a hot key serializing on one server thread (see hot keys)
# 2) huge multi-get blocking a server thread — chunk it
# 3) oversized values (multi-MB) saturating the network/parse
# 4) network latency between app and cache (co-locate them!)
# 5) server CPU saturated (-t too low for load)
# 6) swap! memcached must NEVER swap — check free -m; pin -m below RAM
#
# diagnose:
stats
# STAT curr_connections 4000   <- near -c? pool leak?
# STAT threads 4               <- raise -t?
# top -H -p <pid>              <- which thread is hot?

# on the client: set aggressive timeouts (e.g. 50-100ms) so a slow
# cache degrades gracefully to the origin instead of stalling
18

部署配置

作为守护进程运行

用 -d 将 Memcached 作为守护进程运行,始终用 -u 降权到非 root 用户。关键生产标志:-m(内存)、-l(绑定地址——切勿留在所有接口)、-c(连接上限)、-t(线程)、-f(slab 增长因子)。用 -P 写 pidfile 便于管理,并用 netcat 快速版本检查验证进程。

memcached
# start memcached as a background daemon (-d):
memcached -d -m 256 -p 11211 -u memcache -l 127.0.0.1 -c 2048

# common production flags:
#   -d              daemonize
#   -u memcache     drop privileges to this user
#   -m 256          256 MB item memory
#   -l 127.0.0.1    bind to loopback (or private NIC)
#   -c 2048         max connections
#   -t 8            8 worker threads
#   -f 1.25         slab growth factor
#   -I 1m           max item size
#   -v / -vv        log verbosity
#   -P /var/run/memcached.pid   write a pidfile

# check it's up:
echo "version" | nc -q 1 127.0.0.1 11211
# -> VERSION 1.6.24

Systemd 服务

systemd 单元提供自动重启(Restart=on-failure)、降权(User=memcache)、文件描述符限制(LimitNOFILE 用于多连接)和安全加固(NoNewPrivileges、ProtectSystem、PrivateTmp)。大多数发行版自带默认单元,可通过 /etc/systemd/system/memcached.service.d/override.conf 覆盖而非重写整个文件——为可移植性优先这种方式。

memcached
# /etc/systemd/system/memcached.service
[Unit]
Description=Memcached
After=network.target

[Service]
Type=simple
User=memcache
Group=memcache
ExecStart=/usr/bin/memcached -m 256 -p 11211 -l 127.0.0.1 -c 2048 -t 8 -f 1.25
Restart=on-failure
RestartSec=2
LimitNOFILE=8192

# hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/run

[Install]
WantedBy=multi-user.target

# enable and start:
#   systemctl daemon-reload
#   systemctl enable --now memcached
#   systemctl status memcached

Docker 部署

在 Docker 中,记住有两个内存限制:memcached 的 -m(条目内存)和容器内存上限(docker -m)。始终将容器上限设得高于 memcached 的 -m,为运行时和连接留空间,否则 OOM killer 会出手。固定特定版本标签(memcached:1.6)而非 :latest 以可复现,用 restart=unless-stopped 保持弹性。

memcached
# run memcached in Docker:
docker run -d \
  --name memcached \
  -p 11211:11211 \
  -m 512m \
  --restart=unless-stopped \
  memcached:1.6 \
  -m 256 -c 2048 -t 4 -f 1.25

# note: -m inside the container is memcached's item memory (256 MB);
#       the docker -m 512m limit caps the whole container (memcached +
#       overhead). Always set the docker limit HIGHER than memcached's -m.

# docker-compose snippet:
#   memcached:
#     image: memcached:1.6
#     command: -m 256 -c 2048 -t 4
#     ports:
#       - "11211:11211"
#     restart: unless-stopped

多实例

每主机运行多个 Memcached 实例可隔离热点 slab 类、缩小每进程重启影响、并使用更多核心——客户端只是把它们视为额外的环成员。代价是更多连接和每进程 slab 页开销。对大多数工作负载,单个多线程进程(-t 调到核数)更简单且足够;仅在遇到特定瓶颈时拆分。

memcached
# run several memcached instances per host for isolation or to
# use more cores efficiently:
#
# instance A: port 11211, 4 GB
memcached -d -m 4096 -p 11211 -l 127.0.0.1 -t 4
# instance B: port 11212, 4 GB
memcached -d -m 4096 -p 11212 -l 127.0.0.1 -t 4

# the client treats them as two ring members (more shards).
# trade-offs:
#   + isolates a hot slab class to its own process
#   + smaller per-process working set = simpler restarts
#   - more connections to manage
#   - more memory overhead per process (slab pages don't share)

# alternative: one process with -t 8 threads (simpler, usually fine)

内存规划

按测得的工作集加余量设定 -m,目标峰值 70–80% 利用率,为尖峰留空间且不更替驱逐。切勿让 Memcached 交换——在为 OS 和其他进程留空间后,-m 保持在可用 RAM 以下。若稳态利用率超 90%,要么调大 -m,要么缩小工作集(更短 TTL、更少缓存物)。

memcached
# how big should -m be?
#
# 1) measure your working set: the keys actually hot in a typical hour
# 2) add headroom: target ~70-80% memory utilization at peak
# 3) leave RAM for the OS, connections, and slab overhead
#
# rule of thumb:
#   -m = working_set_bytes / 0.75
#
# example: 3 GB working set -> -m 4096 (4 GB) for 75% utilization
#
# never let memcached swap:
#   -m must be BELOW available RAM (check free -m)
#   account for other processes on the same host
#
# monitor utilization:
stats
# STAT bytes 3221225472          # 3 GB used
# STAT limit_maxbytes 4294967296 # 4 GB cap  -> 75% utilization

# if utilization > 90% steady: raise -m or shrink the working set

日志与详细级别

Memcached 默认安静;仅调试时用 -v/-vv/-vvv 或运行时 verbosity 命令提高详细级别,因为记录每条命令会压垮吞吐。生产中保持安静,依靠 stats 和 Prometheus exporter 获取可见性。日志在 systemd 下进 journald,容器中用 docker logs。用 -vv 短暂追踪特定问题,然后调回。

memcached
# memcached is deliberately quiet; increase verbosity for debugging:
memcached -v        # log errors + warnings
memcached -vv       # also log each command (very noisy!)
memcached -vvv      # even more (connection-level detail)

# change at runtime without restart:
verbosity 2         # telnet in and raise the level
verbosity 0         # back to quiet

# with systemd, logs go to journald:
journalctl -u memcached -f

# with Docker:
docker logs -f memcached

# in production, run quiet (no -v) — verbose modes are only for
# debugging, as logging every command kills throughput.
# for access patterns, use stats + an exporter instead of -vv.
19

高级主题

二进制协议深入

二进制协议的固定 24 字节头部实现高效解析、SASL 认证和 quiet(抑制响应)操作码,使管道批量操作比文本协议高效得多。quiet 管道末尾的 Noop 刷新它并显现错误。大多数生产客户端默认或优先使用二进制协议,因其更低的每操作开销和更丰富的功能集。

memcached
# the binary protocol uses a fixed 24-byte request header:
#
#  Magic (1)    0x80 request / 0x81 response
#  Opcode (1)   0x01 Get / 0x02 Set / 0x03 Add / ... / 0x05 Delete ...
#  Key length (2)
#  Extras length (1)
#  Data type (1)   usually 0
#  VBucket/reserved (2)
#  Total body length (4)   extras + key + value
#  Opaque (4)              client-supplied, echoed back
#  CAS (8)
#
# followed by: extras | key | value
#
# "quiet" opcodes (GetQ, SetQ, ...) skip responses except on error,
# enabling efficient pipelined bulk operations.
#
# a Noop at the end flushes the pipeline and confirms success.
# prefer the binary protocol in production clients for lower overhead.

UDP 支持

Memcached 支持 UDP 用于高吞吐、无连接、即发即弃查询——适用于 TCP 握手成本主导的专门场景。它有损且尺寸受限,因此仅用于幂等缓存读取。关键:UDP 是已知放大 DDoS 向量(CVE-2018-1000115);除非有特定、防火墙内的需求,生产中用 -U 0 禁用。

memcached
# memcached can serve UDP (-U port, default 11211; -U 0 disables).
# UDP is for very high-throughput, fire-and-forget scenarios where
# a connection setup is too costly.
#
# each UDP datagram carries:
#   frame header (8 bytes): request ID, sequence, total datagrams
#   then the (possibly multi-datagram) command
#
# limits:
#   - responses may arrive out of order or be lost
#   - datagram size limits (~1400 bytes payload to avoid fragmentation)
#   - no guarantees — use only for cacheable, idempotent lookups
#
# SECURITY: UDP is a known amplification vector (CVE-2018-1000115).
# DISABLE it (-U 0) unless you have a specific, firewalled use case.

memcached -U 0    # recommended in production

大对象与分块

虽然可用 -I 提高 1MB 条目限制,但大条目损害吞吐和 slab 平衡。更好的模式是应用级分块:将值拆分为 ~512KB 块存于编号键下,存一个计数键,读取时重组。这保持条目对 slab 友好,避免独占服务器线程,并使部分失败可恢复而非丢失整个 blob。

memcached
# memcached's default max item size is 1 MB (-I).
# raising it (-I 4m) works but large items hurt: they're slow to
# transfer, monopolize server threads, and risk slab fragmentation.
#
# better: chunk large values yourself:
#
def set_large(key, value, ttl=3600):
    chunks = [value[i:i+512*1024] for i in range(0, len(value), 512*1024)]
    for idx, c in enumerate(chunks):
        mc.set(key + ":" + str(idx), c, ttl)
    mc.set(key + ":n", len(chunks), ttl)

def get_large(key):
    n = mc.get(key + ":n")
    if n is None: return None
    return b"".join(mc.get(key + ":" + str(i)) for i in range(n))

# trade-off: more keys/round-trips, but smaller items fit slabs
# better and a single chunk failure doesn't lose the whole value.

命名空间与版本化

Memcached 无真正命名空间——用键前缀伪造,并用版本化前缀一次性失效整个逻辑命名空间(提升 nsver 键让旧键不可达并通过 TTL 老化)。包含应用版本前缀让每次部署获得全新缓存。这是批量失效的规范方式,因为 Memcached 无法枚举或模式删除键。

memcached
# memcached has no native namespaces — fake them with key prefixes.
#
# per-tenant isolation:
key = "tnt:" + tenant_id + ":user:" + user_id

# version the whole namespace to invalidate en masse:
ns_version = mc.get("nsver:user") or "1"
key = "user:" + ns_version + ":" + user_id
# to invalidate ALL user:* keys: incr "nsver:user" -> new version
# old keys become unreachable and expire via TTL

# or include a deploy version so each deploy starts fresh:
key = "v" + APP_VERSION + ":user:" + user_id

# time-bucketed namespaces for reports:
key = "report:" + today_date()   # auto-expires by date

# NEVER try "flush_all user:*" — memcached can't enumerate keys.
# versioned prefixes are the canonical bulk-invalidation trick.

缓存雪崩防护

雪崩发生在许多请求未命中同一即将过期的键并全部重算它,压垮源头。规范修复是基于 add 的锁,让仅一个客户端重算而其他短暂等待并重试。结合 TTL 抖动防止同步过期,以及对即使单次未命中也代价高昂的热键做主动刷新(过期前刷新,一个客户端重算时返回旧值)。

memcached
# a stampede: many requests miss the same expiring key, all
# recompute the same expensive value, overwhelming the origin.
#
# fix 1: lock-and-recompute (only ONE client rebuilds)
def get_with_lock(key, rebuild_fn, ttl=3600, lock_ttl=30):
    val = mc.get(key)
    if val is not None:
        return val
    # try to take a rebuild lock
    if mc.add("lock:" + key, "1", ttl=lock_ttl):
        try:
            val = rebuild_fn()           # only the winner recomputes
            mc.set(key, val, ttl)
            return val
        finally:
            mc.delete("lock:" + key)
    else:
        # another client is rebuilding; wait briefly and retry
        time.sleep(0.05)
        return get_with_lock(key, rebuild_fn, ttl, lock_ttl)

# fix 2: add jitter to TTLs so expirations don't synchronize
ttl = base_ttl + random.randint(-60, 60)

# fix 3: "dogpile" — serve a stale value while one client rebuilds
# (store value + a "refresh_at" timestamp; refresh proactively)

这篇内容对您有帮助吗?