← 中文资源

简体中文版本

Murmur3 分片哈希

实现 32 位 Murmur3 哈希并把键稳定映射到分片,用一万个样本检查分布是否均衡。

阅读英文原文

功能说明

分库分表、缓存分片和并行处理都需要把键稳定映射到固定数量的桶。Murmur3 是常用的非加密哈希,速度快、雪崩效应好、实现短小,适合在应用内做分片决策。相同键和种子在任何进程、任何机器上都得到相同结果,因此扩容前无需共享状态。

示例包含完整的 32 位实现:按四字节分块混合,剩余一到三字节单独处理,最后用长度做终结混合。Shard 函数在哈希值上对分片数取模,并把一万个用户键分散到十六个桶中统计分布。

关键代码

主循环每次读取一个小端 uint32,乘以常量后循环左移十五位,再乘第二个常量并与累加值异或;累加值同样旋转并混合乘法常量。尾部用 switch 加 fallthrough 逐字节组装剩余数据,缺少高位时补零。终结阶段先异或长度,再执行两轮异或移位与乘法,使任意一位变化都能扩散到整个结果。

使用注意

Murmur3 不是加密哈希,不能用于签名或口令存储。取模会带来极小的偏差,分片数为二的幂时可以直接用位与代替取模。跨语言实现必须统一字节序,本例固定小端。种子为零时结果稳定,更换种子会整体打乱映射,不能用于平滑扩容。扩容时建议使用一致性哈希或虚拟节点,否则大部分键会重新分配。

验证方式

打印 abc 的哈希与所属分片,同一输入多次运行结果完全一致。十六个桶各约六百二十五条记录,最大值与最小值应在合理范围内,示例会输出完整计数数组以及最小最大值。可以换成 UUID 字符串再跑一遍,确认分布仍然均衡。

代码示例

package main

import (
	"encoding/binary"
	"fmt"
	"math/bits"
)

func Murmur3(data []byte, seed uint32) uint32 {
	const (
		c1 = 0xcc9e2d51
		c2 = 0x1b873593
	)
	h := seed
	blocks := len(data) / 4
	for i := 0; i < blocks; i++ {
		k := binary.LittleEndian.Uint32(data[i*4:])
		k *= c1
		k = bits.RotateLeft32(k, 15)
		k *= c2
		h ^= k
		h = bits.RotateLeft32(h, 13)
		h = h*5 + 0xe6546b64
	}
	tail := data[blocks*4:]
	var k uint32
	switch len(tail) {
	case 3:
		k ^= uint32(tail[2]) << 16
		fallthrough
	case 2:
		k ^= uint32(tail[1]) << 8
		fallthrough
	case 1:
		k ^= uint32(tail[0])
		k *= c1
		k = bits.RotateLeft32(k, 15)
		k *= c2
		h ^= k
	}
	h ^= uint32(len(data))
	h ^= h >> 16
	h *= 0x85ebca6b
	h ^= h >> 13
	h *= 0xc2b2ae35
	h ^= h >> 16
	return h
}

func Shard(key string, shards int) int {
	if shards <= 0 {
		panic("shards must be positive")
	}
	return int(Murmur3([]byte(key), 0) % uint32(shards))
}

func main() {
	fmt.Printf("hash(abc)=%08x shard=%d\n", Murmur3([]byte("abc"), 0), Shard("abc", 16))
	counts := make([]int, 16)
	for i := 0; i < 10000; i++ {
		counts[Shard(fmt.Sprintf("user:%d", i), 16)]++
	}
	minimum, maximum := counts[0], counts[0]
	for _, count := range counts {
		if count < minimum {
			minimum = count
		}
		if count > maximum {
			maximum = count
		}
	}
	fmt.Println("buckets:", counts)
	fmt.Printf("min=%d max=%d\n", minimum, maximum)
}