很长一段时间里,我以为 Hash 是某个特定的函数,直到深入研究后才发现------Hash 是一种转化思想,类似于"协议",由谁来实现、如何实现并不唯一。本文将从词源、本质、常见算法、实际应用场景全方位拆解 Hash。
一、Hash 这个词从哪来的
Hash 最早是一个厨房用语 ,来自法语 hacher ,意思是"切碎、剁碎"。法语 hacher 又源自古法语 hache(斧头)。
英语里至今还有一道菜叫 corned beef hash------把牛肉和土豆切碎混炒。剁完之后,你已经认不出原来的食材了,但同样的食材、同样的剁法,出来的结果是一样的。
1953 年,IBM 研究员 Hans Peter Luhn 在一篇内部备忘录中描述了一种通过数学函数将数据"打散"后快速查找的技术。后来学术界发现这个过程和"剁碎"的意象非常吻合:
- 输入一段完整的数据(一块食材)
- 经过函数处理后变成一段面目全非的短输出(剁成碎末)
- 无法从碎末还原出原来的食材(不可逆)
- 同样的食材同样剁法,结果一致(确定性)
于是 Hash Function(哈希函数)这个术语在 1960 年代被正式确立。
中文"哈希"是纯粹的音译 ,就像"巧克力"翻译自 chocolate。另一个译名"散列"是意译,强调"打散排列"的效果。两者都常见,"哈希"使用更广泛。
二、Hash 的本质:一种契约,而非某个具体函数
这是理解 Hash 最关键的一步。
Hash 是一种规范(specification),不是一种实现(implementation)。 就像"排序"不是指某个具体算法,而是"把无序变有序"这个目标------冒泡、快排、归并都是排序的不同实现。
Hash 的契约是:
| 约定 | 含义 |
|---|---|
| 输入任意长度 | 可以是一个字符、一篇文章、一个文件 |
| 输出固定长度 | 无论输入多大,输出总是固定位数 |
| 确定性 | 同样的输入,永远得到同样的输出 |
| 高效性 | 计算速度要快 |
| 均匀性 | 输出尽量均匀分布,避免扎堆 |
至于具体怎么实现这个契约,由不同的哈希算法各自决定。就像 HTTP 是协议,Nginx 和 Apache 都是实现;Hash 是思想,MD5 和 SHA256 都是实现。
三、常见的 Hash 函数全景
3.1 非加密型哈希函数(追求速度和均匀)
这类函数不在乎安全性,追求的是极致的速度和均匀的分布,主要用于数据结构内部。
DJB2
hash = 5381
for each char c in string:
hash = hash * 33 + c
- 发明者:Daniel J. Bernstein
- 特点:实现极其简单,只用乘法和加法
- 应用:早期很多语言的字符串哈希默认实现
MurmurHash
- 发明者:Austin Appleby(2008)
- 特点:速度极快,分布极均匀,针对现代 CPU 优化
- 应用:Redis 的字典内部哈希、Hadoop、Cassandra、Elasticsearch 的分片路由
xxHash
- 发明者:Yann Collet(2012,就是写 zstd 压缩算法的那位)
- 特点:目前已知最快的非加密哈希函数之一
- 应用:Linux 内核、大数据框架中的校验和
CRC32 / CRC16
- 全称:Cyclic Redundancy Check(循环冗余校验)
- 特点:本质是除法取余,计算快,但碰撞率稍高
- 应用:网络协议校验(以太网帧用 CRC32)、Redis Cluster 用 CRC16 计算槽
FNV(Fowler--Noll--Vo)
hash = offset_basis
for each byte b in data:
hash = hash XOR b
hash = hash * FNV_prime
- 特点:实现简单,碰撞率低
- 应用:DNS 服务器、各种编程语言内置哈希
SipHash
- 特点:兼顾速度和安全性,能防御 HashDoS 攻击
- 应用:Python 3.4+、Rust、Ruby 的字典默认哈希函数
为什么 Python 要换成 SipHash? 因为如果哈希函数可预测,攻击者可以构造大量碰撞的 key,让哈希表退化成链表(O(n) 查找),导致拒绝服务。SipHash 引入了随机种子,使得攻击者无法预测哈希值。
3.2 加密型哈希函数(追求安全性)
这类函数在上面的基础契约上,额外增加了安全性要求:
| 安全要求 | 含义 |
|---|---|
| 抗原像攻击 | 给定哈希值 h,无法反推出输入 x |
| 抗碰撞攻击 | 极难找到两个不同输入 x ≠ y 使得 hash(x) == hash(y) |
| 雪崩效应 | 输入变一个 bit,输出变化超过 50% |
MD5(Message Digest 5)
-
发明者:Ronald Rivest(1991)
-
输出:128 位(32 个十六进制字符)
-
现状:已被破解,2004 年王小云教授团队找到了高效的碰撞方法
-
现在还能用吗:不能用于安全场景(数字签名、密码存储),但仍可用于文件校验(只要不涉及对抗攻击)
MD5("hello") = 5d41402abc4b2a76b9719d911017c592
MD5("hellp") = 9f27079dbf2b3e20c18bee20e4dd5049 // 只差一个字母,结果天差地别
SHA 家族(Secure Hash Algorithm)
| 算法 | 输出长度 | 状态 | 典型应用 |
|---|---|---|---|
| SHA-1 | 160 位 | 已不安全,Google 2017 年实现碰撞 | Git 的 commit ID(历史原因) |
| SHA-256 | 256 位 | 安全,广泛使用 | 比特币挖矿、SSL 证书、区块链 |
| SHA-512 | 512 位 | 安全,更长 | 高安全要求场景 |
| SHA-3 | 可变 | 最新标准(2015),不同于 SHA-2 的架构 | 下一代安全场景 |
bcrypt / scrypt / Argon2(密码哈希函数)
这是一类特殊的哈希函数,专门设计来故意变慢:
-
普通哈希追求快,但密码哈希要慢------防止暴力破解
-
bcrypt 引入 salt (随机盐)和 cost factor(计算轮数)
-
scrypt 额外消耗大量内存,对抗 GPU/ASIC 暴力破解
-
Argon2 是 2015 年密码哈希竞赛的冠军,目前公认最佳
同样的密码,因为 salt 不同,每次结果都不一样
bcrypt("password123") = 2b12LJ3m4ys3Lg...(第一次) bcrypt("password123") = 2b12Kx9q2Wd5Rp...(第二次)
四、Hash 在实际系统中的应用全景
4.1 数据结构:哈希表(HashMap)
这是 Hash 最经典的应用。几乎所有编程语言的"字典"底层都是哈希表:
Java → HashMap 使用扰动函数(高16位异或低16位)
Python → dict 使用 SipHash(防 HashDoS)
Go → map 使用随机种子 + 类似 AES 的哈希
Redis → dict 使用 MurmurHash2(内部字典),CRC16(集群分槽)
当两个 key 的哈希值碰撞时,不同语言的解决策略也不同:
- 链地址法:碰撞的 key 挂成链表(Java 8 之前的 HashMap,Redis 的 dict)
- 链表 + 红黑树:链表长度超过 8 时转为红黑树(Java 8+ 的 HashMap)
- 开放寻址法:碰撞后找下一个空位(Python 的 dict,Go 的 map)
4.2 分布式系统:一致性哈希(Consistent Hashing)
普通取模 hash(key) % N 有个致命问题:N(节点数)一变,几乎所有 key 都要重新映射。
一致性哈希的解决思路:把哈希值空间组织成一个环(0 ~ 2^32-1),节点和 key 都映射到环上,key 顺时针找到最近的节点。当增删节点时,只有相邻的少量 key 需要迁移。
应用场景:
- Memcached 客户端分片
- Amazon Dynamo 的数据分区
- CDN 的请求路由
Redis Cluster 选择了另一条路------哈希槽(Hash Slot),用 16384 个固定槽做中间层,槽的迁移粒度更可控。
4.3 数据完整性校验
# 下载文件后验证 SHA256
sha256sum ubuntu-24.04-desktop-amd64.iso
# 对比官方公布的哈希值,一致则文件未被篡改
这就是利用了哈希的确定性------同样的文件一定产生同样的哈希值,文件被修改哪怕一个字节,哈希值就完全不同。
4.4 Git 的版本控制
Git 的每一个对象(commit、tree、blob)都用 SHA-1 哈希作为 ID:
git log --oneline
# a1b2c3d feat: add login page
# e4f5g6h fix: null pointer bug
这些 a1b2c3d 就是 commit 内容的 SHA-1 哈希值前几位。Git 用它来保证:任何内容的篡改都会导致哈希值变化,整个历史链条就断裂了。
4.5 区块链与比特币挖矿
比特币的"挖矿"本质上是一个暴力求解哈希的过程:
找到一个 nonce,使得 SHA256(SHA256(区块头 + nonce)) < 目标值
因为哈希的不可逆性,没有捷径,只能一个一个试。这就是"工作量证明"(Proof of Work)的原理。
4.6 布隆过滤器(Bloom Filter)
布隆过滤器用多个不同的哈希函数同时映射一个元素,判断元素"可能存在"或"一定不存在":
应用场景:
- 浏览器判断 URL 是否在恶意网站黑名单中(Chrome 用的就是 Bloom Filter)
- 数据库查询前快速判断 key 是否可能存在,减少无效磁盘读取(HBase、RocksDB)
- 爬虫的 URL 去重
4.7 负载均衡
Nginx 可以根据请求的某个字段做哈希,保证同一个用户始终路由到同一台后端服务器:
upstream backend {
hash $request_uri consistent; # 根据 URI 一致性哈希
server 192.168.1.1;
server 192.168.1.2;
server 192.168.1.3;
}
五、Hash 过程的完整拆解
回到最初的困惑:以 Redis Cluster 为例,CRC16(key) % 16384 这个过程里到底哪部分是 Hash?
key → CRC16(key) → % 16384 → slot → node
↑ ↑
这是哈希 这是取模映射
(把数据"剁碎" (把哈希结果
成固定长度) 压缩到指定范围)
CRC16 是哈希函数,负责把任意长度的 key 变成一个 0~65535 的 16 位整数。
% 16384 是取模映射,负责把哈希结果进一步压缩到 0~16383 的槽范围。
业界习惯把整个过程统称为"hash",但严格来说,取模本身不是哈希,它是对哈希结果的再映射。
六、一张图总结
Hash:一种将任意输入映射为固定输出的思想
|
┌────────────────────────┴─────────────────────────┐
非加密型(追求速度) 加密型(追求安全)
| |
┌──────┬──────┬──────┬──────┐ ┌──────┬────────┬────────┬────────┐
DJB2 Murmur xxHash CRC FNV MD5 SHA-1 SHA-256 bcrypt
| | | | | | | | |
简单 极快 最快 校验和 简洁 已破解 不安全 主流安全 密码存储
七、总结
Hash 不是一个函数,而是一种思想,一种契约:
- 目标:把任意长度的输入,映射为固定长度的输出
- 实现:MD5、SHA256、MurmurHash、CRC16......都是这个契约的不同实现
- 侧重:非加密型追求速度均匀,加密型追求安全不可逆,密码型故意求慢
- 应用:从编程语言的字典、数据库索引,到分布式系统路由、区块链挖矿、文件校验------Hash 无处不在
理解了这一点,再回头看 Redis Cluster 的 CRC16(key) % 16384,就能清晰地分辨出:CRC16 是哈希(剁碎),取余是映射(分队),两者配合完成从 key 到 slot 的定位。
Hash 就像面向对象中的"接口"------它定义了"做什么",至于"怎么做",由每个具体的哈希算法自己决定。
如果这篇文章帮你理清了 Hash 的本质,欢迎点赞收藏。有问题欢迎评论区交流。