news 2026/9/7 14:37:07

Redis Hash底层原理与实战:从编码到渐进式rehash

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis Hash底层原理与实战:从编码到渐进式rehash

扯了多年的Redis,Hash这个数据结构我一直觉得是被很多人低估的类型。一说Redis数据类型,String、List、Set、ZSet能聊半天,轮到Hash往往是“哦,就是存个对象用的”,然后就没有然后了。真到面试或者线上排查问题时,才突然发现自己对Hash的底层原理、适用边界、内存表现其实没吃透。

这篇文章打算把Redis的核心数据结构Hash一次性聊透。我会从底层编码讲到渐进式rehash,从常用命令讲到Spring Boot里的实操案例,最后再整理一些我自己踩过的坑。目标很明确:看完之后你不仅能知道Hash是什么,还能在选型、优化、排查问题时想清楚“为什么”。

先提醒一句,这篇文章不是官方文档的翻译。我会尽量用工程项目的实际视角来写,适合刚入门想系统梳理的读者,也适合写了好几年业务代码但一直没深究过底层机制的同学。

1. Hash到底是什么结构

1.1 一张用户信息表引发的思考

假设你现在要缓存一个用户的信息,包括uid、昵称、年龄、城市、签名。最直觉的做法是用String类型,把整个对象序列化成JSON,然后以user:1001为key存进去。读的时候取出来反序列化,写的时候改哪个字段就整个对象重新序列化。这个方案在小规模、低频率更新的场景下完全没问题。

可一旦更新频繁,问题就来了。用户改个昵称,你要把一整条大的JSON字符串读出来、反序列化、改字段、再序列化、写回去。不仅网络开销变大,还容易在并发写的情况下覆盖掉其他字段的更新。这时候Hash就有它不可替代的价值。

Hash在Redis里的模型很简单:一个key下面是多个field-value对。也就是说user:1001这个key下面,可以同时有nickname -> "张三"age -> 25city -> "上海"这样的映射。它天然就是为“对象的字段级存取”设计的,你只更新一个field时,根本不用动其他field,语义清晰,性能也高。

很多人第一次接触Hash时,容易把它类比成关系数据库里的一行记录。这个类比大方向是对的,但严谨地说,Hash更像一个内嵌在Redis key里的二级哈希表——外层key定位对象,内层field定位对象的某个属性。如果你熟悉Java,可以把它直接理解为一个Map<String, Map<String, String>>,在大多数客户端SDK中,它的API接口也确实是这样暴露的。

1.2 底层编码与渐进式rehash

Hash底层在Redis 7.0之后主要有两种编码方式:listpack和hashtable。7.0之前对应的名字是ziplist,本质上是一回事,优化的方向都是把小的数据结构压缩成连续的内存块。

先说listpack编码。当Hash里的field数量比较少、每个field和value的长度也比较短时,Redis会选择用listpack来存。listpack是一块连续的内存空间,每个元素紧密排列,没有额外的指针、没有malloc碎片。它的优势显而易见:内存占用极低,而且因为内存连续性好,遍历时的缓存命中率也高。你可以想想一个最多5个字段、每个字段加起来不超过几十字节的对象,如果用真正的哈希表去存,光节点指针和内存分配器带来的额外消耗可能就比数据本身还要大。

当条件不满足时,也就是field数量超过hash-max-listpack-entries默认值128个,或者某个field的key或value长度超过hash-max-listpack-value默认值64字节,Redis就会把这个Hash从listpack转换成hashtable。hashtable才是真正意义上我们平时说的哈希表,它支持O(1)的field查找,但代价是每个节点都包含指针、哈希值等额外元信息,内存开销明显更大。

这个转换是一次性的,转换后就回不去了,除非你手动把key删掉重新写入。所以设计field数量多的Hash对象时,要提前想清楚这个对象到底会膨胀到什么量级,不要以为Redis会一直替你压缩着。

再说hashtable的rehash过程。Redis扩容时不会像Java的HashMap那样一次性把所有旧桶的数据重新哈希到新桶里,因为Redis是单线程模型,一次性扩一个大哈希表可能阻塞整个服务。于是它采用了渐进式rehash:扩容时先申请新数组,然后把旧数组里的元素分批、分多次地搬过去。每执行一次增删改查操作,都会顺带搬一部分旧节点;此外后台定时任务也在持续搬。这样一来,一个大的rehash过程被分摊到了很多次正常请求中,对延迟的影响变得很不明显。

但这里面有个值得警惕的细节:在rehash进行中,读写操作需要同时访问新旧两个hash表,查找某个field时先查新表,查不到再去旧表。这个机制平时看不太出来,但如果你在某个超大Hash上执行HGETALLHKEYS,而且恰好赶上rehash阶段,那阻塞性地遍历可能会比平时慢得多。所以线上对Hash进行全量操作时,要时刻把“渐进式rehash”这个背景记在心里。

2. Hash的常用命令与实操要点

2.1 高频命令速查表

Redis Hash的命令数量不少,但真正在业务里高频使用的其实就那么几个。我先把命令整理成一个速查表,方便你复制到自己的笔记里。

命令作用复杂度使用场景
HSET key field value设置单个字段的值,field不存在则新建O(1)写入或更新字段
HGET key field获取单个字段的值O(1)读取单个属性
HMSET key field value [field value ...]批量设置多个字段O(N),N为字段数批量写入对象
HMGET key field [field ...]批量获取多个字段O(N),N为字段数只读对象的部分属性
HDEL key field [field ...]删除一个或多个字段O(N),N为删除字段数删除对象的某个属性
HEXISTS key field判断字段是否存在O(1)逻辑判断
HLEN key获取Hash中字段的数量O(1)判断对象大小
HKEYS key获取所有字段名O(N)需要所有field名时
HVALS key获取所有字段值O(N)只关注值集合时
HGETALL key获取所有字段和值O(N)获取整个对象(慎用大key)
HINCRBY key field increment给整数字段增加指定值O(1)计数器、库存等场景
HINCRBYFLOAT key field increment给浮点字段增加值O(1)金额、评分等场景
HSCAN key cursor [MATCH pattern]游标式遍历O(N)但每次有限安全遍历,避免阻塞

这个表格里我特别想标注一下HMSET。Redis 4.0之后官方其实已经不推荐使用HMSET了,因为HSET本身支持可变参数,HSET key f1 v1 f2 v2 f3 v3HMSET key f1 v1 f2 v2 f3 v3效果完全一样。不过很多老项目还在用HMSET,这个不影响使用,只是新项目里没必要再做区分。

2.2 命令行模拟用户信息缓存全流程

纸上谈兵没有意义,直接用命令行跑一遍完整的用户缓存操作,看看Hash的数据持久性、局部更新和原子计数是怎么配合的。

第一步,写入一个新用户的基本信息:

> HSET user:1001 nickname "张三" age 25 city "上海" signature "码农一个" (integer) 4

这里返回4,表示成功新增了4个字段。如果某个field已经存在,HSET会覆盖旧值并返回0,所以返回值能帮你判断是新增还是更新,这个特性在某些幂等设计里非常有用。

第二步,读取部分字段,而不是全部:

> HMGET user:1001 nickname city 1) "张三" 2) "上海"

注意,这里我只取nickname和city,age和signature根本不出现在结果里。这就是Hash相对String存JSON的核心优势——按需读取,不把整个对象拉出来。

第三步,做一次局部更新:

> HSET user:1001 age 26 (integer) 0

返回0,因为age字段已经存在,只是更新了值。这个操作不会影响nickname、city、signature,数据库层面不需要锁,Redis单线程也不会出现字段覆盖的问题。

第四步,统计字段数量和判断字段存在性:

> HLEN user:1001 (integer) 4 > HEXISTS user:1001 age (integer) 1 > HEXISTS user:1001 email (integer) 0

第五步,删掉一个字段,比如用户清空了签名:

> HDEL user:1001 signature (integer) 1

第六步,利用HINCRBY做计数。比如用户登录一次,登录次数加1:

> HSET user:1001 login_count 0 (integer) 1 > HINCRBY user:1001 login_count 1 (integer) 1 > HINCRBY user:1001 login_count 1 (integer) 2

这个操作的原子性由Redis单线程天然保证,多个客户端并发执行也不会出现丢计数的问题。这个特性在分布式场景里很有价值,后面讲计数器时会再展开。

如果只是想快速看一眼整个对象,可以用HGETALL:

> HGETALL user:1001 1) "nickname" 2) "张三" 3) "age" 4) "26" 5) "city" 6) "上海" 7) "login_count" 8) "2"

返回值是field、value交替排列的。看起来很方便,但必须提醒:生产环境如果这个Hash的field数量很多,HGETALL会一次性把所有数据都取回来,轻则占用大量内存和带宽,重则阻塞Redis线程导致其他请求变慢。正确做法是用HSCAN分批遍历,后面我会专门讲。

2.3 什么场景该选Hash而不是String

这是我在很多团队里被反复问到的问题。数据都用String存JSON不就行了吗,为什么非要单独搞一个Hash?

要回答这个问题,得从读写模式来看。String存JSON最典型的痛点是“读多写少但每次写都要全量覆盖”。比如一个用户对象有10个字段,前端改了个头像,后端逻辑如果还是先GET再SET整个JSON,那一次更新就变成了两次网络往返,还有并发覆盖的风险。更糟糕的是,如果两个请求分别改了不同字段,但都按GET-SET方式操作,后写的那次会把另一个请求的修改直接冲掉。

Hash的做法则完全不同。每次更新只操作自己要改的那个field,其他字段天然不受影响。读的时候也不必把整个对象查出来,只要按需取两个字段就行。

还有一个容易忽略的点是过期时间。String存JSON时,整个key只有一个TTL;Hash整体也只有一个TTL,但单个field没有独立的过期时间。这意味着如果你需要“对象内部字段各自有不同的过期策略”,那Hash并不是合适的选择,Semantics上就不是为这个设计的。这时候可以拆成多个独立key,或者用String加lazy delete逻辑,得靠应用层去实现。

另外还要对比一下Hash和多个独立String。从内存角度来说,用多个String存一个对象的多个属性,每个key本身就有几十字节的开销,加上Redis内部的dict entry、过期时间等元数据,攒多了很可观。Hash把这些属性收拢到一个key里,listpack编码下内存优势非常明显。但一旦转成hashtable,每个field又成了独立的dict entry,内存优势就逐渐消失。这个内存分界线我建议你记下来:如果你一个对象有十几个字段、字段值都比较短,用Hash大概率比散落的多个String更省内存;如果字段特别多或者字段值特别长,Hash反而不一定占便宜。

3. 实战:Spring Boot中操作Redis Hash

3.1 环境准备与基础配置

理论聊了不少,接下来落到工程实践。最常见的组合是Spring Boot加Spring Data Redis,这也是Java后端最普及的技术栈。我直接用一套可复用的配置来讲,你照着就能跑。

先启动Redis。如果你本地不方便装Redis,可以直接用Docker,一条命令搞定:

docker run -d --name redis-local -p 6379:6379 redis:7.2 --appendonly yes

这里我特意加了--appendonly yes,开启AOF持久化。开发环境无所谓,但如果你在测试Hash的计数、锁等功能时不小心把容器删了,至少数据还能从AOF恢复,省得每次重启都从头造数据。

Spring Boot项目里引入依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

需要说明一下版本选择。Spring Boot 2.x对应Lettuce连接器,Spring Boot 3.x也是默认Lettuce。Lettuce基于Netty,线程安全,性能足够,不需要像Jedis那样额外搞连接池(当然如果你有特殊需求也可以配Jedis)。

然后写一个配置类,核心是解决序列化问题:

@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 使用String序列化key和Hash的field,避免二进制乱码 StringRedisSerializer stringSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(Object.class); template.setKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashKeySerializer(stringSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }

这里有个非常典型的大坑:如果你不设置HashKey和HashValue的序列化方式,Spring Boot默认会使用JDK序列化。结果就是你在Redis客户端里看到的一堆形如\xAC\xED\x00\x05t\x00\x05nickname的乱码,既没法调试,也会白白浪费大量内存。看到这种乱码基本可以断定就是序列化器没配好。

3.2 用户信息缓存的完整代码演示

配置好了之后,写一个操作Hash的服务。先说清楚,这里直接用StringRedisTemplate其实更简单,因为它key和value默认就是String序列化。但为了让你以后遇到对象存储时知道怎么处理,我今要用RedisTemplate配合泛型转换来演示。

先定义一个简单的用户对象:

public class UserProfile { private String nickname; private Integer age; private String city; private String signature; // getter/setter省略 }

然后写一个面向业务的服务:

@Service public class UserCacheService { @Autowired private RedisTemplate<String, Object> redisTemplate; private static final String USER_KEY_PREFIX = "user:"; // 写入用户信息,分多个field存储 public void saveUserProfile(Long uid, UserProfile profile) { String key = USER_KEY_PREFIX + uid; HashOperations<String, Object, Object> hashOps = redisTemplate.opsForHash(); hashOps.put(key, "nickname", profile.getNickname()); hashOps.put(key, "age", profile.getAge()); hashOps.put(key, "city", profile.getCity()); hashOps.put(key, "signature", profile.getSignature()); } // 读取单个属性 public Object getUserField(Long uid, String field) { HashOperations<String, Object, Object> hashOps = redisTemplate.opsForHash(); return hashOps.get(USER_KEY_PREFIX + uid, field); } // 批量读取多个属性 public List<Object> getUserFields(Long uid, List<String> fields) { HashOperations<String, Object, Object> hashOps = redisTemplate.opsForHash(); return hashOps.multiGet(USER_KEY_PREFIX + uid, fields); } // 局部更新某个属性 public void updateUserField(Long uid, String field, Object value) { HashOperations<String, Object, Object> hashOps = redisTemplate.opsForHash(); hashOps.put(USER_KEY_PREFIX + uid, field, value); } // 使用HINCRBY做原子计数 public long incrementLoginCount(Long uid) { HashOperations<String, Object, Object> hashOps = redisTemplate.opsForHash(); return hashOps.increment(USER_KEY_PREFIX + uid, "login_count", 1); } }

这段代码你直接拿过去就能用。几个细节提一下:

  • 我特意把key前缀user:单独定义成常量,不要散落在各个方法里。否则以后改前缀要全局搜索替换,很容易漏。
  • HashOperations.increment的返回值是Long,如果field的值原来不是纯数字,它会抛异常。
  • 更新字段时用put,内部会调用HSET命令。field不存在就新建,存在就覆盖,语义非常自然。

如果是在内存或写操作频繁的场景下,用redisTemplate.opsForHash()每次都要拼接key,略显繁琐。Spring还提供了BoundHashOperations,可以绑定一个key,后续操作不再需要重复传key:

BoundHashOperations<String, Object, Object> boundOps = redisTemplate.boundHashOps("user:1001"); boundOps.put("nickname", "李四"); boundOps.get("age");

这种写法更适合在一个线程或方法里多次操作同一个Hash对象的场景,代码会清爽不少。

3.3 Hash在分布式锁与计数器中的玩法

聊到Hash时绕不开一个叫作Redisson的框架。很多人都是通过Redisson知道Redis Hash还能实现可重入分布式锁的,其实本质就是用Hash结构来记录当前持锁线程的信息。

我记得Redisson的锁方案大概是这样:key是锁的名字,field是线程ID,value是重入次数。加锁时执行HSET,可重入时执行HINCRBY,解锁时HDEL或DECR。之所以选择Hash而不是简单的SETNX,是因为锁需要支持重入,还得记录是谁持有这把锁,释放时也要判断线程身份,这些信息必须用一个结构装起来。

如果你对分布式锁有兴趣,Redisson已经把它封装得很好了,直接引入依赖用就行,不需要自己手写锁逻辑。我想说的是,理解Hash之后,再看Redisson的实现源码就没有任何神秘感了——它不过是用Hash天然支持“线程维度计数”这个特性,把锁语义落地而已。

除此之外,Hash用于计数器的场景也很常见。比如一个对象有浏览数、点赞数、收藏数,用三个独立String key存储,散落且不好管理。放进一个Hash里,三个字段分别维护,一个对象一个key,既有聚合语义又能独立更新。用HINCRBY做递增递减,原子性有保障,不需要应用层加锁。

3.4 面试里关于Hash的几个高频考点

因为热搜词里出现了大量和“Redis面试题”相关的内容,我在这里顺便把Hash方向最常被问到的几个问题拎出来答一遍,平时面试前默背一遍也够用了。

第一个问题:Hash和Java的HashMap有什么区别?这个比较容易答。HashMap是进程内的,Redis Hash是跨进程、跨机器的,多个客户端可以并发访问。HashMap的扩容是创建新数组、全量rehash,Redis是渐进式rehash,不阻塞服务。HashMap不安全,需要加锁或用ConcurrentHashMap;Redis单线程的原子性让它天然线程安全。

第二个问题:为什么Redis小对象用listpack编码?因为小的Hash如果用hashtable,每个field都要单独分配内存,还得存指针、哈希值,内存浪费严重。listpack把数据连续存放,压缩内存占用,也提升了CPU缓存的局部性。

第三个问题:Hash的field可以设置过期时间吗?不能。Hash整体可以设置EXPIRE,但field本身没有TTL。如果面试官继续问那要实现字段过期怎么办,你就说拆成多个key,或者应用层惰性删除、定时清理。

这几个点虽然看起来基础,但很能看出一个人是不是真的看过源码。背答案没用,最好自己用redis-cli实际体验一遍listpack转hashtable,观察一下内存和性能的变化。

4. 常见问题与排查技巧实录

4.1 HGETALL大Key阻塞怎么办

我遇到过一个真实案例。当时一个业务把用户的设备信息整个存进了Hash,一个用户的device_list字段数量随着时间越积越多,到了几万个field。某一天做了一个批量任务,遍历所有用户并执行HGETALL统计信息,然后Redis的慢查询日志就爆了,最慢的一条操作卡了200多毫秒。因为Redis是单线程,那几秒钟时间里,所有的读请求都受到了影响。

这个问题的根源是用了全量命令。HGETALL、HKEYS、HVALS都是“一次取全部”的命令,复杂度O(N)。当N大到一定程度,单线程模型下就会拖垮整个Redis实例。

正确做法是用HSCAN游标式遍历。HSCAN每次只返回一部分数据,并给你一个游标,你拿着这个游标继续迭代,直到游标回到0为止。下面是一个命令行示例:

> HSCAN user:1001 0 COUNT 100 1) "123" 2) 1) "field1" 2) "v1" 3) "field2" 4) "v2"

返回值第一项是下一次迭代的游标,第二项是field-value交替排列的数据。把游标作为下一个HSCAN的参数继续调用,一直到返回的游标是0就遍历完了。

在Java里可以通过ScanOptions来实现:

ScanOptions scanOptions = ScanOptions.scanOptions().match("*").count(100).build(); Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(key, scanOptions); while (cursor.hasNext()) { Map.Entry<Object, Object> entry = cursor.next(); // 处理单个field } cursor.close();

count(100)表示每次迭代大概取100个元素左右,但这个数字不是硬性保证,只是给底层一个提示。迭代期间如果Hash被其他客户端修改了,会出现重复或漏掉的情况,这是游标遍历固有的行为,业务里要容忍这种最终一致性的读。

4.2 内存优化:field数量设计要有红线

Hash的内存表现,取决于它正处于哪种编码。listpack编码下,数据是紧凑排列的,内存非常友好。可一旦field超过128个或单个value超过64字节,它就会切换到hashtable。hashtable的好处是读写快,坏处是每个field都占用一个dict entry,每个entry都有指针、哈希值等元信息,内存消耗明显上升。

所以设计Hash时,我一般建议把field数量控制在100以内,value长度尽量不超过64字节。如果你确实要存储大对象、长文本,就别硬塞进Hash当value了,拆开存String配合业务读取会更好。

另外,field的名字也别设计得太长。一个field名字30字节,一万个field就是30万字节的额外开销,日积月累在热key场景下会非常可观。

4.3 Spring Data Redis中increment()报错的那点事

热搜词里有一条很具体的报错:java中redis使用redistemplate的increment()报错不是integer or out of range。这个我太熟悉了,基本上就是序列化问题。

increment()命令要求field对应的value必须是能用字符串表示的64位有符号整数。如果你写入时用的是Jackson或JDK序列化器,存进去的就不是纯数字字符串,而是一段带类型信息的二进制序列化数据,执行HINCRBY时Redis解析不了,自然就报“not integer or out of range”。

排查思路很直接:先用命令行HGET key field看一眼存进去的值到底长什么样。如果看到一坨\xAC\xED开头的东西,说明是JDK序列化的锅;如果是一个带引号的字符串,说明是JSON序列化器的锅。

解决方法是保证写入和递增操作使用同一个序列化器,而且这个序列化器能让数字保持为纯字符串。最省心的做法是直接用StringRedisTemplate,对数字字段用opsForHash().increment()操作。如果你必须用自定义RedisTemplate,就把HashKey和HashValue的序列化器都设置成StringRedisSerializer,或者在代码里显式把value转成String后再写入。

一句话总结:凡是涉及数值自增自减的Hash字段,写入值和递增操作必须走同一个序列化体系,别一个用对象序列化一个用String,否则早晚踩坑。

4.4 扩容与rehash对延迟的影响

hashtable在rehash时虽然采用渐进式设计,但也不是完全没有性能波动。当Hash从listpack转成hashtable的那一刻,或者hashtable扩容的那一瞬间,Redis内存里会发生一次申请新数组、建立新哈希表的动作。如果这个Hash特别大,比如几十万个field,它的转换过程也不是瞬时完成的,渐进搬移期间的一次性内存申请可能让内存峰值瞬时升高。

在这个背景下,如果你管理着一批超大Hash(比如上万个field),最好在运维层面监控一下Redis的used_memory和慢查询日志。一旦发现有rehash导致的偶发延迟,可以评估是否值得把大Hash拆分成多个小Hash,比如按业务维度或时间维度拆分,把一个原对象拆成多个子对象,或者用中间层索引把这些子对象串起来。这也是“大key治理”中非常重要的一环。

4.5 关于Hash使用边界的个人经验

写了这么多,最后分享一点我个人的实践判断,算不上标准答案,但长期用下来确实能少踩坑。

第一,Hash适合存储“结构稳定、字段数量固定且较少”的对象。比如用户资料、商品详情、订单摘要,字段基本不变,读写都以字段为单位,用Hash很顺手。

第二,Hash适合做聚合统计。点赞数、浏览数、收藏数这类多个子计数器,放一个Hash里整体管理很舒服,配合HINCRBY原子操作也放心。

第三,Hash不适合作为“大附件容器”。如果你想把整个列表、整篇文章,甚至文件分片塞进Hash,趁早换掉。Redis的Hash是内存数据结构,不是拿来存大对象的。大而长的value用String,或者干脆交给对象存储系统处理。

第四,Hash不要替代数据库做复杂查询。它只支持field维度的O(1)读取,要做范围查询、排序、多条件组合,那是另一个数据结构的职责范畴,别硬用Hash去实现。

第五,对Hash设置TTL时要特别想清楚。整体过期意味着对象所有字段一起失效,如果你需要“字段A保留30天,字段B保留7天”,Hash做不到,拆分成多个key反而是更合理的思路。

我在实际使用中还有一个习惯:所有Hash的key统一加前缀,比如user:product:order:,并且field命名也遵循一套规范。这样在Redis Desktop Manager这类可视化工具里看键列表时,心里一目了然,排查问题时也能秒定位。千万别图省事,key起得乱七八糟,等线上环境有几百个key的时候你就知道什么叫后悔了。

Redis Hash这个结构,技术上不算复杂,但把它用好的关键在于理解它的存储原理和边界。越是基础的东西,越值得沉下心拆开看。希望这篇长文能帮你把Hash从“听说过、用过”变成“用得明白、讲得清楚”。

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

API Integration Guide

API Integration Guide 【免费下载链接】caveman &#x1faa8; why use many token when few token do trick — Claude Code skill that cuts 65% of tokens by talking like caveman 项目地址: https://gitcode.com/GitHub_Trending/caveman1/caveman Authentication…

作者头像 李华
网站建设 2026/9/7 14:35:57

WorkBuddy双模型限免实测:Hy4 preview与Hy3怎么选怎么用?

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

作者头像 李华
网站建设 2026/9/7 14:34:27

Zookeeper原理与实战:分布式协调、选举、锁与集群部署

这个Zookeeper&#xff0c;很多刚接触分布式的朋友一上来就被它绕晕了——又是树形结构、又是选举、又是Watch&#xff0c;看着文档一堆术语&#xff0c;心里发怵。我自己当年也是从“这玩意到底干嘛的”一路踩坑过来的。实际上Zookeeper没那么玄乎&#xff0c;它解决的是分布式…

作者头像 李华
网站建设 2026/9/7 14:34:03

奥拉星涨潮版本御相师-渡平民攻略:资源规划与阵容节奏全解析

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

作者头像 李华
网站建设 2026/9/7 14:33:59

机器人作战平台技术拆解:无人战车如何集成导弹载荷?

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

作者头像 李华
网站建设 2026/9/7 14:30:13

腾讯云AI Agent部署实战:从Litellm代理到Skills插件体系

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

作者头像 李华