1、先从为什么1.8的HashMap引入红黑树说起

我们知道,在1.8中,链表长度大于等于8、数组长度大于等于64时会发生链表树化,将查找效率从O(n)提高到O(logn)。具体的原因有防范哈希函数太差时的哈希碰撞攻击、避免系统卡死。关于避免系统卡死,我的理解是这样的,首先多线程肯定不会用它,会选择ConcurrentHashMap;在单线程下,如果不做优化,在如下场景就会出问题:

  • 你从文件里读了几百万行日志,用 HashMap 做统计
  • 你从数据库里查了几百万条记录,临时放到 HashMap 里做处理
  • 你用 HashMap 做缓存,启动时一次性加载大量数据

这些情况下如果哈希函数过差,就会导致大量数据被挂到同一个桶的链表中,存储数据时写入越来越慢,百万级数据写入直接卡死。因为它在链表尾插法插入数据时需要O(n)复杂度判空、找尾部。而引入红黑树之后,效率变为O(logn)。可能看到这个,我们心里会想,不就是效率提高了吗?该卡死还是卡死。其实并非如此,我们可以通过他们的函数曲线进行对比:
在这里插入图片描述
可以看出,随着节点数增多,红黑树的查找效率会趋于平稳,节点数越多,性能差距越明显。如果是红黑树,哪怕节点再多,也能以一个相对可控的效率进行操作,有效缓解了单线程、高写入情况下的系统直接卡死情况。

为什么是二叉树不是三叉、四叉树?

我觉得这是对于维护的复杂度和系统性能之间的一个平衡点。二叉变三叉,同样是1亿个元素中查找,二叉树比较27次,三叉树比较17次,但是,实现一个自平衡的三叉树,其代码复杂度和节点维护成本(每个节点需要维护3个孩子指针)会远高于红黑树。为了这10次比较的微小性能提升,去极大地增加代码复杂度和内存开销,对于HashMap这个基础组件来说,非常不划算。

2、ConcurrentHashMap到Redis哈希表

2.1 ConcurrentHashMap

在去掉对于高并发的处理,ConcurrentHashMap可以近似看成是HashMap。因此以下就以它的数据结构进行讨论。上文讨论过红黑树能对极端情况下的数据节点查询提供一种可控的操作效率,但那是针对单线程情况下,可能不做任何处理都行。多线程情况下,哪怕有个可控的效率,在大量请求下,系统可能依然无法承受,所以才需要高并发下做性能调优:比如调整负载因子,优化哈希函数以此降低树上的节点数。

不过还有一种做法是使用缓存,如果数据变化不频繁,在这个 Map 前面加一层本地缓存(如 Caffeine),甚至可以直接挡掉 99% 的请求,剩下 1% 才落到ConcurrentHashMap 上。这方面我了解有限,就不展开了。
ConcurrentHashMap需要优化的场景,经过查阅资料发现,对于多级缓存,经常会使用它,防止redis单点故障导致服务不可用,因此就需要进行性能调优。同时这也应该就是高并发情况下,将链表树化的最直接目的。

2.2 Redis哈希表

核心结构:
Redis的哈希表核心结构让人很眼熟:
哈希表 (dictht):可以理解为你熟悉的那个 Node[] table,负责存储数据。
哈希节点 (dictEntry):包括 key、value,以及一个指向下一个节点的指针 next——没错,Redis也是用链地址法解决哈希冲突。

它和Java的ConcurrentHashMap区别如下:
区别一:没有引入红黑树
由于Redis的key类型基本固定为字符串、自带的哈希函数足够优秀,所以基本不存在哈希冲突过多导致链表过长的情况。再加上Redis的设计原则是尽可能加快响应速度,而引入红黑树,它的旋转、变色等操作会影响响应速度。而ConcurrentHashMap由于需要支持各种对象类型,再加上要重写hashcode方法,哈希冲突由ConcurrentHashMap本身无法控制,因此它需要红黑树提供一种极端情况下依然可控的操作效率。二者的设计原则的不同带来了完全不同的数据结构。
区别二:渐进式Rehash
跟ConcurrentHashMap的一次性扩容不同,它采用的是渐进式扩容。有几个特点:

  • 空间换时间:它准备了两个哈希表 ht[0] 和 ht[1]。当需要扩容时,它只做“分家”的规划(比如把 ht[1] 大小设为 ht[0] 的两倍),但不立刻搬家。
  • 化整为零:把搬家这个耗时操作,分摊到了后续每一次增、删、改、查的命令中。每次操作,它都会顺便把旧表里的一个桶的所有数据搬到新表。
  • 兜底策略:为了不让这个搬家过程因为没请求而永远完不成,Redis的后台定时任务也会主动来推进搬家的进度。

区别三:负载因子不同
ConcurrentHashMap的负载因子,通常不会超过1,这样才能兼顾内存和速度,因为它支持并发扩容,所以扩容频率略高的性能影响可以大幅缩减。如果超过1,说明查找数据时需要开始遍历链表了,性能会降低。而对于Redis的哈希表来说,它是单线程,扩容效率低,哪怕是有Rehash机制也不够弥补性能开销,所以需要增大负载因子,通常会大于1,这样就能减少扩容频率,从而减少由于Rehash导致的流量毛刺。

2.3 二者的协作-多级缓存

它们各自的优势,让它们可以完美协作,实现多级缓存。它们顺序如下:
请求 → 本地缓存(ConcurrentHashMap) → Redis → 数据库
本地缓存:拦截绝大部分热点读,它的设计可以说基本都是为读取的极致性能而服务的,比如上面提到的红黑树、小于1的负载因子、并发扩容,所以有极致的单机性能。
Redis:处理本地缓存 miss 的请求,同时承担跨服务共享、避免单点故障丢失数据。
数据库:最终数据源,兜底。

先看下两者性能对比
本地缓存(ConcurrentHashMap):纯内存操作,无网络、无序列化,单次 get 约 50–100 纳秒。
Redis 哈希:即使本机访问,也有网络栈 + 序列化成本,单次 hget 约 0.1–0.5 毫秒(即 100,000–500,000 纳秒)。
结论:单机性能上,ConcurrentHashMap 比 Redis 快约 1000–5000 倍。

为什么还要用 Redis:
跨服务共享:ConcurrentHashMap 只在一个 JVM 内,多服务无法直接访问(非要跨服务访问也可以,远程调用,只不过这就变成重复造redis的轮子了)。
持久化:Redis 数据可落盘,让它重启不丢;ConcurrentHashMap 随 JVM 消亡,服务重启之后数据清零。
高可用:Redis 支持主从/集群,单点故障没问题;ConcurrentHashMap 单机故障数据全都丢失。

2.4 总结

  1. 红黑树的引入不是为了优化正常情况,而是为了兜底最坏情况。

在单线程、海量数据、哈希函数不佳时,红黑树把查询效率从 O(n) 拉回到 O(log n),曲线趋于平稳,避免系统卡死。选择二叉树而非三叉/四叉树,是在“维护复杂度”和“性能收益”之间的工程平衡。

  1. ConcurrentHashMap 和 Redis 哈希表的设计差异,源于线程模型和使用场景的不同。
  2. 多级缓存的正确顺序:本地缓存 → Redis → 数据库

本地缓存(ConcurrentHashMap)比 Redis 快 1000–5000 倍,所以挡在最前面。Redis 负责跨服务共享、持久化、高可用。各司其职,才是高并发系统的常见设计。

  1. 没有完美的数据结构,只有合适的工程取舍。

红黑树、负载因子、扩容方式……每一个设计都是在对“内存、性能、复杂度、可维护性”做权衡。理解这些权衡,比记住结论更重要。

更多推荐