Redis 到底快在哪从一次SET看清数据结构与单线程模型一、一次KEYS让整个实例停住去年排查商品详情接口超时。日志里到处是Read timed out数据库慢查询日志却干净得可疑。最后在 Redis 监控面板上看到一条命令的耗时曲线垂直拉升时间点和告警对得上——有人在大促前敲了KEYS cart:*他的本意是看看购物车数据长什么样。而KEYS是全量键空间遍历执行期间其他客户端的命令全部排队。实例上几十万个 key所有依赖它的服务一起卡住。多数人对 Redis 的理解停在一句话上“它很快因为它在内存里因为它是单线程的。”这句话不能指导任何决策。要回答的是快在哪个环节单线程约束了什么为什么同一个SET有时瞬间返回有时能把整条链路拖垮二、一条命令的旅程2.1 从字节到函数调用SET cart:1001 []在内部经过五步TCP 收包编码成 RESP 数组*3\r\n$3\r\nSET\r\n...纯文本协议解析成本极低。协议解析epoll 发现 socket 可读把字节流拆成参数数组。命令表查找用SET查处理函数校验参数个数、ACL、内存上限Cluster 下判断 slot 是否归自己管不归就回MOVED。单线程执行在内存哈希表上定位或创建 key写入值做过期检查。回复缓冲OK\r\n进输出缓冲区下一轮事件循环写回 socket。[图一条命令从客户端发出到回复返回的五步路径标出 I/O 线程与主线程的分界]2.2 单线程只修饰第 4 步Redis 的单线程特指命令执行串行不是整个进程只有一个线程。环节谁在干是否卡住其他命令执行命令本身主线程—它就是那条串行队列socket 读写、协议解析主线程6.0 起可配io-threads交给 I/O 线程否RDB 快照 / AOF 重写fork出的子进程fork瞬间停顿子进程干活不阻塞AOFfsynceverysec下由后台线程做always下阻塞always会大 key 异步释放UNLINK、FLUSHDB ASYNC后台线程BIO否过期键删除主线程惰性 定期会占用主线程时间集群 gossip 消息处理主线程走单独 cluster bus 端口会占用主线程时间注意最后一行集群通信是后台线程是错的cluster bus 的消息仍在主线程事件循环里处理节点多、心跳频繁时会挤占主线程。2.3 两个结果好处命令天然原子。无锁竞争、无线程切换所有操作在同一份数据结构上顺序发生——这是INCR、HINCRBY、SETNX可以当原子操作用的根本原因。代价一条慢命令会阻塞后面所有请求这是生产上绝大多数抖动的根源复杂度 O(N)KEYS、FLUSHALL、HGETALL几十万字段的 Hash、SMEMBERS一个大 Set、SORT。单个 key 太大对几百万元素的 List 执行DEL回收是 O(N) 的。大 key 用UNLINK小 key 直接DEL——UNLINK有入队开销也可配lazyfree-lazy-user-del yes默认no让DEL整体异步。三、快的三层原因和它的边界数据在内存里不涉及磁盘寻道持久化是另一条路径第 03 篇不影响命令执行。执行模型省掉了协调成本不需要加锁也不需要在多核之间同步状态。数据结构与协议为小操作优化过listpack把一组小对象压进连续内存避免指针追逐。但**快有前提**命令足够小、复杂度足够低。破坏前提后前面三条一条都救不了你——判断标准只有一条复杂度是否和 key 里的元素数量成正比。四、五种结构对应的是五种访问模式选错结构往往不是性能差一点而是根本没解决业务问题结构主场关键命令典型反例String计数、位图、会话、分布式锁INCR、SETBIT/BITCOUNT、SET NX PX整对象序列化进去改一个字段读写全量Hash对象的字段级更新HSET、HINCRBY慎用HGETALL字段数上万还整取变成慢命令List时间线、分页列表、简单队列LPUSHLRANGE、BRPOP当可靠队列用Set去重、交并差、判存在SADD、SISMEMBER、SINTER做判重却没算内存ZSet排行榜、延时队列、滑动窗口限流ZINCRBY、ZRANGE ... REV、ZRANGE ... BYSCORE榜单不分桶一个 key 无限增长三条值得展开位图签到SETBIT sign:1001:202610 28 1记录当天签到一年只占几十字节BITCOUNT sign:1001:202610统计当月签到总天数。连续天数BITCOUNT算不了——它返回 1 的总数而非连续段得靠客户端遍历、BITFIELD分段取或 Lua 逐位判断。ZSet 延时队列score 存应被消费的时间戳ZRANGE key 0 now BYSCORE LIMIT 0 10捞任务。它不是可靠队列——捞出来到处理完之间进程崩了任务就丢需要一张处理中集合兜底。ZSet 滑动窗口限流ZREMRANGEBYSCORE清窗口外 →ZCARD判断 →ZADD三步不原子用 Lua 包。List 当队列必须说重一点LPUSHBRPOP没有 ACK、没有消费组、没有重投消费者拿到消息后进程被杀消息就永久消失。不能接受丢消息用 Stream有消费者组和XACK或专门的消息中间件。4.1 编码地图同一种结构三种内存布局写入123和写入 100 字节 JSON内存里的对象类型并不相同。Redis 为每种结构选最省内存的编码并在超过阈值时升级。[图String / Hash / Set / ZSet 的编码随数据规模切换的阈值示意图]结构紧凑编码通用编码触发阈值官方默认值非实测Stringint能放进long long的整数、embstr≤44 字节raw超long long范围或超 44 字节Hashlistpackhashtablehash-max-listpack-entries128、-value64Listquicklist节点内是listpack无始终 quicklistlist-max-listpack-size控制单节点大小Setintset全整数、7.2 起新增listpackhashtableset-max-intset-entries512、set-max-listpack-entries128、set-max-listpack-value64ZSetlistpackskiplistzset-max-listpack-entries128、-value64三点必须知道7.0 把ziplist换成了listpack配置项随之改名hash-max-ziplist-entries→hash-max-listpack-entrieszset-max-ziplist-entries→zset-max-listpack-entries。旧名在 7.0 仍作别名可用新代码统一用listpack。OBJECT ENCODING的返回值也变了7.x 是listpack6.x 是ziplist。Set 的listpack编码是 7.2 才有的。7.0 及更早装字符串的小 Set 直接就是hashtable。编码升级不可逆。Hash 从listpack变成hashtable后即使字段删到只剩一个也不退回除非删 key 重建。这就是内存占用不是线性的最直接的原因数据量减半内存没降。五、把编码和坑都跑一遍环境Redis 7.xdocker run -d -p 6379:6379 redis:7.2、JDK 17、spring-boot-starter-data-redis默认 Lettuce、jackson-databind。5.1 阈值前后的编码切换redis-cli# --- Stringint / embstr / raw ---SET n12345OBJECT ENCODING n# int超出 long long 范围的整数不会是 int而是 embstr 或 rawSET shelloOBJECT ENCODING s# embstr≤44 字节SET s2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaSTRLEN s2# 45先自证长度再断言编码OBJECT ENCODING s2# raw# --- Hashlistpack → hashtable ---CONFIG SET hash-max-listpack-entries3DEL h HSET h f1 v1 f2 v2 f3 v3 OBJECT ENCODING h# listpackHSET h f4 v4 OBJECT ENCODING h# hashtable超过 3 个字段HDEL h f4 f3 f2 OBJECT ENCODING h# 仍是 hashtable —— 编码不回退# --- Setintset 跨阈值 ---CONFIG SET set-max-intset-entries2DEL st SADD st12OBJECT ENCODING st# intsetSADD st3OBJECT ENCODING st# 7.0 及更早hashtable# 7.2满足 listpack 条件则为 listpack否则 hashtableZSet 同理CONFIG SET zset-max-listpack-entries 2后加三个短成员编码从listpack变skiplist成员值超过zset-max-listpack-value默认 64也会触发同样的切换。CONFIG SET改的是运行时配置、重启失效实验用的极小阈值生产上记得还原。5.2 同一个购物车两种实现String JSON 版本——真实项目里见过最多的写法// 位于 Service 中redis / mapper 为注入字段log 由类上的 Slf4jLombok生成publicvoidaddItem(longuserId,longskuId,intdelta){Stringkcart:userId;Stringrawredis.opsForValue().get(k);// ① 读全量Cartcart;try{cart(rawnull)?newCart():mapper.readValue(raw,Cart.class);}catch(JsonProcessingExceptione){log.warn(cart json broken, drop and rebuild. key{},k,e);redis.delete(k);// 脏数据删掉下次重建thrownewIllegalStateException(购物车数据异常请重试,e);}cart.setQuantity(skuId,cart.getQuantity(skuId)delta);try{redis.opsForValue().set(k,mapper.writeValueAsString(cart));// ② 写全量}catch(JsonProcessingExceptione){log.error(cart serialize failed. key{},k,e);thrownewIllegalStateException(购物车保存失败,e);}}上面这段没设 TTL。真实业务应该写成redis.opsForValue().set(k, json, Duration.ofDays(30))否则购物车 key 永不过期只能靠内存淘汰或手工清理兜底。两个问题第二个更严重读写都是全量30 个商品改一个要把 30 个都读出来再写回去更要命的是丢失更新——两个请求同时执行 ①②后写的用旧快照覆盖掉前一个的修改。这不是理论风险是必然发生。Hash 版本publicvoidaddItem(longuserId,longskuId,intdelta){Stringkcart:userId;Longafterredis.opsForHash().increment(k,String.valueOf(skuId),delta);// HINCRBY 服务端原子if(after!nullafter0){redis.opsForHash().delete(k,String.valueOf(skuId));// 与上一步非原子}}HINCRBY服务端原子并发改同一商品不丢更新。但归零则删字段是第二步两步之间有竞态可能刚删掉字段另一个请求又把它写成 1。要干净得用 Lua-- cart_incr.lualocalvredis.call(HINCRBY,KEYS[1],ARGV[1],ARGV[2])ifv0thenredis.call(HDEL,KEYS[1],ARGV[1])endreturnvprivatestaticfinalRedisScriptLongCART_INCRbuildScript();privatestaticRedisScriptLongbuildScript(){try{StringluanewClassPathResource(lua/cart_incr.lua).getContentAsString(StandardCharsets.UTF_8);returnnewDefaultRedisScript(lua,Long.class);}catch(IOExceptione){thrownewIllegalStateException(cart_incr.lua 加载失败,e);}}publicLongaddItemAtomic(longuserId,longskuId,intdelta){returnredis.execute(CART_INCR,List.of(cart:userId),String.valueOf(skuId),String.valueOf(delta));}getContentAsString()抛IOException写在字段初始化位置编译不过必须用静态工厂包一层DefaultRedisScript的构造签名在不同小版本间也有差异以当前版本文档为准。String JSONHash改一个字段的读写量O(购物车大小)O(1)并发改不同商品互相覆盖互不影响并发改同一商品丢失更新HINCRBY原子取整个购物车一次GETHGETALL字段多时是慢命令结论不是Hash 一定更好。业务永远整个对象一起读、一起写时String 存 JSON 更简单不必关心 Hash 的字段数阈值和逐字段操作一旦出现只改其中一个字段Hash 的优势就是决定性的。5.3 ZSet 热度榜记得分桶ZINCRBY hot:article:202610292111001ZINCRBY hot:article:202610292151002ZRANGE hot:article:202610292109REV WITHSCORES# Top 106.2 起推荐 REV 形式ZREVRANK hot:article:20261029211001# 查单个成员排名0 开始未废弃查排名用它即可EXPIRE hot:article:2026102921172800# 2 天后清理publicvoidhit(longarticleId){Stringbuckethot:article:LocalDateTime.now().format(DateTimeFormatter.ofPattern(yyyyMMddHH));redis.opsForZSet().incrementScore(bucket,String.valueOf(articleId),1);redis.expire(bucket,Duration.ofDays(2));// 与上一步非原子见下}ZREVRANGE、ZRANGEBYSCORE、ZREVRANGEBYSCORE自6.2 起 deprecated7.x 仍可用新代码请写ZRANGE key start stop REV、ZRANGE key min max BYSCORELIMIT只对BYSCORE/BYLEX生效。ZREVRANK没有废弃照常用。incrementScore和expire是两次调用中间宕机就留下一个永不过期的 key用 pipeline 或 Lua 合并第 02 篇细讲。分桶也不只是为了好清理ZINCRBY在skiplist下是 O(log N)但小 zset 走listpack时是O(N)——榜单越界那一刻单次写入代价会跳变。聚合时用ZUNIONSTORE按权重合并或只读最近 24 个桶。5.4 亲手造一个大 keyseq1100000|awk{print RPUSH biglist item-$1}|redis-cli--pipeMEMORY USAGE biglist# 占多少字节采样估算可传 SAMPLESOBJECT ENCODING biglist# quicklist# DEL biglist # 危险主线程同步释放UNLINK biglist# 后台线程回收主线程立即返回要警惕的不是这个 key 有多大而是有没有一条命令会把它整个读出来或整个删掉。六、Key 设计和那 16 个 dbKey 命名固定成业务:对象:标识如cart:1001。排查时能用SCAN 0 MATCH cart:* COUNT 100圈定范围COUNT只是提示单次返回可能多于也可能少于 100做容量分析时按前缀归类。别把时间戳、随机数放在前缀位置。key 多本身不是问题但上 Cluster 后 key 按CRC16(key) mod 16384落到 slot前缀过度集中会让数据倾斜到少数 slot——第 04 篇展开。关于SELECT切库默认 16 个 dbdatabases 16很多人拿它做多业务隔离。我不建议它不是权限隔离任何能连上的客户端都能切到任意 db危险命令范围失控在错误的 db 上FLUSHDB清的是别人的数据Cluster 模式下只有 db 0。结论用 key 前缀做逻辑隔离不要用 db。七、Lettuce 还是 JedisSpring Boot 2.x 起默认 Lettuce。差别不在谁更快而在连接模型LettuceJedis连接模型单连接多路复用、线程安全也可配池一个连接同时只处理一条命令必须靠池阻塞命令BRPOP需单独建连接否则占住共享连接占住池中一条连接池小容易打满响应式 / 集群 / 哨兵原生 Reactor集群哨兵均支持无响应式集群哨兵均支持绝大多数业务用默认的 Lettuce 就行不该切。共享连接执行阻塞命令会把整条连接卡住BRPOP场景要么单独建连接要么改用 Jedis 的池模型。真要切靠依赖排除——Spring Boot 发现 classpath 上没有 Lettuce 就会自动装配 JedisdependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-data-redis/artifactIdexclusionsexclusiongroupIdio.lettuce/groupIdartifactIdlettuce-core/artifactId/exclusion/exclusions/dependencydependencygroupIdredis.clients/groupIdartifactIdjedis/artifactId/dependency代价是失去响应式支持、连接池要自己管。连接池越大越好是错的——连接数超过实例处理能力只会让排队变长第 06 篇展开。最后一个容易漏的前提spring-boot-starter-data-redis不会带进commons-pool2而任何连接池配置都要它——Lettuce 的spring.data.redis.lettuce.pool.*和 Jedis 的spring.data.redis.jedis.pool.*一样缺这个依赖池参数写了也不生效。切 Jedis 时最容易漏因为大家以为只是换个客户端。以上是 Spring Boot 3.x 前缀2.x 统一为spring.redis.*。八、踩坑清单“单线程所以没有并发问题”—— 我写过先GET库存判断大于 0再SET减一。压测时超卖了。单线程保证的是单条命令原子GET和SET之间隔着一次网络往返能插进任意多条别的命令。要原子就用INCR/HINCRBY或者 Lua。线上敲KEYS *—— 见过三次每次都是我就看一眼。用SCAN游标迭代或在从库上跑--bigkeys第 06 篇细讲。整对象塞 JSON改一个字段读写全量—— 见 5.2附赠一个丢失更新的 Bug。拿 List 当可靠队列—— 写过。BRPOP之后服务重启那批订单状态永远停在处理中。以为编码固定不变—— 小 Hash 在测试环境是listpack上线两个月变hashtable内存涨一截谁也说不清为什么。用SELECT做业务隔离—— 迁 Cluster 那天所有SELECT都要删掉重写。九、小结Redis 快在内存 无协调 紧凑数据结构慢在O(N) 命令和大 key。判断一条命令危不危险看它的复杂度是否和 key 的大小成正比。单线程只修饰命令执行这一步。它给你天然的原子性也意味着一条慢命令会阻塞所有人。选结构就是选访问模式。先问我怎么读、怎么改再决定用 String 还是 Hash。下一篇聊缓存。起因就是 5.3 那个榜单Top 100 直接读 RedisQPS 一上来热点 key 把节点带宽打满紧接着商品详情页开始超时。缓存不是加个Cacheable就完事它是一条需要设计的链路——穿透、击穿、雪崩每个都有解法也都有会失效的前提。自检三问你业务里有没有耗时和 key 元素数量成正比的命令它最大的那个 key 现在有多少元素所有先读、判断、再写的 Redis 操作里哪些其实需要原子性两个请求同时执行结果还对吗你用的每个 key 是listpack还是hashtable/skiplist跑一遍OBJECT ENCODING看看内存是被数据占的还是被编码升级占的。
企业数字化 ERP 产品动态
相关推荐
2026费控系统选型:5个硬指标定标尺,3个避坑点帮你少花冤枉钱 做了六年企业费用管理咨询,服务过制造、零售、医药、互联网等行业的选型项目,我越来越确认一件事:费控系统选型翻车,很少输在功能对比表上,几乎都输在"没想清楚自己该测什么"上。这篇评测从独立咨询顾问的视… · 2026/11/4 2:28:11
手选模型的时代结束了,Copilot Fusion 正式发布,token打7折 问大家一个直击灵魂的问题:打开 AI 对话框之前,你要花几秒钟选模型?
选 GPT-6 吧,怕它写代码不如 Claude 5.5 Sonnet;选 Claude 吧,遇到复杂逻辑又怕它思考太慢;想省点 token 选个轻量级模型&am… · 2026/11/4 2:28:11
AI 系统全景:模型、知识、工具、规则、评测与基础设施如何协作? 大语言模型已经能够理解问题、分析信息、生成代码,甚至通过工具执行操作。但当我们真正把 AI 接入业务系统时,会发现一个问题:
模型能力越来越强,为什么构建一个可靠的 AI 应用,仍然需要知识库、工具、业务规则、评测系… · 2026/11/4 2:28:11
台式机组装购买渠道全攻略与避坑指南 1. 台式机购买渠道深度解析:一位老司机的十年踩坑实录作为一个从2014年开始就不断折腾台式机的老玩家,我经手过的电脑少说也有十几台,从淘宝整机到线下电脑城,从京东自营到咸鱼二手,几乎把所有能踩的坑都踩了个遍。今天… · 2026/11/4 4:47:48
NPU推理慢?算子回退到CPU的排查与修复指南 1. 当NPU跑不过GPU时,先别急着换硬件做过端侧推理的兄弟大概率都遇到过这种诡异场景:同一份模型,同一批输入,在GPU上跑得好好的,换到NPU上推理耗时直接翻了几倍,甚至比纯CPU还慢。你打开任务管理器或者npu-… · 2026/11/4 4:47:48
Jev生态爆发:TypeSafe AI与RLCD如何驱动28个开源项目 1. 从“Jev 火了两周”说起:一个开源生态的爆发逻辑Jev 这个名字,最近两周在开发者圈子里出现的频率高得有点离谱。如果你还没听说过它,简单说:Jev 是一个围绕 TypeSafe AI 理念构建的模型与工具链体系,核心卖点是让 A… · 2026/11/4 4:47:48
Codex 从零上手实战:安装配置、模型接入与避坑指南 1. 从零上手 Codex:一个普通开发者的实战入门记录第一次听说 Codex 的时候,我以为又是一个"看文档半小时、配环境一整天、跑起来三分钟就报错"的工具。后来真正用起来才发现,这东西的门槛比想象中低得多,但坑也确实不少… · 2026/11/4 4:47:48
人工智能数学基础:线性代数、概率论与微积分核心概念解析 1. 项目概述"DataWhale二月组队人工智能的数学基础_Task01笔记"这个标题透露了几个关键信息:这是一个关于人工智能数学基础的学习笔记,属于DataWhale组织的二月组队学习活动中的第一个任务。作为参加过多次类似活动的老学员,我深知… · 2026/11/4 4:47:48
K8s声明式API与GitOps实战:配置漂移、字段冲突与滚动更新排错 半夜被电话叫起来处理一次线上故障,原因说出来有点丢人:两周前有人手动kubectl edit改了一个线上 Deployment 的副本数,没留任何记录,后面一次版本发布把仓库里的清单重新 apply 回去,副本数被打回原样,流量… · 2026/11/4 4:46:48
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
AI辅助技术栈评估:提升研发决策效率60% 1. 项目背景与核心价值技术决策一直是研发团队面临的重大挑战。随着技术生态的快速演进,技术栈的选择、维护和迭代变得愈发复杂。我们团队在过去两年里,先后经历了三次重大技术栈迁移,每次都需要投入大量人力进行技术评估和风险分析。直到去年… · 2026/11/4 0:00:17
从生成Token到直接决策:Jev如何重塑AI Agent工程实践 1. 从"生成文字"到"拍板决策":Jev 到底在解决什么问题大多数人第一次接触大模型,脑子里建立的模型是"输入问题、输出文字"。你问它今天天气怎么样,它给你一段描述;你让它写个周报,它给你… · 2026/11/4 0:00:17
32G统一内存跑大模型:内存账、TPS实测与端云分流 想在一台 32G 统一内存的小主机上把大模型跑成"日常可用",这件事我前后折腾了小半年。标题里说的这台 Mac mini M6,是我们这次讨论的目标平台,具体到手规格以实物为准;但下面这套推算方法和实测思路,对任何统… · 2026/11/4 0:00:17
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/4 3:16:49
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46