news 2026/9/12 4:14:42

Redis OOM报错排查:maxmemory与内存淘汰策略实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis OOM报错排查:maxmemory与内存淘汰策略实战解析

我在线上遇到过一次特别有代表性的Redis故障:业务量一上来,客户端就开始成片地抛异常,核心报错就一句话——OOM command not allowed when used memory > 'maxmemory'。新同学第一次看到这行英文基本都是懵的:Redis不是号称高性能缓存吗?怎么还会OOM?

实际上这不是操作系统把Redis进程杀了,而是Redis自己触发了内存保护:当已用内存超过配置的maxmemory上限,同时内存淘汰策略又不允许自动清理旧数据时,新的写命令就会被直接拒绝。换句话说,Redis还活着,但已经“写不进去了”。

这篇文章就从这个报错出发,把maxmemory、内存淘汰策略、大Key排查、业务侧治理、容器化部署这些内容完整串一遍。适合正在排查线上Redis内存告警的后端和运维同学,也适合刚接手一个“缓存总是被塞满”的旧系统、想彻底搞清楚内存管理机制的开发者。我会尽量按排查现场来还原思路,把能直接抄作业的命令和参数都放出来。

1. 先搞懂这条报错:Redis到底在拒绝什么

1.1 OOM不等于进程崩溃

首先要区分两个OOM。操作系统的OOM Killer,是物理内存彻底耗尽后由内核挑进程杀掉;而Redis的OOM是逻辑层面的“软限制”。

Redis在启动时会读取maxmemory配置,之后每个命令进来,主线程都会先算一笔账:当前used_memory有没有超过maxmemory。如果超了,再检查当前maxmemory-policy是什么策略。如果策略是noeviction,也就是“一个key都不淘汰”,那Redis就只能对写命令说“不”。

所以你会看到典型现象:服务不宕机、INFO还能查、读请求也正常,但只要涉及写入,比如SETLPUSHHSETSADD,客户端就收到这段OOM错误。这是Redis牺牲写入能力、保住核心进程存活的一种自我保护设计。

1.2 maxmemory:一个经常被忽略的全局阀门

maxmemory默认值是多少?在64位系统上,默认是0,意思是不限制。很多团队一开始部署Redis时根本没管这个参数,数据量小还好,一旦业务增长、key没有过期时间,内存就会一路涨到系统物理内存耗尽,最后被操作系统OOM Killer直接干掉。

生产环境一般都会显式设置maxmemory。设置成多大不能拍脑袋,需要结合机器物理内存、同机有没有部署其他进程、是否开启了RDB或AOF持久化来综合判断。

举个例子,一台16GB内存的机器,假设Redis独占且开了AOF,我一般会把maxmemory设置在10GB到12GB之间,至少留出25%~30%的内存给操作系统page cache、fork子进程和连接缓冲。不建议把maxmemory直接设成物理内存大小,比如16GB,因为一旦AOF重写或RDB快照fork子进程,内存瞬时翻倍,系统直接卡死。

maxmemory的配置格式也容易写错:

# 可以直接写字节数 maxmemory 10737418240 # 也可以用这种更可读的格式 maxmemory 10gb

注意是10gb,不是10GB,Redis配置解析器对单位大小写不敏感,但用全小写最保险。运行时改的话用CONFIG SET maxmemory 10gb,不过只对当前实例生效,想持久化还要CONFIG REWRITE

1.3 为什么只有写入命令被拒

很多人会问:内存满了,为什么GET还能用?因为读命令不会让内存继续增长,放行读请求是合理的。而SET这类命令必然带来新数据,在noeviction策略下没有任何key可以被清理,Redis只能拒绝执行,防止内存彻底失控。

这里有个容易被忽略的细节:想快速“救急”的时候,删除类操作是可以执行的。比如用DELUNLINK删掉一些大Key,内存被释放,OOM状态会立刻解除。所以遇到全站写失败,不要傻等,先用删除操作腾出内存空间,再考虑后续策略调整。

2. maxmemory-policy:为什么你遇到的偏偏是这条错误

2.1 默认的noeviction,就是OOM的直接来源

maxmemory-policy的默认值就是noeviction。也就是说,从你第一次启动Redis到显式修改淘汰策略之前,只要内存达到maxmemory,Redis就会拒绝所有新增数据的写命令。

这种默认设计其实很“安全”,它不会偷偷删你的数据。但对于绝大多数缓存场景,这就是灾难:你存进去的旧数据明明可以淘汰,Redis却选择把新写入全部挡住,业务一时半会还找不出原因。

所以我的建议是:只要Redis被当作缓存用,就一定要显式设置maxmemory-policy,不要用默认值。具体选哪个,看下面几种策略的对比。

2.2 8种淘汰策略对比,选型不再纠结

从Redis 4.0开始,官方支持的淘汰策略一共有8种:

策略淘汰范围算法适用场景
noeviction不淘汰严格不能丢数据的场景,内存满直接报错
allkeys-lru所有key近似LRU缓存数据冷热分明,能接受淘汰任意key
volatile-lru设置了TTL的key近似LRU只希望淘汰可过期的缓存,持久化key不被动
allkeys-random所有key随机访问非常均匀,无热点,淘汰谁都无所谓
volatile-random设置了TTL的key随机全部key都有过期时间,随机淘汰可接受
volatile-ttl设置了TTL的key剩余存活时间最短优先优先淘汰快要过期的key
allkeys-lfu所有keyLFU热点差异明显,需要识别真正高频访问的数据
volatile-lfu设置了TTL的keyLFU只想在可过期key里做LFU淘汰

这里我最想强调的是volatile-*系列的一个大坑:它只会淘汰那些设置了过期时间的key。假如你的业务代码里大量key都没有设置TTL,那volatile-lru实际上就退化成noeviction,内存满了照样报OOM。

我见过太多线上案例,配置文件里明确写着maxmemory-policy volatile-lru,结果内存还在持续上涨,最后排查发现:业务代码里90%的缓存key都没设过期时间。策略没生效,相当于形同虚设。所以选volatile-*策略之前,先想清楚你的key是否都有TTL。

2.3 从LRU到LFU:淘汰算法背后的细节决策

Redis的LRU并不是严格意义上的LRU,而是“近似LRU”。它不会给每个key维护精确的访问时间,而是用采样方式去猜测哪些key最久没被访问。核心参数是maxmemory-samples,默认值是5。

如果把maxmemory-samples调大到10,淘汰时挑选出的key就更接近真实LRU结果,淘汰精度更高,但CPU消耗会略微增加。反过来调小,比如3,能省一点CPU,但淘汰结果会更“随机”,可能误伤热数据。生产环境我一般保持10,这个性价比最平衡。

Redis 4.0引入了LFU,全称Least Frequently Used,关注的是访问频率,不是访问时间。LFU适合那种数据访问频率差异特别明显的业务,比如某个爆款资源被大量读取,其他资源无人问津。LRU在这种情况下可能因为“偶尔扫一次”而保留冷数据,LFU可以更精准地保留高频热数据。

LFU有两个参数,lfu-log-factor控制频率增长的速率,lfu-decay-time控制访问频率的衰减时间。lfu-decay-time默认是1,意思是每分钟衰减一次。如果业务是周期性爆发,比如每天某个时段才有大流量,要适当调大这个值,否则热点数据可能在低峰期被淘汰。

在选择到底是LRU还是LFU时,我的经验是:大部分业务用allkeys-lru就够用了;除非你有比较明确的热点访问模型,否则不要为了“更高级”去盲目上LFU。

还有一个跟淘汰直接相关的参数,lazyfree-lazy-eviction yes。默认情况下,Redis淘汰key是在主线程里同步执行的。如果你淘汰的是一个几百MB的大Key,主线程会被卡住,业务上表现就是Redis的耗时突然从1ms变成几秒。开启这个参数后,淘汰操作会放到后台异步线程执行,主线程不会被阻塞。Redis 4.0之后支持UNLINK命令,也是异步删除,操作大Key时优先用它。

3. 快速定位内存杀手:一条命令把老底翻出来

3.1 INFO memory:每个字段都是破案线索

遇到OOM告警,第一步永远是看内存的整体构成。执行:

redis-cli -p 6379 INFO memory

重点看这几个字段:

字段含义排查价值
used_memoryRedis分配器实际分配的内存maxmemory直接对比,判断是否触顶
used_memory_rss进程在操作系统里占用的物理内存used_memory大很多说明碎片多
used_memory_peak历史最高内存占用判断当前是不是“历史新高”
used_memory_overhead数据以外额外占用的内存包含缓冲区、字典、复制积压等
mem_fragmentation_ratio碎片率 = RSS / used_memory大于1.5说明碎片严重,可能需要清理碎片
maxmemory配置的内存上限确认配置是否生效
maxmemory_policy当前淘汰策略确认策略是否符合预期

我一般先看used_memory是不是已经顶到maxmemory,再看mem_fragmentation_ratio是否异常。碎片率长期高于1.5,可以考虑开启activedefrag yes,让Redis在后台整理内存碎片。不过这个功能本身也消耗CPU,不是所有版本默认开启,需要观察一段时间。

3.2 MEMORY DOCTOR和MEMORY USAGE:单点体检工具

INFO memory看完整体,下一步是定位具体对象。

MEMORY DOCTOR可以理解成Redis自带的“体检报告”,它会基于当前内存情况给出一段分析建议。虽然建议比较泛,但作为最开始的问题诊断入口很方便:

redis-cli -p 6379 MEMORY DOCTOR

想知道某个具体key占了多少内存,用MEMORY USAGE

redis-cli -p 6379 MEMORY USAGE user:profile:10001

它会返回这个key占用的大致字节数。排查OOM时,可以用它去验证怀疑对象:比如觉得某个缓存value很大,一查可能发现一个key就占了5MB,那问题就清晰了。

MEMORY STATS还能看更细的分配情况,包括overhead.hashtable.mainoverhead.hashtable.expires这类明细,有兴趣可以深入看,日常排查主用前三个命令就够了。

3.3 大Key扫描:别用KEYS,用官方工具

线上定位大Key,强烈不建议用KEYS *,它会在主线程遍历全量key,直接卡住Redis。正确做法是用SCAN,更简单的是直接用Redis自带的--bigkeys参数:

redis-cli -p 6379 --bigkeys

--bigkeys内部用的是SCAN游标遍历,不会一次性阻塞那么久,但它会统计每种数据类型里最大的key,最后汇总成一个报告。执行时CPU会有一些波动,建议在业务低峰期跑。

命令执行后会看到类似这样的输出:

-------- largest 5 strings found -------- 1) 100 KB user:profile:102938 2) 90 KB user:profile:102934 ... -------- largest 5 lists found -------- 1) 1200000 items msg:queue:order

看到几百万条元素的List或者超大String,基本就能锁定内存增长源了。除了--bigkeys,如果你用的Redis版本支持,也可以试试--memkeys,不过主流版本里--bigkeys已经足够。

4. 现场止血与长期治理:maxmemory调教与业务侧改造

4.1 临时止血:三步恢复写入

线上已经出现OOM报错了,最要紧的不是马上改造业务代码,而是先恢复可用性。

第一步,快速判断当前数据的可丢性。如果这个Redis实例存的是纯缓存,可以从别处重建,那直接把淘汰策略改成allkeys-lru

redis-cli -p 6379 CONFIG SET maxmemory-policy allkeys-lru

CONFIG SET是即时生效的,不需要重启,内存不够时Redis会立刻按LRU淘汰一部分key,写命令马上恢复正常。注意,这只是一个应急操作。如果业务数据不能丢,千万不要随意开启淘汰策略,否则你会“成功”地丢失一批重要数据。

第二步,如果不想开启淘汰,那就手动清空间。先用--bigkeys找出大Key,再用UNLINK删除:

redis-cli -p 6379 UNLINK msg:queue:order

UNLINK是异步删除,不会阻塞主线程。老版本Redis没有UNLINK,只能用DEL,但删大Key会卡,所以生产环境尽量把Redis升级到4.0以上。

第三步,所有CONFIG SET的修改都要记得执行CONFIG REWRITE,把配置持久化到配置文件里。不然下一次重启,配置又回到老样子,问题复现。

4.2 容量规划:maxmemory设多大才合理

止血之后要思考容量规划。如果maxmemory设置得太小,数据稍微一涨又OOM;设置得太大,Redis扛住了,操作系统可能先挂。这里给一套比较实用的估算方法:

假设业务预期缓存数据量为D,Redis自身管理内存会有哈希表开销、过期字典、客户端输出缓冲、复制积压缓冲等额外占用,大概占数据量的10%~30%。如果开启了AOF重写或RDB快照,fork子进程还会因为copy-on-write机制额外占用内存,这部分可能达到数据量的20%~30%甚至更高。

那么一个相对安全的公式是:

maxmemory ≈ 预期数据量D / (1 - 30%)

也就是预留30%的内存给额外开销。举例:你预估缓存最多放5GB数据,那maxmemory不要设置成5GB,建议设置成7GB左右;物理机内存至少要12GB以上,才能给系统留出充足余量。如果物理机只有8GB,那maxmemory最多设置5GB~6GB,同时要严格控制业务数据量。

used_memory_peak也是一个很好的参考指标。如果历史峰值是6GB,而当前maxmemory只有4GB,说明容量明显不够,要么扩容,要么优化数据。

4.3 业务侧改造:TTL和序列化是两座大山

内存问题只靠调Redis参数,永远治标不治本。真正决定内存增长上限的,是业务代码怎么设置key的生命周期、怎么序列化数据。

先说TTL。很多缓存的key根本没有设置过期时间,相当于永久驻留。这在一开始没什么感觉,但日积月累,数据量会持续增长。业务上应该明确:缓存类key必须设置TTL,具体时长看数据容忍度,比如用户会话可以15分钟,商品详情可以30分钟,活动配置可以1小时。

代码里常见的问题是:

// 错误示范:set之后忘了expire redisTemplate.opsForValue().set("user:" + userId, userJson);
// 正确示范:带过期时间 redisTemplate.opsForValue().set("user:" + userId, userJson, 30, TimeUnit.MINUTES);

再说序列化。JDK原生的Java序列化会把类全限定名、对象头、字段元信息全部写进去,膨胀率特别高。一个原本2KB的Java对象,JDK序列化后可能变成5KB甚至更大。同一份数据改用JSON序列化,能降到1.5KB;再用压缩,能压到500字节。

我之前测过一个用户画像缓存,三种序列化方案的内存占用差异非常明显:

序列化方式单个value大小10万个key占用
JDK原生序列化4.8KB480MB
Jackson JSON2.1KB210MB
JSON + Snappy压缩780B78MB

同样的数据量,光是换序列化方案,内存就能降低一半以上。如果是新系统,更推荐Protobuf这类二进制序列化方案,体积小、解析快,但改造成本也会高一些。

还有一个容易踩的坑是String类型存大对象。Redis的String是二进制安全,但它底层是SDS,存储大JSON时除了数据结构本身没有太多额外功能。如果你要存一个业务对象并且会频繁改字段,建议用Hash分散存储,至少不会因为一次GET大value把带宽和线程拖死。

5. 容器化部署:Docker里Redis内存限制要特别小心

5.1 Docker容器的内存限额和maxmemory是两回事

现在很多团队用Docker部署Redis,docker-compose里经常看到这样的配置:

services: redis: image: redis:7 container_name: redis-cache command: redis-server /usr/local/etc/redis/redis.conf mem_limit: 2g ports: - "6379:6379" volumes: - ./redis.conf:/usr/local/etc/redis/redis.conf restart: unless-stopped

这里有个关键点:mem_limit: 2g是容器能使用的物理内存上限,maxmemory是Redis自身能使用多少内存来存数据。两者必须留出差值,绝对不能相等。

我见过有人把mem_limit设成4GB,maxmemory也设成4GB,运行一段时间后Redis触发AOF重写,子进程fork时复制页表,内存瞬间超过容器限制,整个容器被OOM Killer杀掉。Docker不会因为你配了maxmemory就保护你不被cgroup杀掉,它只认实际物理内存消耗。

所以容器环境下,maxmemory一定要小于容器的mem_limit。如果容器限制2GB,我建议maxmemory设置在1.5GB左右,给fork子进程和缓冲区留出500MB。对应的redis.conf可以这样写:

maxmemory 1.5gb maxmemory-policy allkeys-lru appendonly yes lazyfree-lazy-eviction yes

另外,容器里通常看不到物理机的完整内存,free -h显示的可能是宿主机内存。所以排查时不要只看容器内的数值,要结合docker stats观察真实占用。

5.2 主从复制场景下从节点的内存陷阱

热词里出现了“docker安装redis主从”“redis主从”,说明很多人都在容器里搭主从。主从复制有个默认参数,replica-ignore-maxmemory yes,意思是当从节点内存超过maxmemory时,它不会主动淘汰数据,因为数据应该由主节点通过DEL命令同步过来。

这本来是合理的,但会带来一个隐患:从节点不会主动淘汰,只被动接受主节点的写命令。如果主从节点的物理内存规格不一致,比如主节点是8GB内存、maxmemory设6GB,从节点只有4GB内存,那主节点写到6GB时从节点可能已经物理内存耗尽了,容器被OOM Killer干掉。

所以在主从架构下,主从节点的内存规格、maxmemory配置必须保持一致。尤其是用Docker部署时,每个容器的mem_limit都要对齐。

还有一个主从场景容易被忽略:主节点因为网络分区短暂断开复制积压缓冲区(repl-backlog-size,默认1MB)会累积数据。如果大量写命令堆积,积压缓冲区也可能占内存。连接断开时间越长,积压越多,内存上涨越明显。遇到这种问题,可以适当调小repl-backlog-size,并且尽快恢复主从网络。

5.3 监控告警比调参更重要

无论是容器还是物理机,Redis内存问题最有效的防治手段是监控告警。等到业务报障才发现OOM,说明监控链路是缺失的。

比较常规的一套方案是redis_exporter+Prometheus+Grafanaredis_exporter会采集INFO memory里的指标,Prometheus负责存储和计算告警规则,Grafana做展示。我自己的经验是,至少配置三个告警阈值:

  • used_memory / maxmemory大于80%,警告,通知值班同学关注趋势;
  • used_memory / maxmemory大于90%,紧急,说明随时可能OOM;
  • evicted_keys增长速率异常,说明淘汰策略开始生效,可能有人在大量淘汰数据,需要排查原因。

日志方面,容器部署的直接docker logs redis-cache,物理机部署一般看/var/log/redis/redis-server.log。Redis的OOM错误本身不会直接写进日志,它是以客户端报错形式返回的,所以监控客户端异常率也很重要。

6. 典型故障复盘:三次不同原因的OOM排查记录

6.1 案例一:缓存全没TTL,volatile策略形同虚设

有一次线上告警,Redis实例写入大面积失败,报错正是OOM command not allowed。配置文件里明明写着maxmemory-policy volatile-lru,按道理内存满应该触发淘汰,怎么还会OOM?

排查过程走了不少弯路。先看INFO memoryused_memory已经和maxmemory齐平,但expires字段统计的“带过期时间的key数量”只有几百个,而dbsize显示的key总数是十几万。

问题一下就清楚了:业务代码里绝大部分缓存key都没设置TTL,volatile-lru只能淘汰那几百个带TTL的key,淘汰完之后没有新的key可淘汰,Redis就按noeviction的逻辑拒绝写入。

这个过程验证了前面提到的坑:volatile-*策略的生效前提是key有TTL。最后的修复方案是:先临时把策略改成allkeys-lru恢复写入,然后用脚本把所有没有TTL且允许丢弃的缓存key批量设置过期时间。后续把maxmemory-policy改回volatile-lru,并推动业务侧统一封装缓存工具,强制要求传入过期时间。

6.2 案例二:分布式锁key堆积,锁不释放

另一个案例更隐蔽。某订单系统的Redis内存持续上涨,但--bigkeys扫描出来的大Key并不明显,总体积也不大,说明是海量小key堆积。

看了INFO keyspace,发现有个固定前缀的key数量异常多:lock:order:*。再结合代码排查,发现老项目用的是SETNX加锁,拿到锁之后再EXPIRE设过期时间:

Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock:order:" + orderId, "1"); if (locked) { redisTemplate.expire("lock:order:" + orderId, 30, TimeUnit.SECONDS); // 业务逻辑 }

这个写法看起来没问题,但如果业务逻辑执行过程中抛了异常,或者进程在SETNX成功之后、EXPIRE执行之前突然宕机,锁key就永远没有过期时间,一直残留在Redis里。订单量一大,这类残留key累积起来,内存自然被吃光。

这种问题最好的解法是放弃这种组合命令,直接用一条带过期时间的原子命令:

Boolean locked = redisTemplate.opsForValue() .setIfAbsent("lock:order:" + orderId, "1", 30, TimeUnit.SECONDS);

或者直接用Redisson,它的看门狗机制会自动续期,也能避免key永久残留。清理已经产生的垃圾锁key,则可以用SCAN匹配lock:order:*前缀,逐个删除。

6.3 案例三:List bigkey无脑增长,消费者跟不上

第三个案例是典型的List大Key增长。业务把Redis List当消息队列用,生产者持续LPUSH,消费者用RPOP消费。原本一切正常,但消费者服务发版后一直报错重启,消费速度骤降,List里的消息只进不出。

等到发现的时候,List已经有几百万条元素,key占用了几百MB内存。因为在noeviction策略下,内存撞到上限,连LPUSH都被拒绝了,生产者开始大量报错。

这种问题的处理思路是:先停掉部分生产者流量,然后用UNLINK把整个List key删掉,或者用LTRIM只保留最近一段数据:

# 只保留最近10万条消息 LTRIM msg:queue:order 0 99999

业务侧还要增加消费堆积告警,监控LLEN的长度,超过阈值就要人工介入。更长远来看,Redis List做消息队列不如专业的Stream类型,它支持消费者组和消息确认,对消费堆积情况也更好控制。

6.4 OOM现场排查速查表

最后整理一个可以直接抄的排查顺序,碰到OOM command not allowed的时候按这个来:

  1. 执行INFO memory,确认used_memorymaxmemory是否已经齐平;
  2. 执行CONFIG GET maxmemory-policy,看当前淘汰策略;
  3. 如果策略是noeviction,考虑是否允许临时切换成allkeys-lru恢复写入;
  4. 执行redis-cli --bigkeys,找大Key和异常增长的key集合;
  5. MEMORY USAGE验证怀疑目标;
  6. UNLINK删除大Key或垃圾key,释放内存;
  7. 检查业务代码里的TTL、序列化、锁key、队列消费逻辑;
  8. CONFIG REWRITE持久化修改;
  9. 回顾监控告警阈值,确保下次在OOM之前就有通知。

这套流程跑下来,大部分Redis内存问题都能定位到具体原因。

我个人在实际操作中的体会是:allkeys-lru确实能快速恢复写入,但它本质上是拿业务数据换可用性。如果你没有告警、没有TTL治理、没有容量评估,OOM只是被延迟了,不是被解决了。Redis内存管理真正要打磨的地方,是把“key生命周期”这件事管起来,让数据在写进去的那一刻就已经想好什么时候淘汰、什么时候清理。最后再分享一个小技巧:日常就把used_memory / maxmemory的告警阈值配到80%,收到告警之后慢慢排查,远好过被用户报障逼着救火。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/12 4:14:16

Starlight与Microsoft Clarity集成指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 4:13:43

微电网多时间尺度调度优化与MATLAB实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 4:13:24

前端UMD模块方案:跨环境兼容的终极指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 4:10:28

新能源汽车4S店保养系统开发实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 4:08:02

LLM应用可观测性实战:Langfuse+Langchain+DeepSeek监控系统

如果你也在用 DeepSeek 这类大模型做线上服务,一定遇到过类似的场景:用户跑来问“为什么今天的回答质量变差了”,你打开日志一看,HTTP 200、响应正常、耗时也正常——可问题就是复现不出来。我上个月做 AI 客服时就卡在这个坑里&a…

作者头像 李华