前言Memcached 装了服务也起来了telnet 127.0.0.1 11211里敲stats有输出可 PHP 里new Memcached()就是报Class Memcached not found。——这是最典型的一幕。原因几乎总是同一个memcached服务端和memcachedPHP 扩展是两样东西服务端跑起来了不等于扩展装好了。第二个常见症状是扩展装上了、php -m里也能看到memcached但代码里$mc-set(k, $v)返回true、$mc-get(k)却返回falsegetResultCode()给出RES_NOTFOUND。这种情况通常是连到了不同的实例或者 key 被前缀改了——PHP 客户端会给 key 加Memcached::OPT_PREFIX_KEY你以为写的是user:1实际存进去的是myapp_user:1。本文把从零开启 Memcached 缓存这条链路完整走一遍先分清楚扩展的选型再讲编译安装和php.ini配置项然后给出可运行的连接与读写代码最后落到排查手段。本文示例在 PHP 8.5 上验证语法。有一点必须提前说明memcached扩展是 PECL 上的第三方扩展不在 PHP 官方源码包里它的可用性取决于 PECL 是否已经发布了支持 PHP 8.5 的版本。安装前请先在 PECL 上确认当前版本号不要直接把 8.2 的包拿来编译。一、先选对扩展memcache 还是 memcachedPHP 生态里有两个名字极像的扩展很多人装错了对比项memcache扩展memcached扩展底层库自己实现协议libmemcachedC 库维护状态长期停滞对新 PHP 版本支持滞后仍在维护实例化方式new Memcache()new Memcached()添加节点$m-addServer(127.0.0.1, 11211)$m-addServer(127.0.0.1, 11211)一致性哈希需自己配置Memcached::OPT_LIBKETAMA_COMPATIBLE二进制协议不支持Memcached::OPT_BINARY_PROTOCOLSASL 认证不支持memcached.use_sasl会话处理器session.save_handler memcachesession.save_handler memcached结论新项目一律用memcached扩展。两者的函数名和常量名完全不通用混用会导致一堆Call to undefined method。下文所有内容都针对memcached扩展。二、安装编译扩展与服务端2.1 装服务端# Debian / Ubuntu sudo apt-get install -y memcached libmemcached-tools # RHEL / CentOS / Rocky sudo dnf install -y memcached libmemcached # 启动并设为开机自启 sudo systemctl enable --now memcached sudo systemctl status memcached默认监听127.0.0.1:11211。生产环境至少要改三件事写在/etc/memcached.conf# 只监听内网绝对不要 -l 0.0.0.0 暴露到公网 -l 127.0.0.1 # 内存上限超过后按 LRU 淘汰 -m 512 # 单条 value 上限默认 1MB超过 set 会失败 -I 4m # 以专用用户运行 -u memcache2.2 装 PHP 扩展memcached扩展依赖libmemcached的头文件所以必须先装开发包# 编译依赖 sudo apt-get install -y libmemcached-dev zlib1g-dev pkg-config # 用 pecl 安装会自动挑当前 PHP 对应的版本 sudo pecl install memcached # 如果 pecl 报版本不兼容指定通道版本重试 sudo pecl install channel://pecl.php.net/memcached-3.3.0装完后不要只在php.ini里加extensionmemcached就完事memcached扩展在部分发行版下会拆成两个.so主扩展 memcached.so的 igbinary/msgpack 序列化后端用下面的方式确认php --ini # 先确认真实加载的是哪个 php.ini php -m | grep -i memcached # 看模块是否在列表里 php -r var_dump(extension_loaded(memcached)); php -r echo phpversion(memcached), PHP_EOL; # 打出版本号2.3 php.ini 配置; 加载扩展Debian 系通常写在 conf.d 里不要重复加载 extensionmemcached.so ; ---- 会话session相关 ---- ; 用 Memcached 存 session 时会读这几个配置 memcached.sess_locking On memcached.sess_lock_wait_min 150 memcached.sess_prefix sess_ memcached.sess_consistent_hash On memcached.sess_binary_protocol On memcached.sess_number_of_replicas 1 ; ---- 通用 ---- ; 压缩value 超过此字节数才尝试压缩默认 2000 memcached.compression_threshold 2000 memcached.compression_factor 1.3 ; 序列化器可选 php / igbinary / json / msgpack需对应后端已编译进扩展 memcached.serializer php ; SASL 认证开启后必须给用户名密码 memcached.use_sasl Off改完配置后必须重启 PHP-FPM不是reload扩展只在进程启动时加载sudo systemctl restart php8.5-fpm2.4 用 Memcached 存 session如果目的是做多机共享会话还需要改php.inisession.save_handler memcached session.save_path 127.0.0.1:11211 ; 多个节点用逗号分隔 ; session.save_path 10.0.0.1:11211,10.0.0.2:11211session.save_path里的密码等参数用?携带例如127.0.0.1:11211?persistent1weight1timeout1。三、代码实战连接、读写、健康检查下面是一个可以直接保存为memcached_demo.php运行的完整示例。?php declare(strict_types1); /** * Memcached 连接与读写示例 * 运行环境PHP 8.5 memcached 扩展PECL memcached 服务端 * 运行前请确认php -m | grep memcached 有输出 */ final class Cache { private Memcached $mc; /** * param liststring $servers 形如 [127.0.0.1:11211] */ public function __construct( array $servers, private readonly string $prefix myapp_, ) { // 传入唯一 id 会得到一个持久连接persistent connection // 同一进程内多次 new 同一个 id 会复用同一个连接 $this-mc new Memcached(myapp_pool); // 只有在连接池为空时才添加节点。 // 持久连接会把服务器列表记住后续重复 addServer 会被忽略 // 因此必须做这个判断否则改配置后不生效。 if ($this-mc-getServerList() []) { $this-mc-setOptions([ // 与 libmemcached 客户端互操作时必须开启否则哈希算法不一致 Memcached::OPT_LIBKETAMA_COMPATIBLE true, // 二进制协议支持 SASL且更省带宽 Memcached::OPT_BINARY_PROTOCOL true, // 连接超时毫秒 Memcached::OPT_CONNECT_TIMEOUT 200, // 发送/接收超时微秒 Memcached::OPT_SEND_TIMEOUT 1_000_000, Memcached::OPT_RECV_TIMEOUT 1_000_000, // 失败时不要阻塞重试太久 Memcached::OPT_RETRY_TIMEOUT 2, Memcached::OPT_PREFIX_KEY $prefix, // 压缩阈值超过 2KB 的 value 自动压缩 Memcached::OPT_COMPRESSION true, ]); foreach ($servers as $server) { [$host, $port] array_pad(explode(:, $server, 2), 2, 11211); $this-mc-addServer($host, (int) $port); } } } public function get(): Memcached { return $this-mc; } public function set(string $key, mixed $value, int $ttl 300): bool { $ok $this-mc-set($key, $value, $ttl); if ($ok false) { // 失败原因不一定是致命错误也可能是 value 超过 -I 限制 trigger_error( memcached set 失败: . $this-mc-getResultMessage(), E_USER_WARNING ); } return $ok; } /** 缓存旁路cache-aside命中就返回未命中回源并回写 */ public function remember(string $key, int $ttl, callable $loader): mixed { $value $this-mc-get($key); if ($this-mc-getResultCode() Memcached::RES_SUCCESS) { return $value; } $value $loader(); $this-set($key, $value, $ttl); return $value; } } // ------------------------------------------------------------------ // 1) 健康检查拿到版本与统计 // ------------------------------------------------------------------ $cache new Cache([127.0.0.1:11211]); $mc $cache-get(); $stats $mc-getStats(); if ($stats false) { exit(无法连接 Memcached: {$mc-getResultMessage()}\n); } foreach ($stats as $server $row) { printf( 节点 %s | 版本 %s | 运行时长 %s 秒 | 当前连接 %s | 命中 %s | 未命中 %s\n, $server, $row[version] ?? ?, $row[uptime] ?? ?, $row[curr_connections] ?? ?, $row[get_hits] ?? ?, $row[get_misses] ?? ?, ); } // ------------------------------------------------------------------ // 2) 基本读写 // ------------------------------------------------------------------ $cache-set(user:1001, [id 1001, name 张三], 60); $user $mc-get(user:1001); var_dump($user); // ------------------------------------------------------------------ // 3) 结果码排查未命中 vs 真失败 // ------------------------------------------------------------------ $mc-get(不存在的key); switch ($mc-getResultCode()) { case Memcached::RES_SUCCESS: echo 命中\n; break; case Memcached::RES_NOTFOUND: echo 未命中正常回源即可\n; break; case Memcached::RES_SERVER_ERROR: case Memcached::RES_CONNECTION_SOCKET_CREATE_FAILURE: echo 服务端或网络异常: {$mc-getResultMessage()}\n; break; default: echo 其他: {$mc-getResultMessage()}\n; } // ------------------------------------------------------------------ // 4) 原子自增做计数器 // ------------------------------------------------------------------ $mc-set(hits, 0, 0); $mc-increment(hits, 1); $mc-increment(hits, 1); echo hits , $mc-get(hits), PHP_EOL; // ------------------------------------------------------------------ // 5) 批量读写减少 RTT // ------------------------------------------------------------------ $mc-setMulti([k1 v1, k2 v2, k3 v3], 60); $result $mc-getMulti([k1, k2, k3, k4]); print_r($result); // k4 不存在不会出现在结果里 // ------------------------------------------------------------------ // 6) 清理 // ------------------------------------------------------------------ $mc-delete(user:1001); // 谨慎flush 会清空该实例的所有数据线上执行前想清楚 // $mc-flush();几个必须记住的返回值语义get()返回false不等于失败。RES_NOTFOUNDkey 不存在和RES_SERVER_ERROR服务端挂了都返回false必须靠getResultCode()区分。set()返回false时一定要看getResultMessage()。value 超过-I配置的大小时它会明确告诉你 value 太大。addServer()不会真的建立连接。它只是把节点加进哈希环实际连接发生在第一次读写时。所以addServer没报错不代表能连上。四、排查手段# 1) 扩展层确认装的是哪个、什么版本 php -m | grep -i memcached php -r echo (new ReflectionExtension(memcached))-getVersion(), PHP_EOL; # 2) 网络层确认能连上 telnet 127.0.0.1 11211 # 在交互里敲 stats能看到 pid / version / curr_connections # 3) 协议层用 nc 手工发一条 get看服务端怎么回的 printf get myapp_user:1001\r\n | nc 127.0.0.1 11211 # 4) 看是否被前缀改写直接列 key 是不支持的 # Memcached 没有 KEYS 命令只能靠 stats items 和 stats cachedump 观察 printf stats items\r\n | nc 127.0.0.1 11211第 4 点是 Memcached 与 Redis 的一个重要差异Memcached 不支持遍历 key它没有KEYS *。所以我明明 set 了怎么 get 不到这类问题无法靠列 key 排查只能靠核对OPT_PREFIX_KEY、核对节点列表、核对是否连到了不同实例这三板斧。常见坑点1. 把服务端和扩展混为一谈❌systemctl status memcached正常就以为 PHP 里new Memcached()一定能用。 ✅ 两者独立服务端是 C 写的守护进程扩展是 PECL 上的 PHP 模块。看到Class Memcached not found第一步永远是php -m | grep memcached而不是去看服务端。2. 持久连接下重复 addServer 被静默忽略❌new Memcached(pool)之后每次都无条件addServer($newHost, 11211)—— 第一个请求建立的服务器列表会被持久化后续请求里新增的节点根本不会生效你改了配置却没有任何报错。 ✅ 先判断getServerList() []再添加或者干脆不用持久连接 id。3. 忘了 libmemcached 的头文件❌pecl install memcached直接跑报configure: error: libmemcached not found然后去找扩展的 bug。 ✅ 先apt-get install libmemcached-dev——这是唯一的编译前置依赖报错信息里已经写清楚了。4. key 超过 250 字节静默失败❌ 把整段 SQL 或完整 URL 当 key$mc-set($sql, $result)—— Memcached 的 key 上限是250 字节超长时set返回falsegetResultMessage()提示 key 太长。 ✅ 对长 key 做摘要q: . hash(xxh128, $sql)既缩短长度又保持稳定。5. value 超过 1MB 上限❌ 把整个大数组比如 5 万行的报表结果塞进去。 ✅ 用-I调大单条上限注意服务端-I和Memcached::OPT_COMPRESSION要配合或者改成分片存储 / 根本不该放缓存。超过 1MB 的 value 本身就说明这个缓存粒度设计错了。6.get()返回 false 就断定缓存坏了❌if (!$value) { 记一条 error 日志 }—— 未命中会被记成错误日志里全是噪音。 ✅ 用getResultCode()区分RES_NOTFOUND和真正的服务端错误。7. 序列化器配置不一致❌ A 机器memcached.serializer igbinaryB 机器是php同一份数据在两台机器间读出来是垃圾或读失败。 ✅ 全部机器配置一致并且把这项写进部署检查清单。改序列化器等于数据格式不兼容必须整体重启并接受缓存全量失效。8. 缓存里存了false或null造成永远未命中❌$cache-set($key, $db-find($id))—— 查不到时值是null写进去后get返回null而null与未命中在返回值上无法区分每次请求都回源缓存形同虚设。 ✅ 用一个哨兵值如__NULL__表示查过了不存在。这类缓存穿透是缓存设计中排第一的坑。总结步骤关键点选型用memcached扩展PECL不要用停滞的memcache依赖libmemcached-dev必须装否则 PECL 编译失败加载extensionmemcached.so改完必须restartFPM版本PECL 扩展独立于 PHP 发版装 8.5 前先确认 PECL 已发布对应版本会话共享session.save_handler memcachedsession.save_path连接要点持久连接会记住服务器列表重复addServer会被忽略结果判断get()返回false必须用getResultCode()区分未命中与故障限制硬限制开启 Memcached 缓存这件事难点从来不在写几行代码而在把服务端、扩展、配置、连接复用这四层的边界分清楚。排查时按扩展装没装 → 服务端通不通 → key 前缀对不对 → 连的是哪个实例的顺序走绝大多数问题在前两步就能定位。最后提醒Memcached 是纯内存缓存进程重启数据全丢只适合放丢了能回源重建的数据。