功能说明
分库分表、缓存分片和并行处理都需要把键稳定映射到固定数量的桶。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)
}