
Linux下面做性能优化、扛高并发memcached早晚绕不开。这篇博文是我在实际运维和项目开发中反复使用memcached之后整理出来的入门实战笔记从原理、安装到命令行操作、项目接入、调优排错一条线讲清楚新手看完能直接上手干活老手也能查漏补缺。1. memcached到底是什么为什么要学它1.1 先说清楚它是干什么的memcached是一个开源的高性能分布式内存对象缓存系统本质就是一个住在内存里的键值对数据库。你在项目里把热点数据提前塞进去等真正要读的时候直接从内存拿不再每次打数据库这样数据库压力小一大截接口响应速度也快得多。它最早是LiveJournal这个网站搞出来的后来被Facebook、Wikipedia这些流量大户大规模使用。Linux服务器上部署memcached几乎是后端架构的标配技能跟Nginx、Redis一样属于基础中间件。我见过很多刚入门的同学把memcached想得太玄乎。其实它的核心就三件事存、取、过期。存就是set取就是get过期就是你设一个存活时间到了之后自动淘汰。所有高级用法都是从这三件事延伸出来的。你把这个底层逻辑吃透后面不管接什么语言的项目心里都有底。1.2 它跟Redis的区别怎么选网上讨论最多的就是memcached和Redis的区别我直接用一张表说明方便新手上路时做选择对比项memcachedRedis数据类型仅支持字符串键值String、Hash、List、Set、ZSet等持久化无持久化重启即丢支持RDB和AOF持久化集群模式客户端一致性哈希服务端无集群官方Cluster模式多线程多线程处理网络IO默认单线程事件驱动内存管理Slab AllocatorLRU多种内存淘汰策略适用场景单纯缓存热点数据缓存队列分布式锁等并不是Redis一定比memcached好。如果你的业务就是纯缓存、对数据结构要求简单、希望多核CPU把吞吐量压到极致memcached依然是很务实的选择。很多老牌高并发系统跑了好多年还是memcached稳定得很。1.3 学它之前需要哪些Linux基础想顺利走完这篇入门你最好已经掌握几个Linux基本功会用终端敲命令、会用vim编辑文件、会用systemctl管理服务懂一点网络端口的基本概念。如果这些还不熟建议先去把Linux常用命令过一遍比如grep、ps、netstat、systemctl、firewall-cmd这些否则后面操作会卡壳。此外最好知道一点简单的TCP/UDP概念因为memcached默认走TCP端口11211后面我们排查连接问题时会用telnet、nc这类工具去手动模拟协议交互不懂端口和套接字的基础知识排查起来会像无头苍蝇。2. 环境准备与安装部署2.1 CentOS/RHEL系安装memcached在CentOS或者Rocky Linux这类RHEL系发行版上安装memcached直接走yum/dnf仓库最省事sudo dnf install -y memcached libmemcached装完之后先看一眼版本确认装上了memcached -version我之前遇到过一种情况部分精简安装的Linux服务器默认仓库里没有memcached包这时候先执行sudo dnf makecache刷新元数据再安装一般就能找到。如果还是不行可以用EPEL源先sudo dnf install epel-release再装。libmemcached这个库强烈建议一起装它是一个C语言客户端库后面我们用memcstat、memccat这些工具排查数据时要用到省得自己封装协议。2.2 Ubuntu/Debian系安装Ubuntu系用apt安装同样简单sudo apt update sudo apt install -y memcached libmemcached-tools装完Debian系之后默认服务是自动启动的你可以直接检查一下端口sudo systemctl status memcached ss -lntp | grep 11211看到11211端口在监听了说明已经跑起来了。我建议不管哪个发行版装完都顺手执行一次memcached -h把帮助信息扫一遍里面有不少默认参数对理解配置很有帮助。2.3 用源码编译安装的方式如果生产环境对版本有强制要求或者官方仓库版本太老就得走源码编译。我从实践角度说编译安装最核心的依赖是libeventmemcached是基于libevent做的事件驱动模型这个库必须先搞定。# 安装编译需要的工具和依赖 sudo dnf install -y gcc make libevent libevent-devel # 下载源码包这里假设版本号例如1.6.21 wget https://memcached.org/files/memcached-1.6.21.tar.gz tar zxvf memcached-1.6.21.tar.gz cd memcached-1.6.21 # 配置、编译、安装 ./configure --prefix/usr/local/memcached make -j4 sudo make install这里有个坑如果./configure时报找不到libevent说明devel包没装全。RHEL系是libevent-develUbuntu系是libevent-dev别搞混。2.4 配置文件详解与建议参数rpm或apt方式安装的memcached配置文件一般在/etc/sysconfig/memcachedCentOS系或者/etc/memcached.confDebian系。CentOS系默认内容大致如下PORT11211 USERmemcached MAXCONN1024 CACHESIZE64 OPTIONS-l 127.0.0.1,::1这几个参数我用大白话解释一下PORT监听端口一般默认11211不用改。USER以哪个系统用户身份运行绝不建议用root权限最小化原则。MAXCONN最大连接数。连接数满了之后新的连接直接拒绝表现为客户端报连接超时。CACHESIZE单位是MB这个就是memcached最多能吃掉多少内存。默认64MB太小了生产环境一般按机器内存的1/4到1/3分配绝不是越大越好要留内存给系统本身和应用程序。OPTIONS额外的启动参数-l指定监听地址。如果只跑本机监听127.0.0.1就够了需要给其他服务器提供缓存服务就监听内网IP。我这里放一个生产环境常用的配置参考PORT11211 USERmemcached MAXCONN4096 CACHESIZE1024 OPTIONS-l 0.0.0.0 -U 0 -m 1024 -t 4其中-U 0表示关闭UDP端口。memcached默认同时开UDP 11211端口这个端口历史上出过放大攻击漏洞非必要直接关掉。-t 4是启用4个工作线程处理网络请求。2.5 systemd管理memcached服务改完配置后需要重启服务让配置生效sudo systemctl restart memcached sudo systemctl enable memcached检查服务是否正常运行我习惯用三步确认法先看进程状态再看端口监听最后实际set一个键验证systemctl status memcached ss -lntup | grep 11211 printf set health 0 0 2\r\nok\r\n | nc 127.0.0.1 11211nc命令这里如果没装可以用telnet 127.0.0.1 11211手动连上去做同样的操作。最后一步能在命令行里跟memcached直接对话非常直观这也是我强烈建议新手掌握的调试技巧。防火墙方面如果开启了firewalld需要放行11211端口sudo firewall-cmd --permanent --add-port11211/tcp sudo firewall-cmd --reload但这里我多提醒一句除非业务架构真的需要跨服务器访问否则别把memcached暴露在非必要的位置。缓存里可能有用户会话、商品数据一旦裸奔被扫到数据泄露或者被刷流量都是很麻烦的事。3. 核心操作命令行与文本协议实战3.1 手动用telnet/nc对话memcachedmemcached有一个非常朴素的文本协议直接用telnet或者nc就能模拟客户端操作。我先把最常用的一组命令列出来命令作用示例set新增或覆盖一个键值对set name 0 60 5add仅当键不存在时新增add name 0 60 5replace仅当键已存在时覆盖replace name 0 60 5get读取一个或多个键get name agedelete删除一个键delete nameincr/decr对数值类型的值做原子自增/自减incr num 1stats查看统计信息statsflush_all清空所有数据flush_allset命令的具体格式要注意它实际上有四行协议交互。假设我要存一个name值为Tomtelnet 127.0.0.1 11211 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is ^]. set name 0 60 3 Tom STOREDset name 0 60 3这四个字段拆开看name是键名0是标志位客户端用来区分数据类型memcached本身不关心60是存活时间单位秒3是value的字节数必须跟实际字节数一致多一个少一个都会报错。下一行直接输入Tom回车返回STORED就说明存成功了。这地方新手最容易栽跟头的是字节数数错。字符串Tom刚好3个字节没问题但如果内容是中文比如张三在UTF-8编码下是6个字节这时候写set user 0 60 6然后输入张三才会返回STORED。我见过有人在这个细节上卡了半小时一直报CLIENT_ERROR bad data chunk。3.2 get查询和delete删除在telnet里执行get name VALUE name 0 3 Tom END返回结果里VALUE name 0 3就是告诉你有这个键标志位是0数据长度是3字节。紧接着下一行是真正的数据然后以END结束。如果get一个不存在的键直接返回END不会有VALUE行。删除操作更简单delete name DELETED如果键不存在会返回NOT_FOUND。实际项目中delete常用于清除缓存比如后台改了文章标题就要立刻把前台对应的缓存删掉防止用户读到脏数据。3.3 incr/decr做原子计数memcached的incr/decr命令在秒杀、限流、统计这类场景非常实用。存一个数值需要先set注意存活时间设为0表示永不过期set visitor_count 0 0 1 5 STORED incr visitor_count 1 6 get visitor_count VALUE visitor_count 0 1 6 END你可能会问为什么不用get然后程序里1再set回去因为那两步操作之间有间隙高并发下两个请求同时读到旧值就会丢失一次自增。incr是服务端原子操作不管多少请求同时打过来最后的结果一定是准确的。这个特性在做计数器功能时比读改写稳妥得多。3.4 过期时间的几种写法set命令第三个参数是过期时间写法上有两种方式0到2592000之间的数字表示相对时间单位秒意思是从现在开始多少秒后过期。超过2592000的数字会当作Unix时间戳处理表示到某个绝对时间点过期。为什么是2592000这个数因为memcached内部约定30天以内的当作相对秒数超过30天当作绝对时间戳。你不小心把过期时间设成3600000这种大数字本意是1小时结果它被解释成了时间戳缓存瞬间就过期了。我专门在代码评审时抓出过这种bug新手值得留意。另外过期时间不一定被立即物理清除。memcached用的是惰性过期就是说一个键过期了但占用的内存并不会马上释放而是等到下次访问这个键时发现过期才会标记为可复用。还有一种情况是内存不够时由LRU算法被动淘汰。理解这个机制就能明白为什么有时stats里的curr_items不会立刻降下来。3.5 stats命令快速判断缓存健康度对运维来说stats信息是判断缓存系统是否健康的仪表盘。我在排查问题时的第一件事就是敲statsstats STAT pid 12345 STAT uptime 3600 STAT curr_items 1024 STAT total_items 2048 STAT get_hits 5000 STAT get_misses 1000 STAT delete_misses 100 STAT bytes 1048576 STAT curr_connections 5 STAT total_connections 100 STAT cmd_get 6000 STAT cmd_set 2048 STAT evictions 10 STAT limit_maxbytes 1073741824 END这里重点关注两个指标命中率get_hits/cmd_get和驱逐数evictions。命中率太低说明缓存利用率不行可能key设计有问题或者缓存预热不够。evictions大于0说明内存不够装了旧的key被强制踢出去这时候该考虑扩容缓存内存或者优化缓存策略了。计算命中率的公式是get_hits/(get_hitsget_misses)*100%我一般以90%为健康分界线。还有一个stats cachedump 1 100命令可以用来枚举某个slab类里的key但这个命令在生产环境要慎用它会遍历整个类别的数据量大时对性能有影响调试环境用用就好生产我基本不用它。4. 实际项目中的使用套路4.1 缓存读写的经典流程到了真实项目中我们不会直接拿命令行操作memcached而是通过PHP、Python、Java这些语言的客户端库去调用。但不管用什么语言代码里的读写逻辑长成一个套路也就是面试喜欢考的缓存范式第一步先查缓存。第二步缓存没有命中就查数据库。第三步数据库查到结果之后回写一份到缓存同时设置过期时间。第四步返回数据给调用方。我用伪代码描述一下这个流程def get_user_profile(user_id): cache_key user:profile:%s % user_id data mc.get(cache_key) if data: return data data db.query(select * from user where id%s, user_id) if data: mc.set(cache_key, data, time300) return data这段代码看起来简单但里面藏了好几个细节。第一个是缓存key的设计必须有统一规范:分隔的业务前缀很有讲究user:profile:123比裸写123好一万倍既能避免键冲突又方便出问题时用工具精确删除某类缓存。第二个细节是设置过期时间时要加一点随机扰动比如300秒你写成300 random.randint(0, 60)防止大量key在同一时刻集中过期导致缓存雪崩。4.2 缓存穿透、击穿、雪崩的应对思路使用缓存必然要考虑三个经典问题我把它们和memcached场景对应起来缓存穿透是指查一个必然不存在的数据比如查一个已被删除的文章ID。因为缓存和数据库里都没有每次请求都会穿透到数据库高并发下数据库可能被压垮。应对方案有用布隆过滤器提前拦截或者在查到空结果时也往缓存里写一个占位符比如把空值缓存一分钟而这个空值的过期时间要设置得比正常数据短。缓存击穿是指某一刻某个热点key恰好过期同时大量需要这个key的请求涌入结果全部打到了数据库。解决办法一般有两种思路一是用互斥锁只允许一个请求去重建缓存二是把热点key的过期时间设成永不过期然后由后台任务主动更新值这个方案在集中式缓存系统里更稳。缓存雪崩是指大量key在同一时间段失效导致一批请求全部到达数据库。原因是前面说的过期时间太整齐解决办法也很简单初始化缓存时把过期时间打散加一个随机偏移量。这三个问题面试常问工作中也一定会遇到我的建议是不要死记概念画一下请求流向图搞清楚数据在哪个环节断档自然会知道在哪个环节补措施。4.3 PHP项目接入memcachedPHP接入memcached用得最多的是php-memcached扩展它基于libmemcached功能比较全。安装方式在CentOS上sudo dnf install -y php-pecl-memcached代码示例这是我在实际项目里常用的写法$mc new Memcached(); $mc-addServer(127.0.0.1, 11211); $key product:detail:1001; $detail $mc-get($key); if ($detail false) { // 从数据库读取 $detail getProductDetailFromDb(1001); $mc-set($key, $detail, 300); }这里有个注意点$mc-get($key)返回false有两种情况一种是缓存不存在另一种是缓存的值恰好就是false。所以如果你要缓存的值本身就是布尔型false建议在外面包一层数组或者用字符串0避免歧义。另一种更稳妥的写法是先用$mc-getResultCode()判断是否命中$detail $mc-get($key); if ($mc-getResultCode() Memcached::RES_NOTFOUND) { // 确实没有缓存读数据库 }PHP的Memcached类还支持设置多个服务器做一致性哈希分布$mc-addServers([ [192.168.1.10, 11211], [192.168.1.11, 11211], [192.168.1.12, 11211], ]); $mc-setOption(Memcached::OPT_DISTRIBUTION, Memcached::DISTRIBUTION_CONSISTENT);多服务器时一致性哈希能尽量减少增删节点时key重新映射的数量不然某台缓存服务器宕机会导致大量缓存key失效数据库压力瞬间飙升。4.4 Python和Java接入简例Python生态里最常用的客户端是pymemcache和python-memcached。我推荐pymemcache纯Python实现维护状态好而且支持连接池from pymemcache.client.base import PooledClient client PooledClient(127.0.0.1, 11211, max_pool_size10) client.set(foo, bar, expire60) value client.get(foo)pymemcache的set过期时间参数是expire单位秒。还有一点要注意pymemcache序列化时默认不处理Python对象你要自己传字符串或者字节实际项目中我一般会配合json做序列化用serializer参数指定目前pymemcache提供pickle和json的序列化模块需要显式声明。Java接入则最常用spymemcached或xmemcached。我举一个spymemcached的例子MemcachedClient client new MemcachedClient( AddrUtil.getAddresses(127.0.0.1:11211) ); client.set(foo, 60, bar); Object value client.get(foo);Java客户端相对重一些但功能完善内置了连接池和失败重连机制。这里提醒一下Java里使用set时存活时间是秒但有些客户端版本接口签名不同有的是int秒有的是long毫秒建议写代码前先看一眼客户端接口定义别想当然。4.5 缓存与数据库的一致性处理使用缓存绕不开一个问题数据库更新了缓存怎么办。常见的策略是Cache Aside模式也就是更新数据库时直接删缓存再等下次读请求来时回填。也许你会想为什么不更新数据库的同时也更新缓存因为更新缓存的时机很难把握高并发下两个线程同时更新同一个key可能出现数据库先更新的值后写进缓存造成新旧数据互相覆盖。删缓存虽然也是在数据库操作后执行但它不怕覆盖问题因为下次读时缓存不存在自然会把最新数据写进去。更稳妥的做法是延迟双删先删一次缓存再更新数据库最后休眠几百毫秒再删一次。这个方案主要针对极端情况第一个删缓存后有旧请求把旧数据回填了缓存然后数据库更新第二次删除就清掉了这份旧数据。实际生产里如果业务允许极短时间的不一致简单的一次删缓存加expire兜底也够用。5. 性能监控与参数调优5.1 内存管理机制Slab Allocatormemcached的内存管理不同于常规的malloc直接分配它用了一套叫Slab Allocator的机制。刚运行时memcached把分配的内存划分成一系列slab类别每个类别里又分出固定大小的chunk。比如slab类1的chunk是96字节slab类2是120字节按倍数递增最到最大chunk上限。存入一个100字节的数据时memcached会把它放进chunk大小能容下它的最小slab类比如120字节的chunk。这个机制的优点是避免了频繁分配和释放内存碎片缺点是会造成内存浪费。如果你大量存入的是1KB大小的数据但memcached的slab增长因子是1.25它可能被分配到1.5KB或者2KB的chunk里浪费率就比较明显。在实际项目中如果数据大小差异特别大比如既有几十字节的小key又有几百KB的大value可以考虑调整-f增长因子或者干脆拆分业务小key和大文件各自用独立的memcached实例内存利用率会高不少。5.2 evictions与LRU当memcached内存满了之后新的写入不会报错而是通过LRU算法把最久没用的数据淘汰掉。stats里evictions字段就是被淘汰的数据条数它和过期不一样过期是时间到了淘汰是内存不够被强制挤掉。我的经验是当evictions开始稳定增长第一反应不是加内存而是检查是不是有些缓存key永远不会再被读取了比如脏数据或者无效会话。把无用key清掉比无脑加内存更理性。如果你确认所有缓存数据都是高价值热点再考虑调大-m参数。memcached还提供了LRU爬虫机制默认情况下过期数据会等被访问时才清除但你可以开启-o lru_crawler参数让后台线程主动扫描并回收过期key占用的空间。开启后能一定程度减少过期数据积压但会带来一点额外CPU消耗机器资源紧的服务器要权衡。5.3 线程模型与连接数调优memcached虽然是多线程处理网络事件但要注意它的线程模型是多个线程分别处理不同的连接不是多线程处理同一个连接上的命令。所以调整-t参数时socket连接会被分发到不同线程单个连接的命令还是串行执行的。stats里的curr_connections如果长期靠近MAXCONN上限先检查是不是有客户端忘了关连接。很多语言里每次请求如果都new一个client而不是复用连接很容易把连接数打满。这时候优化方向是使用连接池而不是盲目调大MAXCONN。连接数开得过大每个连接都有接收缓冲和发送缓冲内存会被无谓吃掉。5.4 命中率优化实战命中率是缓存系统最核心的指标。我手头一个真实case原先某个查询接口的命中率只有70%数据库压力很大。分析后发现问题出在缓存key的设计上接口用的是user:list作为key根本没有把分页参数带进去。也就是说用户翻第1页和第100页缓存里都是同一份数据而这份数据只能展示默认首页的内容。把key调整成user:list:page:1:size:20之后每个分页都有自己的缓存命中率直接升到95%。类似的情况还有很多比如查询条件里不同排序维度、不同省份、不同时间段都应该体现在缓存key里。5.5 用memcstat和memcslap做体检安装了libmemcached-tools之后可以用memcstat快速查看运行状态memcstat --servers127.0.0.1:11211它会把stats信息整理成可读格式比自己在telnet里看舒服。还有一个memcslap工具可以用来自带压测memcslap --servers127.0.0.1:11211 --concurrency100 --execute-number10000它会模拟并发请求打set和get输出QPS和响应时间统计。这是我在上线缓存服务之前必做的基础压测确认机器承受得起预期流量。压测结果如果QPS远低于预期优先检查网络协议栈、连接数和实例本身的内存分配不要一开始就怀疑memcached性能差很多瓶颈其实在客户端侧。6. 常见问题与排查技巧实录6.1 问题速查表我在这个部分把实际踩过的坑整理成一张表遇到类似问题可以先对照问题现象可能原因排查与解决客户端连接被拒绝监听地址不是客户端能访问到的地址防火墙未放行检查-l参数和firewalld规则确认11211端口可达能ping通但连不上11211UDP关闭后TCP未监听服务未启动ss -lntp看监听列表systemctl status看服务状态set返回SERVER_ERROR out of memory内存配置过小或者数据量超出查看stats的limit_maxbytes和bytes评估是否调大-mset返回SERVER_ERROR object too large for cachevalue超过单条最大限制默认1MB压缩大value或者调大-I参数一般建议压缩而不是调大缓存get不到数据但数据库有key拼写不一致过期时间太短用命令行手工get验证key检查代码里key的拼接逻辑缓存数据不更新代码只写缓存不删缓存没有过期时间采用Cache Aside模式更新数据库后主动delete缓存key高并发时数据不准用了get后修改再set非原子操作改用incr/decr做原子计数stats中evictions持续增长内存不足LRU被迫淘汰清理无效key调整过期时间再考虑扩容重启服务后缓存全部丢失这是memcached正常行为不持久化前置做缓存预热或者把需要长存的切到Redisget返回false但代码判定不存在PHP里值本身就是false导致歧义用getResultCode判断是否RES_NOTFOUND6.2 排查实录一连接超时问题有一回线上某个接口突然变慢日志里全是Connection timed out。我先看了memcached的stats发现curr_connections已经顶到1024上限了而total_connections增长异常快说明有客户端在不断建立新连接。后来定位到是某个Java服务升级了连接池配置默认把超时时间从1000ms改成了100ms导致在访问高峰时连接池里的连接还没来得及返回就直接超时报错于是服务频繁重建连接连接数越堆越多最终把memcached连接数打满。解决办法是把超时时间调回合理值同时给连接池设置足够的最大空闲连接。这个案例让我养成了一个习惯每次改缓存客户端的参数都要看一遍curr_connections和total_connections的变化趋势。6.3 排查实录二缓存穿透把数据库打挂另一个case是商品详情页在秒杀活动开始瞬间数据库CPU飙升到100%。现象是缓存里根本没有那些活动商品的key每次请求都直接打到MySQL。排查后发现这批活动商品在管理员后台批量上架时代码没有做缓存预热。活动开始后大量用户瞬间访问新商品缓存里没有数据于是全部穿透到数据库。处理方案分为两步第一步在后台商品上架时主动把商品详情写入缓存提前预热第二步对查询不到的key在缓存里也写一个短暂的空值占位防止恶意遍历或哈希攻击穿透。这两个措施加上之后数据库CPU马上就下来了。6.4 排查实录三内存碎片与Slab分配失衡有一台memcached服务器内存配置了2GB但每次压测到一定程度时set就开始报错。我用stats slabs查看各slab类的chunk使用情况发现某个slab类的chunk分配了大量内存但命中率极低而另一个slab类内存不足导致大量evictions。原因是我们把几百万个小session和一个大JSON混在同一个memcached实例里小数据占的slab类别和大数据占的slab类别内存隔离大JSON的slab类因为chunk大数量不多就吃光了内存。解决方案是把不同大小的数据拆成两个memcached实例分别配置不同的-f增长因子session把-f调到1.15大JSON用默认1.25。拆分后两个实例的内存利用率都健康了set错误也消失了。6.5 安全加固建议前面说了UDP端口默认关闭这里再补充几条安全加固措施。第一监听地址尽量不对外网公开如果业务方必须跨网络访问限制源IP为已知服务网段在云安全组层面做访问控制。第二禁止在公网环境下无认证部署memcached它本身没有内置认证机制暴露出去等于给全网开放了读写权限。第三如果希望加认证可以考虑SASL机制配置比较繁琐但能挡住大部分未授权访问需求。第四定期检查stats里的连接来源如有异常连续建立连接的来源IP要及时封禁。7. 从一个memcached到缓存架构演进7.1 什么时候该引入缓存代理层业务体量小的时候客户端直连memcached服务器没什么问题。但当服务器数量变多客户端需要感知所有缓存节点的变化时管理成本就会显著上升。我见过一个项目里业务方在代码里维护了十几台缓存节点每上下线一台都要改配置发版本非常痛苦。这个阶段可以考虑引入一层代理比如twemproxy或者更现代的方案。代理层负责把key分布到不同缓存节点的路由逻辑客户端只跟代理通信节点变动时只需要在代理层配置热加载业务代码零改动。代价是代理层本身成为新的故障点需要部署多台冗余。7.2 egg缓存抽取与复用再往后走一步缓存架构优化不只能靠堆机器还可以把缓存key的生成规则和失效策略统一成一个单独的模块。我经常建议团队做一层通用的缓存封装对外暴露get(key, expire, loaderFunction)内部逻辑固定先查缓存没有就执行loaderFunction读数据库拿到结果写缓存。这样业务代码里不再散落set/get/delete逻辑长此以往维护成本会低很多排查问题时也只需要看封装层。7.3 与Redis共存的场景拆解很多团队最终会同时使用memcached和Redis而不是二选一。我目前维护的系统就是这么做的memcached负责承载高吞吐的纯KV热点数据比如用户session、查询结果缓存Redis负责需要数据结构的场景如排行榜、队列、分布式锁、布隆过滤器。这种分工的原因很实际memcached在纯KV读写场景下延迟极低且多线程扩展性好代码简单不容易出错而Redis的数据结构丰富可以替代更多手工编码逻辑。运维上两类中间件分别监控memcached关注命中率和驱逐数Redis关注内存使用率和持久化策略故障定位起来反而更清晰。7.4 Quit命令与连接优雅关闭最后提一个容易被忽略的细节文本协议里还有一个quit命令。客户端在不需要连接时应该发送quit或者直接关闭socket而不是让连接挂着等服务端超时。不礼貌的客户端连接会占用curr_connections最终影响其他正常业务的连接。用telnet测试完我习惯输入quit退出生产代码里客户端连接池会自动管理空闲连接但如果你写自定义脚本去连memcached记得在结束前关闭连接。这些都是细节但细节才见经验。7.5 从入门到进阶的路径建议如果看完这篇你打算深入下去我的建议路线是这样的先用stats里的各项指标把内存计算清楚会看slabs的增长情况再试着给现有缓存写一个命中率统计脚本然后模拟缓存穿透和雪崩场景做一次应急预案演练。等这套跑通了可以说你已经具备日常运维和调优memcached的基本功了。另一步可以继续研究内存分配器源码实现比如LRU队列、slab划分逻辑、crawler机制这些都是高性能缓存的经典设计值得反复琢磨。我这个操作过程中最大的体会是别急着上多复杂的架构先把一台memcached的安装、命令行、统计数据吃透再去谈分布式、集群、代理层。缓存系统的问题往往不在性能而在使用姿势和数据设计把这两件事做对了绝大多数问题都不会主动找上门。