Valkey 是一个键值对数据库,在 Valkey 中的键值对是由字典(也称为 hash 表)保存的,如下图所示的链式哈希表。
在 Valkey8.0 之前,在哈希表中查找一个 key 及对应的 value 步骤如下描述:
计算 key 的 hash 值,找到对应的 bucket
遍历存储在 bucket 中通过链表连接的 entry,直到找到需要的 key
如果找到 key,再访问 key 映射的 RedisObj(也就是存储的 value),如果存储的 value 是 OBJ_ENCODING_RAW 类型,还需要进一步访问内存地址获取真正的数据
每一步操作都需要等待前面的步骤完成内存数据读取,整个访问过程是一个串行步骤,这种动态数据结构会阻碍处理器推测未来可以并行执行的内存加载指令的能力,因此访问内存成为 Valkey 处理数据的性能瓶颈。
在 Valkey8.0 中,对于具有可执行命令的客户端(即 IO 线程已解析命令的客户端),主线程将创建一个最多包含 16 条命令的批次,批量处理这些命令。并且执行命令前,先将命令参数预取到主线程的一级缓存中,再将所有命令所需的字典条目 entry 和值 value 都从字典中预取。
同时,预取命令所需的字典条目 entry 和值 value 时遍历字典的方式与上述查找 key 过程类似,不同的是,每个 key 每次只执行一步,然后不等待从内存中完成读取数据,而只是预取数据,然后继续执行下一个 key 的下一次预取动作。这样当所有 key 都遍历完成第一步后,开始执行第二步的时候,执行第二步所需的第一步数据已经预取到了 L1 高速缓存。这样通过交错执行所有 key,并且结合预取,达到分摊访问内存的效果。
单个 key 预取流程如下所示:
每批次多个 key 预取流程则是循环遍历每个 key 交错执行上述步骤,先预取其中一个 key 的 bucket,然后不会执行预取该 key 的 entry,因为此时如果接着流程预取该 key 的 entry,需要等待将该 key 的 bucket 内存读取出来;而是执行下一个 key 的预取动作。也就是达成所有 key 的预取动作一直在并行执行效果,分摊内存访问时间。
多个 key 批量预取流程如下所示:
循环遍历每个 key 交错执行上述步骤,先执行一个 key 的预取动作,然后交错执行另一个 key 的预取动作,所有 key 的预取动作并行执行,降低所有 key 访问内存总时间。
同一批次所有 key 和 value 都完成预取后,主线程开始批量执行命令。相比在 Valkey8.0 之前的版本中,主线程逐个处理每个客户端命令,批量预取数据加上批量处理,大幅提升单节点 Valkey 服务器性能,社区测试单节点 Valkey 访问请求可以达到每秒 120W。