
1. 从“PHP操作redis”这个需求开始先说一下我为什么会写这篇东西。这几年做PHP后端几乎没有哪个项目能绕开Redis。最开始我也只是拿它存个验证码、存个用户登录态觉得不就是set和get嘛文档一翻就会。直到后来做商城秒杀、做消息队列、做排行榜才发现如果只停留在set/get这个层面Redis在PHP项目里真正能发挥的价值连一半都用不到。“PHP操作redis”这个标题看起来简单背后的技术点其实很密phpredis扩展和Predis的选型、五种数据类型的PHP侧封装、管道与事务的配合、分布式锁的边界问题、缓存与数据库的一致性问题、主从和集群下的读写策略、key的设计规范。更别提那些藏在运行时的坑比如连接数打满、序列化后数据占用过大、Big Key阻塞、keys *导致线上卡死。这些我在不同项目里基本都见过有的我亲手踩过有的帮同事排查过。这篇文章适合正在用PHP做Web开发的人不管是刚接触Redis还是已经在用但总觉得“差点意思”的同学。我会按项目落地的顺序来写先讲环境与扩展选型再逐个数据类型给可运行的PHP代码然后是三个高频场景的完整实现缓存、锁、队列最后把我和身边人踩过的坑整理成速查。看完你可以直接照着把一套能用的Redis封装搬到自己的项目里去。如果一定要给这篇文章定个目标那就是让一个会用$redis-set(key,value)的PHP开发者看完之后有底气去处理高并发扣库存、缓存穿透、队列削峰这类真实业务。2. 方案选型与整体设计思路先别急着写代码2.1 为什么是Redis而不是MySQL硬扛或者用文件缓存很多新手最喜欢问的问题其实是我的数据量没多大为什么非要引入Redis答案不在于“快”这个字而在于访问模型。MySQL的数据在磁盘上虽然也有缓冲池但每一次查询都要经过SQL解析、权限检查、执行计划、存储引擎的多次磁盘/内存交互。即使平均1毫秒在高并发下连接数一上来数据库连接池和锁竞争会迅速放大延迟。而Redis的数据在内存里单线程事件循环处理请求读写都是微秒级而且IO模型是I/O多路复用单实例可以轻松扛住10万的QPS。用一个生活类比MySQL像是去档案室查纸质文件再快的档案管理员也得一趟趟跑Redis像是你办公桌上贴的便利贴抬眼就能看到但便利贴也意味着空间有限东西丢了也不心疼——所以Redis注定是缓存和热数据的层不是全量数据的归宿。这里要强调一点方案选型不是“用Redis替代MySQL”而是“让Redis帮MySQL挡住90%的重复读流量”以及“用Redis的原子操作解决并发写的问题”。所以我会建议团队在设计缓存时把数据回源、过期策略、一致性方案一起想清楚而不是先装了Redis再说。2.2 phpredis扩展和Predis客户端怎么选PHP操作Redis主流方案有两个一个是C语言写的phpredis扩展通过Redis类直接调用另一个是纯PHP实现的Predis通过Composer安装。我个人的建议是生产环境优先用phpredis扩展本地调试或者要求部署极简的时候可以用Predis。原因有三phpredis底层是C实现调用开销极小函数名和Redis命令基本一一对应$redis-hSet(key,field,value)这种写法非常直观。Predis使用Composer安装无需在PHP环境里启用扩展但它每次请求都有PHP函数调用层级和IO封装的开销性能大约是phpredis的70%左右严格压测时差距更明显。phpredis天然支持Redis的序列化选项Redis::SERIALIZER_PHP、Redis::SERIALIZER_JSONPredis需要自己json_encode/json_decode麻烦不说还容易漏。选型后有一个关键动作检查PHP版本和Redis扩展版本的兼容性。比如PHP 7.4对应phpredis 5.xPHP 8.0以上推荐phpredis 6.x。如果用太旧的扩展连接新版本的Redis 7.x某些命令比如ACL相关可能不兼容但日常的get/set影响不大。2.3 环境搭建Linux和Windows分别怎么装Linux环境下的扩展安装我习惯用pecl两条命令pecl install redis echo extensionredis.so /etc/php.ini如果是用Docker部署PHP一般PHP镜像直接docker-php-ext-install redis也行不过需要提前把redis.tgz下载并解压到/usr/src/php/ext目录下。Windows下的情况稍微特殊一点。要特别注意PHP在Windows下是线程安全TS和非线程安全NTS两套版本扩展必须与PHP版本对应否则php -m里加载会直接报错。我从windows.php.net/downloads/pecl/releases/redis/下载对应php_redis.dll然后放进ext目录在php.ini里加上extensionphp_redis.dll。这里有个坑PHP 8.x在Windows上只能用phpredis 5.3.x以上的版本低版本编译方式不匹配会提示“Unable to load dynamic library”。Redis服务端安装比扩展简单Linux直接apt install redis-server或yum install redisWindows下虽然官方不提供原生版本但微软维护过一份移植版或者用WSL跑Linux版。我建议本地学习时直接用Docker跑一个最省事docker run -d -p 6379:6379 --name redis-demo redis:7-alpine装完先别急着写业务代码我会习惯先跑一条命令验证连通性redis-cli ping返回PONG说明服务正常。这一步能帮你把问题范围缩小到“服务端没起来”还是“PHP扩展没加载”省得后面调代码时一脸迷茫。3. 核心数据类型与PHP实操要点3.1 五种基础类型的PHP封装与业务映射Redis的数据类型不止五种但日常99%的场景都跑不出这五种String、Hash、List、Set、ZSet有序集合。如果把这五种理解了基本就理解了Redis的数据模型。String最基础的键值对适合存验证码、接口返回值、计数器和简单的锁。PHP里最常见的用法$redis-set(user:10001:nickname, 张三, [EX 3600]); $nickname $redis-get(user:10001:nickname);注意第三条命令还加了过期时间。我特别建议所有缓存key都设置过期时间否则一旦业务侧的逻辑有bugRedis内存会被脏数据吃光。Hash适合存对象、用户资料、商品信息它比String的优势在于可以只修改其中一个字段不用整个字符串序列化/反序列化。看这个例子$redis-hMSet(product:10086, [ title 一个标题, price 99.9, ]); $redis-hIncrBy(product:10086, sold_count, 1);hIncrBy这种原子增加操作在后续做计数器、库存时非常有用。List底层是链表结构左侧写入右侧读取非常适合做消息队列的简单版也适合做“最新N条”的列表。比如用户最近浏览记录$redis-lPush(user:10001:history, $productId); $redis-lTrim(user:10001:history, 0, 9); // 只保留最近10条lTrim是关键它决定了List不会无限增长。Set无序字符串集合自带去重。适合做标签、关注关系、共同好友这类场景还支持交并差集运算一个SINTERSTORE就能算出两个用户的共同关注这在MySQL里是好几条关联查询。ZSet这是我认为Redis最被低估的类型。有序集合每个成员带一个分数score天然就是排行榜。比如游戏积分排行$redis-zAdd(rank:global, $score, $userId); $top10 $redis-zRevRange(rank:global, 0, 9, true);按分数从高到低取出前十名一条命令搞定。它的底层是跳表插入和查询的时间复杂度都是O(logN)性能非常稳定。3.2 序列化的选择存PHP数组到底怎么存这个坑我见过太多次。新手最爱做的是$redis-set(cart, $array)然后取出来发现是假的“Array”或者直接报错。原因很简单Redis只存字符串数组和对象必须序列化之后才能存。三种常见方式serialize()/unserialize()PHP原生序列化能还原类型和中文但数据带类型标识结构较重。json_encode()/json_decode()最通用跨语言也好读但注意浮点精度和空数组变成[]的问题。我一般取的时候会强制转数组(array) json_decode($str, true)。phpredis自带的序列化器在连接初始化时设置$redis-setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP)之后set数组时扩展自动序列化get时自动反序列化。这个方式最好用但也会带来一个隐患一旦计划把缓存数据交给其他语言Go、Java消费就很容易出现别人解析不了你的PHP序列化格式的情况。所以我的经验法则是如果Redis数据只给PHP自己读写用Redis::SERIALIZER_PHP最舒服如果Redis数据要跨服务共享统一用JSON。这个决定最好在项目初始化时定下来后面改起来极其痛苦。另外还有一个容易被忽略的点序列化后的字符串长度。我用strlen()测过一个包含20个字段的数组serialize()和json_encode()体积能差30%以上。在内存型数据库里少1KB乘以10万key就是100MB内存这点差距不能无视。3.3 连接池与长连接每次new Redis()太浪费很多人写PHP操作Redis习惯在代码里每次new Redis()用完就销毁。这在单次操作量小的场景下看不出问题但压测或者业务量上来之后TCP握手和Redis连接释放的开销会被无限放大。实测数据一次new Redis() connect close大约会多出0.3ms-0.8ms的开销对一个需要10次Redis操作的接口来说就是3ms-8ms的额外耗时。PHP没有常驻内存的概念每个请求结束后变量都会被释放所以“连接池”在PHP传统模式下很难实现。但是phpredis提供了pconnect()方法也就是长连接它会复用同一个TCP连接减少握手开销。示例$redis new Redis(); $redis-pconnect(127.0.0.1, 6379, 2.5); // 最后一个参数是超时时间 $redis-auth(yourpassword);用pconnect有几个前提Redis服务端timeout配置要设置合理否则空闲连接可能被服务端断开PHP-FPM的pm.max_children连接数要评估因为每个PHP-FPM进程会保持一条连接如果走的是Nginx代理或负载均衡还要注意连接在中间链路是否会被回收。我在生产里更常用的做法是如果是公司内部服务把Redis连接对象放到一个单例类里配合FPM长驻进程复用连接。但说实话当QPS真正高到需要精确控制连接时团队大概率会考虑Swoole或Workerman这种常驻内存方案那就完全是另一个话题了。在这之前pconnect已经能解决大部分连接开销问题。3.4 管道与事务性能提升和一致性怎么权衡再讲两个提高“PHP操作redis”效率的手段pipeline管道和multi事务。Pipeline是把多个命令一次性发给Redis减少网络RTT。在网络开销是主要瓶颈的场景下用管道能把性能提升5-10倍。PHP里这样用$pipe $redis-pipeline(); for ($i 0; $i 1000; $i) { $pipe-set(key: . $i, $i); } $results $pipe-exec();注意exec()返回的是每条命令的结果数组。这里有个细节phpredis的pipeline()和multi()的调用方式几乎一样但语义完全不同。pipeline不保证原子性它只是减少发包次数multi/exec则是真正的事务中间任何一条命令出错其它命令也不会执行。事务和RDBMS事务不是一回事。Redis事务不支持回滚也不会在你修改数据时锁定其它连接的操作。它的保证只有两条事务内命令按顺序执行事务执行期间不会被其他命令插入。所以如果在事务里先get一个值再根据值做set这个判断过程依然存在并发风险。要真正实现“判断更新”的原子性得用Lua脚本Redis内置脚本执行机制这个在讲分布式锁的时候会具体演示。如果在网上搜“php redis事务”会看到很多multi()的写法但我建议大多数场景直接上Lua或者单个原子命令只有那种“必须一串命令一起执行”时才用multi/exec。4. 高频场景完整实现缓存、锁、队列与排行榜4.1 商品详情页缓存穿透、击穿、雪崩的完整对策先从最常见的缓存模式说起。缓存商品信息的基本逻辑是“先查缓存缓存没有就查数据库查到后写回缓存设置过期时间”。但真正上线之后你一定会在某个深夜收到报警数据库CPU飙到100%。原因大概率就是缓存穿透、缓存击穿或缓存雪崩之一。缓存穿透请求了一个不存在的商品ID缓存永远没有数据库也永远查不到于是每次请求都打到数据库。解决方法是把空结果也缓存起来设置一个较短的过期时间比如60秒$info $redis-get(product: . $id); if ($info false) { $product $db-getProductById($id); if (empty($product)) { $redis-set(product: . $id, , [EX 60]); } else { $redis-set(product: . $id, json_encode($product), [EX 3600]); } }注意这里get返回false时有两种情况key不存在或者key的值就是false/空的字符串。所以缓存空值时建议显式处理空字符串别用false当值。缓存击穿某个热点key在大量请求同时到达时正好过期所有请求都穿过缓存打到了数据库。解决办法是让“重建缓存”的动作并发可控可以用互斥锁见4.2或者用逻辑过期把过期时间写在value里异步重建实现复杂一些但更稳。缓存雪崩大量key在同一时间段集体过期导致数据库瞬时压力变大。解决办法很简单过期时间加随机值不让它们“手拉手一起过期”。$expire 3600 mt_rand(0, 300);这个技巧我几乎每次都会写在项目里属于花钱最少效果最明显的保护措施。4.2 分布式锁与库存扣减别用get-then-set先抛一个经典错误代码// 错误做法 $count $redis-get(stock:10086); if ($count 0) { $redis-decr(stock:10086); echo 扣减成功; }这段代码在并发下一定会超卖因为get和decr之间不是一个原子操作。两个请求可能同时读到库存还剩1然后都执行decr最终库存变成-1。正确的做法是直接用Redis的原子命令比如decr本身就是原子操作所以判断库存可以直接$remain $redis-decr(stock:10086); if ($remain 0) { // 恢复库存提示失败 $redis-incr(stock:10086); echo 库存不足; }这里利用了一个关键点decr返回值是自减之后的最新值这个值在并发下不会重复。只要判断返回值是否小于0就能准确知道本次扣减是否成功。如果失败就补回去因为正常情况下只有最后一个超卖请求才会得到负数。另一种常见方案是Lua脚本。将“检查并扣减”原子化-- lua脚本 if redis.call(get, KEYS[1]) - tonumber(ARGV[1]) 0 then return redis.call(decrby, KEYS[1], ARGV[1]) else return -1 endPHP侧$lua LUA if redis.call(get, KEYS[1]) - tonumber(ARGV[1]) 0 then return redis.call(decrby, KEYS[1], ARGV[1]) else return -1 end LUA; $result $redis-eval($lua, [stock:10086, 1], 1);eval的最后一个参数是key数量告诉Redis前面几个参数是key。这个细节容易写错写错会直接报ERR wrong number of arguments for eval command。分布式锁则是另一个经典场景秒杀活动中多个PHP-FPM进程可能同时操作同一个订单号。在单机时可以用文件锁分布式环境就得用Redis锁。一个可用的锁实现要考虑三点加锁要设置过期时间防止死锁释放锁时要校验持有者获取锁的等待要控制在合理范围。// 加锁 $lockKey lock:order: . $orderId; $token uniqid(, true); // 每个操作者唯一的标识 $locked $redis-set($lockKey, $token, [NX, EX 10]); if (!$locked) { throw new Exception(操作太快请稍后再试); } // 业务处理... // 释放锁用Lua保证比较与删除的原子性 $lua if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; $redis-eval($lua, [$lockKey, $token], 1);为什么删除锁要用Lua脚本而不是先get再del因为如果某个操作者的锁已经超时自动释放另一个操作者拿到了锁这时第一个操作者如果直接del就会把别人的锁误删了。用Lua脚本先比较持有者token再删除才能避免这个问题。这个细节是面试高频题也是线上事故的高发点。4.3 基于List的轻量消息队列与延迟队列思路当你需要“削峰填谷”的时候不一定立刻上RabbitMQ或Kafka用Redis的List也能扛住中等规模的流量。PHP端生产者把任务塞到列表右侧消费者从左侧取出// 生产者 $redis-lPush(queue:email, json_encode([to userexample.com, subject hello])); // 消费者在CLI脚本中循环 while (true) { $task $redis-brPop([queue:email], 3); // 阻塞3秒等待 if ($task) { // 处理任务 } }brPop是阻塞弹出比lPop多一个超时参数避免消费者轮询浪费CPU。这里有个容易被忽略的问题如果任务处理失败任务就已经从队列里丢了。所以更健壮的做法是用rPopLPush先把任务从主队列移到“处理中”队列处理成功后才从“处理中”队列删除超时没处理的再重新入队。PHP 7.0以上的phpredis原生支持rpoplpush$task $redis-rpoplpush(queue:email, queue:email:processing); // 处理成功 $redis-lRem(queue:email:processing, $task, 0);说白了就是把“至少一次处理”变成了可靠投递的基础版。如果任务允许丢失直接brPop就够了。延迟队列更高级一点可以用ZSet实现score存“要执行的时间戳”消费者定期查score小于当前时间的数据再处理。比如订单未支付超时关闭// 10分钟后关闭 $redis-zAdd(delay:order, time() 600, $orderId); // 消费者定时扫描 $dueOrders $redis-zRangeByScore(delay:order, 0, time()); foreach ($dueOrders as $orderId) { // 关闭订单 $redis-zRem(delay:order, $orderId); }注意ZSet是按score排序的zRangeByScore取出的数据正好是按时间从小到大排列天然就是“先到先处理”。这套方案我在订单超时关闭场景里跑过数据量在百万级别以下都挺稳的。4.4 排行榜、计数器与布隆过滤器排行榜用ZSet已经讲过这里补充一个细节分数相同时如何排序。比如游戏排行两个人分数一样希望先达到该分数的排在前面。做法是让score带上小数位整数部分是分数小数部分是时间戳的倒数值。或者直接score总分*10000 (某时间基数-时间戳)这样分数越大越靠前时间越早越靠前。计数器更是Redis最基础的应用incr/decr天然原子。比如统计接口访问次数、用户签到连续天数、积分变化。我曾经用Redis给一个报表系统做了分钟级和天级计数器每天10亿次写入Redis单实例扛住了只需要定期把增量刷到MySQL归档。布隆过滤器算是一个进阶用法适合判断一个数据“大概率不存在”。比如防止恶意注册的手机号反复查询、防止缓存穿透时对大量不存在的ID做判断。PHP里可以用phpredis的bf系列命令Redis 4.0之后带布隆过滤器模块设置误判率1%预期数据量10万初始化过滤器然后bfAdd添加、bfExists判断。它能把“查询数据库判断是否存在”变成一次内存判断对高并发防穿透非常有效。不过要注意布隆过滤器删除元素不方便适合只增不减的场景。5. 性能优化、键设计与安全加固5.1 键命名规范与内存控制有一次我给同事排查问题发现Redis内存涨了2GB。查了半天发现是每条用户操作日志都用了一个全新的key永远不过期日积月累把内存塞满了。从那之后我要求团队必须遵守几个键设计规则用冒号分层级业务:实体:ID:字段比如user:10001:profile、order:20241001:list。根据业务明确过期时间避免“永不过期”的key泛滥。标准做法是建立一张“key资产表”记录每个key的用途、过期时间、负责人。控制key的长度key本身也占内存尤其当key数量达到百万级时每多一个字节都是浪费。避免Big Key。一个Hash里有几万个字段、一个List里有上百万条数据都会导致Redis在操作这几个key时出现明显阻塞。可以用hscan分批扫描、用ltrim裁剪、或者将大key拆分成多个小key。检查Big Key的习惯我建议一直开着用redis-cli --bigkeys就能扫描全库。我自己踩过最大的坑是某个日志队列List积压了几百万条任务某个时间点消费端恢复后Redis主线程执行llen和lrange时直接卡了十几秒。因为Redis是单线程一条慢命令会阻塞后面所有命令这就是“一个key拖垮整个实例”的现场。5.2 慢查询和延迟监控Redis自身提供慢查询日志设置阈值CONFIG SET slowlog-log-slower-than 10000 # 单位微秒10毫秒 CONFIG SET slowlog-max-len 128然后通过SLOWLOG GET查看慢命令。PHP侧我习惯封装一个超时检测调用命令前后记录microtime(true)超过阈值记录日志。这个做法在排查线上“偶发超时”时特别有用。还要注意网络开销。Redis性能极高但PHP走到Redis之间的每一次RTT在跨机房场景下可能是1-2ms。如果一次请求要串行调用十几个Redis命令光网络耗时就能达到几十毫秒。这时候可以考虑把多个命令合并到pipeline或者把数据一次性通过MGET取出而不是循环GET。5.3 访问控制与危险命令日常开发中有几个命令在线上要极其小心KEYS *会阻塞Redis、FLUSHALL/FLUSHDB清库、MONITOR会消耗性能、SORT大数据集下可能阻塞。我习惯在Redis配置里直接禁用或重命名rename-command KEYS rename-command FLUSHALL rename-command FLUSHDB 同时设置密码requirepass并且在云服务器上不要把Redis端口(6379)暴露到公网。我曾经遇到过某公司因为Redis没设密码、6379端口对公网开放被拖库加勒索的案例。Redis本身没有太强的权限体系更不会像MySQL那样细粒度管理用户权限所以网络层隔离才是第一道防线密码是不得已的辅助。5.4 主从、哨兵与集群的PHP侧配置当单台Redis扛不住或需要高可用时常见的演进路径是主从复制 → 哨兵模式 → Cluster集群。主从模式下PHP的配置一般是主库负责写从库负责读。phpredis没有自动读写分离需要自己在代码里维护一套逻辑。我的做法是封装一个RedisManager类内部维护两个连接主和从写操作走主连接读操作走从连接并定期检查从库的健康状态。不过这里有个严重警告主从复制有延迟。如果你刚写入主库的数据立刻从从库读可能读到旧值。所以强一致性的数据比如扣减库存后的余额绝不能走从库。哨兵模式的PHP连接phpredis 5.3以上支持RedisSentinel类$sentinel new RedisSentinel(127.0.0.1, 26379); $master $sentinel-getMasterAddrByName(mymaster); // 得到主库地址后建立连接Cluster模式更复杂phpredis有自己的RedisCluster类它会自动处理分片路由不需要在客户端手动选库$redis new RedisCluster(NULL, [node1:7000, node2:7000], 1.5);注意RedisCluster模式下事务和pipeline的限制很多跨slot的multi/exec是不允许的。这个限制会让很多从单机迁移过来的PHP代码直接报错所以集群化不是简单的“改个连接串”而是要对key做slot层面的规划用Hash Tag可以让某些key落到同一slot。6. 实操经验与问题排查实录6.1 连接失败php_redis.dll加载不了、Connection refusedWindows下最常见的是扩展加载不了。表现为php -m看不到redis或者直接报PHP Warning: PHP Startup: Unable to load dynamic library php_redis.dll。排查步骤就三条确认PHP版本用php -v查看是TS还是NTS是64位还是32位。下载对应版本的dll注意phpredis 5.x版本还要求本机有Visual C Redistributable运行库缺失会报缺失msvcr110.dll之类的错。把dll放到php.exe同目录的ext下php.ini里配置extension_dir并加上extensionphp_redis.dll重启PHP服务。如果是Linux下连接不上Redis服务大概率是Redis只监听了127.0.0.1或者防火墙拦了。先redis-cli ping验证服务本身再telnet 127.0.0.1 6379验证网络通不通。如果是Docker容器之间连接注意别写成localhost要用容器名或服务名。6.2 数据读到null或一段奇怪的字符串这个问题多半出在序列化方式不一致。比如你用$redis-set(k, json_encode($arr))写入读取时用了unserialize($redis-get(k))必然产出垃圾数据。或者你把setOption配成了Redis::SERIALIZER_PHP但Redis Deshift Manager里看到的是带s:5:value;前缀的串这都是正常的PHP序列化格式只是肉眼直观上不太友好。解决方案是统一封装。我推荐在项目里预定义好一套全局的Redis代理类把所有set/get都走封装封装内部统一序列化策略。这样就算将来调整方案也只需要动一个文件。还有一种情况是get返回false但数据明明存在。这个问题出在值本身是false或空字符串或者你把值设置成了0但用了strlen判断。我的建议是判断key是否存在用$redis-exists(key)而不是靠get的结果真假来判断。6.3 并发抢购场景库存超卖、锁失效先说库存超卖。这个我已经在4.2给了两种原子方案核心就是不要“先取后判”要用decr或者Lua让检查和扣减合并成一个不可分割的动作。线上还有一种执行埋点decr之后发现小于0补回去的瞬间可能又有一个新请求在decr导致库存补错。这种情况我一般会把“恢复库存”也通过Lua和标志位一起做或者直接把库存初始值预减一部分作为缓冲区不再恢复。锁失效的问题常见于加锁后业务执行时间超过了锁的过期时间。你以为锁还在其实已经自动释放了别的请求就进来了。解决办法有几种过期时间设得足够长给业务预留3-5倍余量或者在业务代码里同时维护一个“并发标记”锁超时后把标记打成false让第二个请求感知到更保险的做法是用Redisson式的看门狗续期机制PHP里没有官方看门狗我一般用一个独立进程定时给锁续期或者直接把过期时间设成15秒业务内单次操作控制在2-3秒内完成。6.4 Redis内存飙升与OOM这是个非常常见的线上问题。优先检查以下几步redis-cli info memory看used_memory、maxmemory、mem_fragmentation_ratio。redis-cli --bigkeys找大key找出罪魁祸首。检查有没有key没有设置过期时间。Redis不限制你存多少key直到内存被打满。用INFO keyspace看key数量再用samples抽样看TTL情况。maxmemory-policy的配置决定了内存满时怎么淘汰我一般建议allkeys-lru全部key按LRU淘汰或者allkeys-random。注意如果用了noeviction内存满了直接写不进去业务会开始报错。还有一个小细节Redis内存碎片率mem_fragmentation_ratio如果长期大于1.5说明内存碎片严重可以考虑重启实例或者调大jemalloc的预分配。碎片率小于1.0则说明可能触发了swap这是性能杀手得赶紧检查系统内存和SWAP使用情况。6.5 持久化策略RDB和AOF该选哪个FAQ里经常有人问容器重启之后Redis数据全没了怎么办这取决于持久化配置。Redis有RDB快照和AOF追加日志两种方式。RDB性能好加载快但可能丢失最近一次快照后的数据。适合对数据一致性要求不高的缓存场景。AOF每个写操作都追加日志数据安全性更高但文件体积大恢复速度慢。可以设置appendfsync everysec每秒刷盘一次性能和数据安全性之间取一个平衡。混合持久化Redis 4.0RDB作为基础快照加上AOF增量日志兼顾了两者优点。我的经验是如果Redis只做缓存丢了可以回源那完全关闭持久化都行save 、appendonly no如果Redis里有订单号、计数器这类不能丢的数据至少开AOF并设置为everysec。别指望Redis像MySQL一样可靠地长期存核心数据它更适合“丢了能从别处恢复”的场景。最后分享一下我个人的操作习惯每次在项目里引入Redis时先花半小时写一个统一的Redis工具类覆盖连接、序列化、过期时间管理、慢查询监控和日志后面的业务开发效率会高很多。很多看起来很复杂的坑其实都是因为在项目初期没有做好这层封装每个人各自为政地new Redis()导致序列化、重试策略、监控全都不统一。这篇内容里提到的每一种方案和坑都是我在实际项目里验证过的希望能帮你少踩几个。如果后续有时间我再写一篇关于Redis 7新特性在PHP里的落地比如CLIENT CACHING、SHARDED PUBSUB以及如何用Swoole把Redis性能压到极限的实战内容。你如果有在自己的项目里遇到其他奇怪的Redis问题欢迎一起交流这种问题往往越是冷门越有意思。