说实话,现在聊缓存,十个人里有九个先想到Redis,memcached这个名字好像已经有点“过气”了。但我这两年做Linux运维和系统调优,反而越来越觉得memcached是个该认真学一学的东西。它不复杂,没有持久化,没有复杂数据结构,连认证都得靠外部手段,可正因为简单,它在纯KV缓存场景下的性能极其稳定,内存利用率也相当好算,出了问题基本一眼就能看穿。这篇文章就基于Linux环境,从零开始讲讲memcached怎么装、怎么用、怎么排查,适合刚接触Linux缓存这一块、或者面试前想快速把memcached脉络理清楚的朋友,你可以把它当一份带踩坑记录的入门笔记看。
1. 认识memcached:它到底解决什么问题
1.1 从数据库压力说起
先聊一个我碰到过的真实场景。某个内部运营系统,每天早高峰一堆查询请求打到MySQL上,有些统计类的SQL一张表就几百万行,虽然加了索引,但并发一上来照样把数据库的CPU打到80%以上,慢查询日志里全是同一个查询。后来做的事情很简单:把那些热点数据按key塞到memcached里,查询先走缓存,缓存没有再查库再回填,数据库负载肉眼可见地降了下来。
这就是memcached最核心的价值——给后端的数据库或API接口挡掉重复读的压力。它的定位不是存业务数据,而是存那些“读多写少、稍微丢一点也没关系、可以重建”的热数据,比如用户登录状态、商品详情、配置项、验证码。理解这点很重要,因为很多人一开始就把它当数据库用,存些不能丢的东西,结果一重启全没了,就开始骂这玩意儿不靠谱。
1.2 memcached核心特性与适用边界
memcached是纯内存缓存系统,数据只存在内存里,进程一停数据就没了,它自己也压根不打算帮你持久化。它的几个关键特性,值得一条一条捋清楚:
- 基于key-value的简单结构,value最大默认能到1MB,改参数能调大,但真要存大对象不如直接存文件路径。
- 协议简单,基于文本行协议,客户端随便用telnet连上去就能手动读写数据,调试非常方便。
- 服务端是多线程模型,CPU核多的机器能跑满多核,这点比很多单线程缓存服务在纯KV场景下更占便宜。
- 内存管理用slab allocation机制,按固定chunk大小分桶,避免内存碎片,但也会带来一定的内存浪费。
- 支持LRU过期淘汰和主动过期,用得很省心。
那什么时候该用它而不是Redis?我个人的表是这么分的:
| 对比维度 | memcached | Redis |
|---|---|---|
| 数据结构 | 只有字符串KV | 字符串、哈希、列表、集合、有序集合等 |
| 持久化 | 不支持 | RDB/AOF |
| 多核利用 | 多线程,天然利用多核 | 单线程为主,高版本有一些多线程IO |
| 内存管理 | slab分桶,分配固定 | 多种策略,相对灵活 |
| 适用场景 | 纯KV热点缓存,体积小量大 | 数据结构复杂、需要持久化、发布订阅等 |
入门阶段不要纠结谁取代谁,先把memcached在纯KV缓存上的效率和Linux下的部署方式吃透,后面再学Redis,你会觉得很多东西是通的,甚至memcached的协议和内存模型对理解Redis的内存优化也有帮助。
2. Linux下安装部署:从源码编译到systemd托管
2.1 安装前先想清楚版本和依赖
memcached本身依赖libevent这个事件库,所以装之前要先搞定它。我在CentOS系的机器上一般这么做,先更新、装编译工具链和依赖:
yum install -y gcc gcc-c++ make autoconf libtool yum install -y libevent libevent-devel如果是Ubuntu/Debian系,对应命令是:
apt update apt install -y build-essential libevent-dev有个容易踩的坑:有些精简环境里libevent-devel没装,编译memcached时它会报找不到event.h,卡住半天。遇到这种错误别慌,先确认依赖包状态,再检查是否存在多版本libevent冲突。我在一个最小化安装的CentOS 7上试过,忘了装devel包,编译报错,补上后重新configure就好了。
2.2 编译安装还是用包管理器
我其实两种都试过。用yum或apt直接装最快,版本一般也够用:
yum install -y memcached或者:
apt install -y memcached但如果是生产环境,我更推荐源码编译,因为你可以指定编译参数、装到固定目录,后续多实例部署也方便。官网下载源码包,然后走标准三步:
tar zxf memcached-1.6.21.tar.gz cd memcached-1.6.21 ./configure --prefix=/usr/local/memcached --with-libevent=/usr make make install需要注意一点,如果libevent是自定义路径安装的,configure时一定要用--with-libevent=/your/path指定,不然会提示找不到。编译过程很快,一两分钟的事。装完二进制在/usr/local/memcached/bin/memcached,用-h验证一下:
/usr/local/memcached/bin/memcached -h能看到一大串启动参数说明,就说明装好了。
说到这里,我顺便提一下热词里总有人讲“memcached源码分析”。如果你想深入理解它的内存模型和LRU策略,建议先把1.6.x源码下下来,重点看slab.c和items.c这两个文件。入门阶段不用啃太深,但知道源码结构长什么样,面试时聊起来会扎实很多。
2.3 启动参数解析和内存分配细节
启动memcached最精简的方式就是这样:
/usr/local/memcached/bin/memcached -d -m 64 -u root -l 127.0.0.1 -p 11211 -c 1024 -P /tmp/memcached.pid逐个拆一下,这里每个参数背后都有讲究:
-d:以守护进程方式运行,不加的话关闭终端它就跟着退了。-m 64:给memcached分配64MB内存。这个参数特别重要,它决定了你最多能缓存多少数据,也决定了内存用完后淘汰会不会变得很激进。-u root:指定运行用户。默认很多发行版会让你用memcached用户跑,但你手动测试时用root也行,生产环境建议专门建一个低权限用户。-l 127.0.0.1:监听地址。默认会监听所有地址,如果你这台机器上有公网IP,最好把它改成内网IP或回环地址,不然容易被扫描利用。memcached本身没有认证,暴露在公网基本等于裸奔。-p 11211:监听端口,默认就是11211。多实例部署时每个实例可以分配不同端口。-c 1024:最大并发连接数,默认1024就够测试用。-P /tmp/memcached.pid:写pid文件,方便后面stop或reload。
关于-m这里有个容易忽略的点:64MB是memcached用来存数据的内存上限,它实际占用的物理内存不只是64MB,还包含本身slab分配器的一些开销、连接缓冲、哈希表等等。所以在评估机器内存时,建议给memcached留出一些余量,比如-m 1024时,实际常驻可能是1.1GB上下。我见过有人把-m设成机器物理内存的90%,然后发现SWAP狂涨,机器卡死。建议最多给到机器内存的一半左右。
如果你用systemd管理,可以写一个service文件,方便开机自启。这里给一个常见模板:
[Unit] Description=Memcached Daemon After=network.target [Service] Type=forking ExecStart=/usr/local/memcached/bin/memcached -d -m 256 -u root -l 127.0.0.1 -p 11211 -c 1024 -P /tmp/memcached.pid PIDFile=/tmp/memcached.pid Restart=on-failure [Install] WantedBy=multi-user.target把文件放到/usr/lib/systemd/system/memcached.service,然后:
systemctl daemon-reload systemctl enable memcached.service systemctl start memcached.service这样的好处是以后可以用systemctl status memcached看运行状态,出问题也容易排查。
2.4 如何验证服务真的在跑
服务起来后,先用最基础的命令看进程和端口:
ps -ef | grep memcached ss -lntp | grep 11211如果你用Linux的ss命令看到LISTEN状态的11211端口,说明服务已经监听。再进一步可以用stats命令确认内部状态:
printf 'stats\r\n' | nc 127.0.0.1 11211这条命令会返回一堆以STAT开头的行,包括当前连接数、命中次数、内存使用等。只要能看到STAT uptime在增长,就说明进程是活的。
3. 基础使用:从telnet增删改查到数据过期策略
3.1 命令行手动读写数据
memcached的文本协议非常直观,入门阶段我强烈建议先别急着写代码,用telnet或nc手动读写几组数据,把协议格式吃透。首先连上去:
telnet 127.0.0.1 11211或者用nc:
nc 127.0.0.1 11211连上后,存一条数据用set命令,格式是这样:
set <key> <flags> <exptime> <bytes> <value>比如我想存一个字符串hello到key为name的位置,过期时间设0表示永不过期,然后敲回车:
set name 0 0 5 hello服务端会返回STORED,看到它就说明写入成功了。这里有几个细节容易出错:bytes必须和实际输入字节数完全一致,多一个空格、少一个字符都会失败;输入完value后需要再敲一次回车让服务端确认。我自己刚开始试的时候,经常在这里敲多了空格,返回CLIENT_ERROR bad data chunk,后来才反应过来bytes是字节数不是字符数,中文要按字节算。
我们常用的几个指令整理如下:
| 指令 | 作用 | 示例 |
|---|---|---|
| set | 新增或覆盖 | set name 0 0 5 |
| add | 仅当key不存在时新增 | add name 0 0 5 |
| replace | 仅当key已存在时覆盖 | replace name 0 0 5 |
| get | 取一个或多个key | get name |
| delete | 删除指定key | delete name |
| incr | 对数字value自增 | incr num 5 |
| decr | 对数字value自减 | decr num 2 |
| stats | 获取运行状态 | stats |
add和replace这两个命令在并发场景下很有用:add可以用来实现简单的分布式锁;replace则适合只更新已存在的缓存,避免把刚过期的数据重新写回去。实际项目里,add配合get是防缓存击穿的一个小技巧,后面排查部分我会再细说。
3.2 数据过期时间格式,别踩这个坑
memcached的过期时间有两种格式。如果你设置的时间小于等于30天(准确说是60*60*24*30秒),它就把这个值当成从现在开始多少秒后过期。比如set name 0 60 5,60秒后这条数据会过期。如果设置的秒数超过这个阈值,memcached会将其解释为Unix时间戳,也就是从1970年1月1日到某个绝对时间点的秒数。
我踩过一次坑:想设置一个key在2天后过期,心里算了下172800秒,直接写成set key 0 172800 5,结果发现数据不到几分钟就消失了。排查半天才意识到,我那个环境里的memcached是旧版本,且我给的数值不小心超过了30天上限,被当成了时间戳解析。所以入门阶段记住一个习惯:要么用小于30天的相对秒数,要么干脆用date +%s算出绝对时间戳再填进去,不要自己脑补大数字。
读取数据用get,一次可以传多个key:
get name age city服务端会把命中的key和value分块返回,最后以一个END结束。不存在的key不会出现在结果里,这也是memcached“无脑缓存”的体现——查不到就查不到,不会像数据库那样报错。
3.3 统计信息怎么看
stats命令用得非常多,它是排查缓存命中率和内存状况的第一入口。返回的信息里,我一般重点看这几个字段:
| 字段 | 意义 |
|---|---|
| uptime | 进程运行秒数 |
| curr_items | 当前存的key数量 |
| total_items | 历史累计写入次数 |
| get_hits | get命中的次数 |
| get_misses | get未命中的次数 |
| curr_connections | 当前连接数 |
| bytes | 数据实际占用的字节数 |
| evictions | 因内存不足被LRU淘汰的条目数 |
| limit_maxbytes | 允许使用的最大内存字节数 |
命中率可以自己算:get_hits / (get_hits + get_misses)。我一般要求线上命中率至少到90%以上,如果低于80%,说明缓存价值不大,要么key设计有问题,要么过期时间太短。evictions这个值要是持续增长,说明内存不够用了,得考虑调大-m或优化key数量。
除了基础stats,还有stats slabs和stats items,分别查看slab分桶情况和每个桶里的item数量。入门阶段理解即可:memcached会把不同大小的value分配到不同chunk的slab桶里,如果value大小分布不均匀,比如全是1KB到2KB的数据,但slab桶划分得不对,就会有一部分内存被浪费。这是它内存碎片率高的根源,想深入再研究,初级应用只要知道有这回事就行。
3.4 多实例部署的常见思路
一台机器跑多个memcached实例,在生产环境并不罕见,特别是你要对不同业务线做隔离的时候。做法很简单:每个实例分配不同端口和不同的-m内存值,比如订单缓存跑11211端口给2GB,用户会话跑11212端口给1GB。多实例之间互不干扰,一个崩了也不影响另一个。
但要注意,多实例不等于集群。memcached本身没有原生的集群模式,所谓集群一般是客户端根据key的哈希值路由到不同节点,或者你前面再挂一层代理。入门阶段别被“集群”这个词吓到,先把单机多实例玩明白,后面理解一致性哈希就顺理成章了。
4. 常见问题与排查技巧实录
4.1 连接不上、拒绝访问怎么查
这是新手上路问得最多的一个问题。我按顺序给一个排查思路:
第一步,先确认进程是活的:
ps -ef | grep memcached如果进程不在,看/var/log/message或systemd日志:
journalctl -u memcached -n 50常见原因是启动参数里指定的-m超过系统可用内存或-l绑定地址写错。
第二步,确认端口在监听:
ss -lntp | grep 11211如果没看到,确认启动时是不是加了-d,有时候你手动前台启动、然后Ctrl+C把进程带退了,端口自然就没了。
第三步,测试本机能不能连通:
nc -vz 127.0.0.1 11211返回Connected说明本机没问题。如果本机能通、外部机器不通,90%是防火墙挡住了:
iptables -L -n | grep 11211 firewall-cmd --list-all需要放行就执行:
firewall-cmd --permanent --add-port=11211/tcp firewall-cmd --reload还有种情况是云服务器安全组里没有放行11211端口,这个也要检查。
4.2 内存暴涨和淘汰策略
有时候你会发现memcached进程占用的内存比-m配置多很多,或者stats里evictions值一直在涨。前者通常是slab分配器的问题:每个slab桶的最小chunk大小是固定的,如果放入的value尺寸稍大于某个chunk,就会分配下一档更大的chunk,造成内存浪费。举个例子,你存一个900字节的value,memcached可能把它放进1024字节的chunk桶里,每个item浪费124字节,数据一多内存“虚胖”就很明显。
解决思路有两个:一个是尽量把缓存value的大小控制在相似范围,避免极端尺寸;另一个是用-n参数调最小item大小,用-f参数调增长因子,这两个参数入门阶段可以不动,但要明白它们影响内存利用率。
至于evictions增长,说明内存用完了,memcached按LRU策略把最久没访问的item挤出去。表面上缓存还能用,但命中率会掉。我遇到过一个案例:一台2GB内存的机器,memcached配了1.5GB,然后命中率只有75%,evictions高达几百万。后来把一些很久不用的业务key单独拆分到另一个实例,主缓存只存高频数据,命中率才回到95%以上。
4.3 缓存穿透、击穿和雪崩的实战处理
这三个词面试常问,实际运维中也必须面对。
缓存穿透:查询一个必然不存在的数据,比如拿一个不存在的用户ID查详情,每次都会穿过缓存直接打到数据库。我在项目中一般用两个办法:一是把这类空结果也缓存起来,设一个很短的过期时间,比如30秒,让数据库喘口气;二是在接口层做参数校验,明显不合法的ID直接拒绝,不落到查询环节。
缓存击穿:某个热点key突然过期,大量请求同时打到数据库。这边可以用互斥锁或add命令实现“只放一个请求去重建缓存”的逻辑。具体做法就是当缓存miss时,先尝试add一个带特殊标记的key,如果add成功,说明自己是第一个来重建的,就去查数据库回填;如果add失败,说明别人正在重建,自己就sleep几十毫秒再重试get。
我给一段伪代码风格的示意:
# 伪代码:用add实现单飞重建缓存 cache_key = "hot_data" data = mc.get(cache_key) if data is None: lock_key = "lock:" + cache_key if mc.add(lock_key, 1, 3): # 3秒内其他请求不会重复重建 data = query_db() mc.set(cache_key, data, 60) mc.delete(lock_key) else: time.sleep(0.05) data = mc.get(cache_key)缓存雪崩:大量key在同一时间过期,造成数据库瞬间压力飙升。我常用的规避手段有三层:过期时间加随机值,比如统一60秒过期的key,实际生成时加一个0到30秒的随机偏移,让过期时间错开;热点数据过期时间设置得足够长,并且通过后台任务主动更新;关键接口做好降级方案,数据库扛不住时直接返回兜底数据或限流。
4.4 用代码接入:Python和PHP的简单示例
虽然命令行能读写,实际业务里还是要靠客户端库来操作。这块我以最常用的Python和PHP做例子。
Python下用pymemcache比较轻量,先安装:
pip install pymemcache然后连接和读写:
from pymemcache.client.base import Client client = Client(('127.0.0.1', 11211)) client.set('username', 'zhangsan', expire=60) value = client.get('username') print(value) # b'zhangsan'注意,pymemcache的get返回的是bytes类型,不是str,需要自己做解码。如果你想要返回值自动出来,可以用pymemcache的JsonJsonSerde,但入门阶段先明白字节流的坑就好。
PHP下更简单了,装上memcached扩展:
$mc = new Memcached(); $mc->addServer('127.0.0.1', 11211); $mc->set('username', 'zhangsan', 60); $value = $mc->get('username');PHP的memcached扩展默认会帮你做序列化,存数组、对象都方便,但跨语言读写要注意序列化格式不同的问题。
4.5 自检清单和日常维护小习惯
最后给一份我平时维护memcached时用的检查清单,照着做基本能避免大多数坑:
- 启动参数里
-l是否绑定了正确的网卡,生产环境不要监听0.0.0.0。 -m是否小于物理内存的一半,给系统和其他进程留余地。- 是否设置了pid文件,方便异常退出时快速定位。
- 定期看命中率和
evictions,命中率持续走低就要分析key设计。 - 对公网环境要加防火墙策略,memcached原生没认证,不要裸奔。
- 多实例部署时确保端口不冲突,使用
ss -lntp确认。
我自己在经历过几次线上事故后,养成一个习惯:每次上线memcached配置变更,都会在变更前记录一次stats快照,变更后再快照一次,对比命中率、占用内存、淘汰数这几个指标。别看动作小,出了问题定位速度能快不少。
5. 写在最后的一点体会
我在学习和使用memcached的过程中最深的感受是:越简单的东西越容易用错,但一旦用对了,收益立竿见影。很多人觉得memcached太单调,不值得花时间,但它的内存模型、淘汰策略和文本协议,恰恰是理解其他缓存系统的基础。你如果能在Linux上徒手部署、用命令行调通、再通过stats指标判断缓存健康状态,这笔技能就算真正落地了。后续想继续深入的话,建议看看slab分配器的实现思路,再对比一下Redis的内存编码方式,会有种豁然开朗的感觉。