当前位置:首页 > 问答 > 正文

Redis哈希冲突真是个麻烦事,解决起来还挺复杂的那些挑战和细节

Redis在处理数据时,内部大量使用了哈希表这种数据结构,比如存储我们设置的键值对,理想情况下,每个键通过哈希函数计算后,都能得到一个独一无二的“座位号”,然后安安稳稳地坐在哈希表里属于自己的位置上,但现实很骨感,当不同的键计算出相同的“座位号”时,冲突就发生了,这就好比你去电影院,发现自己的座位上已经坐了别人,这时候就得想办法解决,Redis解决这个问题的方法本身不算稀奇,但把它做好、做得高效,里面全是挑战和复杂的细节。

最基础的挑战就是怎么处理这个“占座”问题,Redis和很多系统一样,采用了一种叫做“链地址法”的方法,简单说,就是当两个键的“座位号”一样时,它不强行赶走先来的那个,而是在这个座位后面拉一条链子,让后来者依次排下去,形成一个链表,这个方法听起来简单直接,但第一个麻烦就来了:如果冲突的键非常多,这条链子就会变得非常长,想象一下,一个座位后面排了成百上千的人,当你要找其中一个时,就得从第一个人开始,一个个问过去,直到找到你要找的那位,这在计算机里叫做“查找效率下降”,最坏的情况下,查找一个键所花的时间会变得很长,Redis那引以为傲的高速性能就会大打折扣,这是Redis必须面对的第一个严峻挑战,即如何避免链表变得过长,导致性能退化。

为了解决长链表造成的性能问题,Redis使出了它的关键武器:“扩容”,也就是业内常说的“Rehash”,这个思路不难理解:电影院座位不够导致大家挤在一起,那我们就换一个更大的电影院,有更多的座位,这样自然就能把人群疏散开,每个人都能找到空位坐下,具体到Redis,就是创建一个新的、更大的哈希表,然后把旧表里所有的键值对,重新计算它们在新区里的“座位号”,再一个个“搬迁”过去,这个过程,就是Rehash。

正是这个Rehash过程,包含了Redis为了解决哈希冲突而引入的最复杂、最精妙的细节,挑战在于,如果Redis存储的数据量非常大,比如有几个亿的键值对,那么一次性完成所有数据的搬迁工作,将会是一个极其耗时的操作,在这段时间里,Redis主线程可能被这个搬迁任务长时间占用,无法及时处理用户新发来的请求,导致服务出现明显的停顿甚至暂时不可用,这就像是给一个正在高速行驶的汽车更换所有轮胎,风险极高,这种停顿对于要求高可用的系统来说是致命的。

为了化解这个核心挑战,Redis设计并实现了一种非常巧妙的“渐进式Rehash”策略,它不追求一步到位,而是把庞大的搬迁任务拆分成无数个小任务,零敲碎打地完成,Redis会同时持有两个哈希表:旧的(当前正在用的)和新的(更大的那个),当开始Rehash后,每次有新的读写请求到来时,Redis除了处理这个请求本身,还会“顺便”从旧表里搬迁一小批键(比如几个)到新表里,Redis还会定时在后台任务中也进行一小批的搬迁,这样,就像蚂蚁搬家一样,经过一段时间,所有数据都会在用户几乎无感知的情况下,慢慢地、平稳地从旧表迁移到新表,当旧表清空后,Redis释放旧表的内存,完全切换到新表。

这个渐进式过程听起来很完美,但它本身也带来了新的复杂性,在Rehash进行期间,Redis需要同时维护两张表,当一个查找请求进来时,它得先在旧表里找,如果没找到,还得再去新表里找,删除和更新操作也一样,需要同时考虑键可能存在于哪张表,只有新增的键会被直接放到新表里,这套复杂的逻辑确保了数据在迁移过程中的一致性,但无疑增加了代码实现的复杂度和维护的难度,这是为了平滑解决哈希冲突问题所必须付出的代价。

除了主要的链地址法和渐进式Rehash,Redis在细节上还有其他考量,它的哈希函数的选择就非常讲究,需要尽可能快地将键打散,减少冲突发生的概率本身,一个好的哈希函数是预防冲突的第一道防线,触发Rehash的时机(负载因子)也需要精心设置,设置得太敏感,可能会造成不必要的频繁扩容,浪费内存和CPU;设置得太迟钝,又可能让链表过长的问题已经影响了性能才行动,这些参数的调优都需要深厚的经验和细致的测试。

Redis面对哈希冲突这个“麻烦事”,通过链地址法、触发扩容、特别是精巧的渐进式Rehash等一系列环环相扣的机制来应对,这些方案有效地解决了冲突带来的性能问题,但同时也引入了系统复杂性的显著提升,包括双表维护的逻辑复杂性、Rehash时机判断的微妙性等,可以说,Redis在哈希冲突处理上的挑战,核心是如何在保证高性能和高可用的前提下,平衡内存占用、CPU消耗和实现复杂度,而它的解决方案充分体现了工程上的智慧与折衷。

Redis哈希冲突真是个麻烦事,解决起来还挺复杂的那些挑战和细节