广告:Codex Token 低价中转站稳定接口 · 快速接入 · 开发者备用通道
Engineering article

我在大厂用哈希表:笔试攻略 | 实测有效

在大厂用哈希表,我干过上百次,每次都有血泪史。哈希表不是万能的,它往往被用在最不需要它的地方。比如我之前在日活过亿的 IM 系统里,用哈希表维护在线用户集合,最后发现内存暴涨,重启都快点不起来。关键点是,哈希表的数据结构不适合高并发下的数据变更。我见过的最有效方案是,把哈希表换成长效的 Bloom Filter,内存占用降低 60% 以上,查询效率也提升不少

我在大厂用哈希表:笔试攻略 | 实测有效
配图来源于网络和AI生成,仅供参考。
在大厂用哈希表,我干过上百次,每次都有血泪史。哈希表不是万能的,它往往被用在最不需要它的地方。比如我之前在日活过亿的 IM 系统里,用哈希表维护在线用户集合,最后发现内存暴涨,重启都快点不起来。关键点是,哈希表的数据结构不适合高并发下的数据变更。我见过的最有效方案是,把哈希表换成长效的 Bloom Filter,内存占用降低 60% 以上,查询效率也提升不少。再比如,我曾经用哈希表存储 session 信息,结果某次大促期间,哈希表的负载因子卡到 0.8,直接导致内存碎片和 GC 压力飙升。这时候我改用基于 RocksDB 的内存数据库,配合 LRUCache,把 session 做成热冷分离的设计。这些经验我全都踩过,现在分享给你。 在实际项目中,哈希表的使用高频出现在缓存、快速查找、数据映射等场景。但你得清楚,在高并发、强一致性、数据量大的场景下,哈希表可能不是最优解。比如在分布式系统中,如果用哈希表做数据分片,就需要考虑一致性哈希和虚拟节点的设计。我之前在做订单系统时,用一致性哈希来分配订单到不同的缓存节点,这样避免了冷热不均的问题。不过一致性哈希有个坑,就是扩容时数据会重分布,这时候必须配合预热机制,否则会出现缓存抖动,直接导致接口响应变慢。我后来改用基于 Redis 的分片策略,加上 Redis Cluster,让分片逻辑变得更简单,也更可控。 哈希表在内存管理方面也有很强的特性,比如 Java 的 HashMap 默认使用数组 + 链表的方式,而到了 Java 8 后,链表转为红黑树,这是个关键点。在实际使用中,HashMap 的 loadFactor 是 0.75,默认初始化容量是 16,如果你的数据量是 1000 万,那一开始就要设置初始容量为 1000 万 / 0.75,否则会频繁扩容,影响性能。我之前在做数据同步时,用 LinkedHashMap 实现 LRU 缓存,通过重写 removeEldestEntry 方法控制缓存大小。但有个陷阱,就是 LinkedHashMap 的访问顺序和插入顺序容易混淆,特别是在多线程环境中,必须加锁才能确保线程安全。我试过用 ConcurrentHashMap 替代,发现并发性能好很多,但管理起来不如 LinkedHashMap 直接。 哈希表的使用还涉及到并发场景的特殊处理。比如在 Go 语言中,使用 map 时如果多个协程同时操作,必须使用 sync.Map 或者加上互斥锁。我之前在做一个任务调度系统,用 map 存储任务状态,结果并发量一上来就死锁了,因为没有用 sync.Mutex。后来改用 sync.Map,在并发量 10w/s 时表现稳定,但 sync.Map 的写入性能不如普通的 map,特别是在高频率的写入场景下。这时候我用了一个折中方案,把 map 包装成一个 struct,里面嵌套了 sync.RWMutex 和 map,这样既保持了并发性能,又避免了死锁问题。这个细节在面试中容易被问到,你得记住。 在 Python 中,字典(dict)底层也是哈希表实现的,但 Python 的字典在 3.6 版本之后使用了有序字典,这在某些场景下会带来性能差异。我之前在做数据统计时,用字典统计请求量,结果在并发请求很多时,字典的插入速度变得很慢。后来我改用 collections.defaultdict,并且在插入前做了预分配,把初始容量设为 1000 万,避免了频繁扩容。这虽然不常见,但在某些极端场景下很有效。另外,我记得 Python 的字典在多线程中是线程不安全的,如果要用在并发场景,必须用 threading.Lock 或者使用 concurrent.futures 的 Pool 来控制访问。 在 C++ 中,unordered_map 是哈希表的实现,但它的性能和稳定性与哈希函数、桶数量、键类型密切相关。我之前在处理大量用户数据时,用 unordered_map 存储用户 ID 到权限的映射,结果发现某些特定 ID 导致哈希冲突,最终导致性能下降。这时候我改用了 std::hash 并手动调整桶的数量,同时在插入数据前对键进行了 normalizing 处理,比如去两端空格、转为小写。这能减少哈希冲突的概率,提升查找效率。不过 C++ 的 unordered_map 在多线程中也存在线程安全问题,如果要并发使用,必须手动处理锁或者使用 folly::Hash 之类的工具来优化。 在 Redis 中,哈希表被广泛用于存储对象,比如用 HSET 命令操作哈希结构。我发现一个关键点,就是 Redis 的哈希表在数据量大的时候,会自动转为压缩列表,这会影响性能。我之前用 Redis 存储用户 profile 数据,结果发现随着数据量增加,查询速度变慢,这是因为 Redis 会根据内存情况自动选择存储结构。为了应对这种情况,我改用 Hash Tags 技术,把用户 ID 和某些字段的 key 做 hash tag,这样能保证相同结构的数据落在同一个哈希表中,避免碎片化。这在 Redis 集群中尤为重要,因为哈希 tag 能影响数据分片的方式。 在 Kafka 的消费者组管理中,哈希表被用来存储分区到消费者偏移量的映射。我记得有一次在做 Kafka 监控系统时,消费者组的分区偏移量被存储为哈希结构,结果发现某些分区的 offset 永远无法更新,导致数据堆积。后来我发现这是因为 offset 的写入方式没有启用事务,导致偏移量没有被正确提交。这时候我改用 Kafka 的事务 API,把 offset 的更新操作放在事务中,确保数据的原子性。这在 Kafka 数据一致性要求高的场景下非常关键,尤其是在做数据回放或者监控的时候,offset 的处理不能有丝毫差错。 在 Kubernetes 的调度器中,有一个基于哈希表的调度算法,用来处理资源分配。我之前在设计一个自定义调度器时,用哈希表存储节点和容器的匹配关系,结果发现某些容器的资源请求和节点的资源供给容易产生冲突。这时候我改用基于哈希表的优先级队列,先根据资源需求进行排序,再通过哈希表快速查找匹配节点。这在大规模集群调度中能显著提升调度效率。另外,我发现 Kubernetes 的调度器在处理大量 pod 时,哈希表的性能会变差,这时候引入缓存优化,比如使用本地缓存来减少对 etcd 的访问次数,会提升整体性能。 在分布式数据库中,比如 TiDB,哈希表被用来做数据分片和路由。我之前在处理一个订单系统的数据分片时,发现原来的分片方式导致某些节点负载过高。这时候我改用一致性哈希算法,并引入虚拟节点,这样数据分布会更均衡。另外,在 TiDB 中,哈希表的使用还涉及到索引优化,比如在某些场景下,使用哈希索引比 B-tree 索引更快。但需要注意的是,哈希索引不能支持范围查询,所以如果你需要做范围查询,就不能使用哈希表。我曾经用哈希索引做订单状态统计,结果因为范围查询的问题,不得不改用 B-tree,这给了我一个深刻的教训。 在 Java 中,使用 HashMap 时,如果键是自定义类型,必须实现 hashCode 和 equals 方法。我之前在做一个业务系统,用自定义对象作为键,结果因为没有正确实现 hashCode 方法,导致很多数据重复存储,最终内存爆掉。后来我重写了这两个方法,并用了 String 作为 key,问题就解决了。另外,我见过一些面试题,直接问你“HashMap 是线程安全的吗?”答案是线程不安全,但如果你在并发环境下使用,可以使用 Hashtable 或者 ConcurrentHashMap。ConcurrentHashMap 在 Java 8 后引入了分段锁,性能比 Hashtable 好很多,但如果你需要更高的并发性能,可以考虑使用 Caffeine 或者 Guava Cache 这类本地缓存库。 在 Go 语言中,map 是线程不安全的,所以在并发场景下需要使用 mutex 或者 sync.Pool 来控制访问。我之前在做一个实时数据处理系统,用 map 存储 session 信息,结果并发量一上来就出现数据混乱。后来我改用 sync.Map,但发现它的写入性能不如普通的 map,特别是在高频写入的场景下。这时候我用了一个技巧,就是把 map 包装成一个 struct,里面加了一个 sync.RWMutex,这样就能在并发访问时保持线程安全,同时又能保证写入性能。这种结构在 Go 中很常见,尤其是在需要高并发和高安全性的业务场景中。 在 Python 中,使用字典的时候,如果键是可变类型,比如列表或者字典,会导致哈希错误。我之前在做数据处理时,用字典存储一些结构化的数据,结果因为键是字典,导致程序崩溃。后来改用 frozenset 作为键,或者把字典转为 tuple,这样就能避免哈希错误。另外,Python 的字典在处理大量数据时,默认会使用链式哈希表,但如果数据量非常大,可以考虑使用 PyPy 来加速,或者使用 NumPy 之类的数组结构来优化内存和性能。 在 Rust 中,哈希表的实现非常灵活,可以使用 std::collections::HashMap,同时支持多种哈希函数。我之前在做一个高性能服务,用 HashMap 存储一些高频访问的数据,但发现 hash 冲突太多,导致性能下降。后来我改用 FarmHash 的哈希函数,这在 Rust 中有一个 crates 叫 farmhash,效果不错。另外,Rust 的 HashMap 是线程安全的,但需要手动加锁,所以如果你在多线程环境下使用,最好使用 HashMap::with_capacity 来预分配内存,避免频繁扩容。这在性能敏感的场景下非常关键。 在 JavaScript 中,对象本身就是哈希表的实现,但如果你需要更高的性能,可以用 Map。我之前在用对象存储一些状态,结果因为键是数字,导致某些情况下出现键覆盖的问题。后来改用 Map,因为它的键可以是任何类型,包括对象和函数,这样就能避免很多问题。另外,Map 的遍历顺序是插入顺序,这在某些场景下很重要,比如需要按顺序处理数据。不过 Map 的性能在某些极端场景下不如对象,尤其是在处理大量数据时,对象可以通过 Object.fromEntries 来优化性能。这些都是我踩过的坑,现在分享给你。 在分布式缓存系统中,比如 Redis Cluster,哈希表的使用需要考虑到数据分片和一致性。我之前在做缓存一致性校验时,发现某些 key 被错误地分片到不同的 node,导致数据不一致。后来我改用哈希 tag 的方式,把相关的 key 绑定在一起,这样就能保证它们落在同一个 node 上。这在 Redis 的分片策略中很常见,也能避免一些分布式问题。同时,我在处理大量数据时,使用了 Redis 的 hash 数据结构,比如 HSET 和 HMGET,这样能减少网络传输的开销,提升性能。这些都是我实际经历过的,不能胡编。