众所周知,git在版本切换的时候,速度非常的高,它是怎么做到的呢?
看下面这张图。
本文重点参考了 这才是真正的Git——Git内部原理揭秘! - 知乎,感兴趣的可以去看原文
快照而非复制
在 Git 中,文件版本的管理方式既不是直接创建新文本,也不是单纯记录行级补丁,而是基于 内容寻址的对象模型,通过 快照(snapshot) 的方式高效存储和管理版本差异。以下是详细的逻辑和流程:
1. Git 的核心机制:快照而非补丁
Git 的设计理念是保存每次提交的 完整文件快照(并非生成增量补丁),但会通过内部优化(如对象复用和压缩)避免冗余存储。这意味着:
- 如果文件未修改:Git 会直接复用之前的对象,无需存储新数据。
- 如果文件修改了(例如新增一行) :Git 会为修改后的文件生成一个新的不可变对象(Blob),但其底层存储会根据变化情况自动优化(例如通过 Delta 压缩节省空间)。
2. Git 的对象模型
Git 内部通过以下四类对象实现版本管理:
- Blob 对象:存储文件内容。文件内容的微小变化(如新增一行)会生成新的 Blob。
- Tree 对象:存储 目录结构(文件路径、文件权限等),记录当前版本下所有 Blob 和子 Tree 的指针(SHA-1 哈希值)。
- Commit 对象:记录提交的元数据(作者、时间、父提交等),并指向一个 Tree 对象(对应当前提交的目录结构)。
- Tag 对象:可为特定提交打标签(如版本号)。
3. 具体流程示例
假设有一个文件 example.txt,其内容变化如下:
初始版本(example.txt 内容):
Line 1
-
初次提交:
-
Git 将文件内容生成 Blob 对象(假设哈希为
A1B2C3)。 -
生成 Tree 对象,指向该 Blob。
-
生成 Commit 对象,指向这个 Tree。最终提交历史如图:
Commit_1 (parent: none) --> Tree_1 --> Blob_A1B2C3("Line 1")
-
新增一行后(example.txt 内容):
Line 1
Line 2
-
第二次提交:
-
Git 检测到文件内容变化,生成新的 Blob 对象(假设哈希为
D4E5F6)。 -
生成新的 Tree 对象,指向新 Blob。
-
生成 Commit 对象,父提交为
Commit_1。提交历史此时为:+----------------------------+ | V Commit_2 (parent: Commit_1) --> Tree_2 --> Blob_D4E5F6("Line 1\nLine 2")
-
结果:
- Blob 对象的变化:新增了一个 Blob(
D4E5F6),但未改动原始 Blob(A1B2C3)。 - 存储空间优化:实际存储中,Git 会自动对相似对象进行 delta 压缩(底层使用 Packfile 技术),使其在不损失快照特性的前提下,物理存储上接近补丁方式,但逻辑上仍是独立对象。
4. 为何不直接生成补丁?
- 优势 1:快速切换版本
Git 的「快照」模式允许直接通过 Tree 和 Blob 的指针还原任意版本的文件,无需逐层计算差异。而差异补丁(如 SVN)在恢复历史版本时可能需递归叠加所有历史变更,效率较低。 - 优势 2:数据完整性
每个对象的 SHA-1 哈希既是存储标识,也是校验和。任何数据篡改都会破坏哈希链,确保数据安全。 - 优势 3:分支和合并的高效性
快照机制使得分支仅需生成一个轻量级指针,而不是复制文件副本,极大提高了分支操作的速度。
5. 用户视角 vs 物理存储
-
用户视角:
git add或git commit生成的是新的 Blob 和 Tree。- 看到的是完整文件历史,而非补丁链。
-
物理存储:
- Git 会定期执行
git gc(自动垃圾回收),将松散对象(Loose Objects)压缩成 Packfile(类似增量存储的优化文件),以节省空间。
- Git 会定期执行
6. 验证示例
可以使用 Git 测试工具观察存储行为:
-
创建文件并提交:
git init echo "Line 1" > example.txt git add example.txt git commit -m "Initial commit"-
查看对象存储:
find .git/objects -type fOutput:
.git/objects/XX/XXXX...(初始 Blob 和 Tree)
-
-
修改文件并提交:
echo "Line 2" >> example.txt git add example.txt git commit -m "Add Line 2"-
对比新旧 Blob:
git cat-file -p HEAD:example.txt # 新内容 git cat-file -p HEAD~1:example.txt # 旧内容
-
总结
Git 通过内容寻址的快照模型(而非传统补丁链)管理文件的多个版本:
- 修改后的文件生成新 Blob:无论修改大小,逻辑上创建新对象。
- 底层按需压缩存储:物理存储通过 Packfile 优化,减少空间占用。
这种设计平衡了版本管理的灵活性和存储效率,适合复杂的协作场景和高频变更的代码库。
什么是快照
通俗解释:完整文件快照 ≠ 复制文件
可以用一个办公室里的“文档版本”场景来类比:
情景设定:
假设你有一份重要的《项目报告.txt》文件,需要频繁修改它,但每次修改都必须保存历史版本。
方法 1:复制文件(普通复制)
-
每次修改后,你都手动复制出一份新文件:
项目报告_v1.txt(原始内容)项目报告_v2.txt(加了一句话)项目报告_v3.txt(又改了几个字)
-
问题:
- 文件数量爆炸,占用大量磁盘空间(假设是 100MB 的文件,改 10 次就占用 1GB)。
- 无法快速对比差异,也无法轻松知道每个版本的修改时间和作者。
方法 2:完整文件快照(Git 的做法)
-
第一次保存(v1) :
- Git 把文件内容(比如全文 100MB)取一个唯一指纹(哈希值,如
A1B2C3),把内容存到仓库的一张小纸条(Blob)上。 - 效果:仓库里记录了“v1 是
A1B2C3对应的文本内容”。
- Git 把文件内容(比如全文 100MB)取一个唯一指纹(哈希值,如
-
第二次修改(v2,新增一行) :
- 如果新增的内容导致文件变成 101MB,Git 会直接生成一个新的指纹(比如
D4E5F6),存到另一张小纸条上。 - 关键点:Git 中每个版本的“快照”都是独立的,但它们在物理存储上可能不用真的占用 100MB + 101MB 的空间(后文解释优化)。
- 如果新增的内容导致文件变成 101MB,Git 会直接生成一个新的指纹(比如
-
第三次修改(v3,又改回 v1 的内容) :
- Git 会发现内容与 v1 完全相同(哈希值还是
A1B2C3)! - 此时它会直接复用 v1 的小纸条,无需额外存储新的内容。
- Git 会发现内容与 v1 完全相同(哈希值还是
核心区别:快照的逻辑和物理分离
用户看到的(逻辑快照)
-
Git 像魔法书一样,每次存修改后的完整文件,每个版本都能直接打开。比如:
- v1:
文本内容(100MB) - v2:
文本内容 + 一行(101MB) - v3:
改回成 v1 的内容(100MB)
- v1:
底层实现的秘密(物理存储优化)
-
相似内容高效存储:
如果新旧内容接近(比如 v1 和 v2 有重叠),Git 会自动用一种“压缩术”(Packfile)存储它们的差异部分,而不是完整复制。例如:- 实际物理存储:v1 存全文 100MB,v2 只存新增的内容(1MB)。
-
内容哈希唯一性:
只有内容变化了才会存新数据。如果未来 10 次提交都复用 v1 的内容,物理存储中它只会占一份 100MB。
效果对比
| 场景 | 文件复制 | Git 快照 |
|---|---|---|
| 存储空间 | 改 10 次 -> 10 倍空间占用 | 实际根据内容变化占用少量空间 |
| 恢复速度 | 需要从一堆文件里挨个找 | 直接跳转哈希值,瞬间还原版本 |
| 关系复杂度 | 独立文件,无法关联逻辑 | 自动构建版本树,清晰记录历史 |
举个更具体的例子
- 初始文件(v1) :
内容:今天天气不错。(哈希假设为111111) - 修改一次(v2) :
内容:今天天气不错。适合出去玩。(哈希222222) - 再改一次(v3) :
内容:今天天气不错。适合在家写代码。(哈希333333)
在 Git 的物理存储中可能:
v1(111111):存完整句子。v2(222222):由于新增的内容,可能存补丁(如“+ 适合出去玩”)。v3(333333):在 v2 的基础上,可能直接覆盖修改为“在家写代码”。
但 从用户视角:每次提交都能看到完整的“最终结果文件”,而不是一堆补丁。
总结
- 完整文件快照(Git) 是从用户逻辑层定义的:每个版本就像一个完整文件,可独立使用。
- 物理存储 实际上是智能优化的(复用相同内容、压缩差异),避免了普通复制的空间浪费。
- 复制文件 是简单粗暴的逐份保存,占用空间大,且版本管理困难。
- 快照 通过哈希指纹和压缩技术,实现了高效存储和精确追踪历史的平衡。
对于二进制,Git怎么办?
对于二进制文件,Git 的存储方式既不是简单“复制”整个新文件,也不完全等同于高效的增量存储,其实质取决于文件的具体类型和 Git 的优化策略。以下是逐步解析:
1. Git 的基础机制:一切皆对象
无论文件是文本还是二进制,Git 都会将每次修改后的内容视为一个新的 Blob 对象 存储。每次提交(git commit)时,Git 按以下逻辑处理:
- 文件未变 ⇒ 复用旧 Blob
- 文件变化 ⇒ 生成新 Blob
这意味着从 逻辑角度,每个二进制文件的修改版本都是独立存储的,类似“复制”。
2. 物理存储优化:Delta 压缩的局限性
虽然逻辑上每个 Blob 独立,但 Git 在物理存储时会通过 Packfile 技术对对象进行压缩,包括 Delta 压缩(记录差异而非完整文件)。
-
文本文件的优势:
文本内容由可读字符组成,变更通常是局部插入或删除,Delta 压缩能高效找到差异,仅需存储少量改动部分。 -
二进制文件的挑战:
- 非结构化数据:二进制文件(如图片、可执行文件、Office 文档)通常缺乏重复或线性模式,Delta 压缩难以找到有效差异。
- 微小修改可能导致全局变化:例如修改一张图片的某个像素,可能导致整个二进制数据的冗余位变化,使得 Delta 效果差。
- 默认不启用 Delta 压缩:Git 可能自动回避对二进制文件进行 Delta 压缩,避免浪费计算资源(需手动配置)。
3. 用户场景验证:二进制文件的存储行为
示例:频繁修改的二进制文件
假设有一个 image.jpg 文件,初始大小 2MB。
- 版本 1:生成 Blob
A(2MB) - 版本 2:仅修改了图片元数据 ⇒ 生成 Blob
B(2MB)
存储结果:
- 逻辑上:仓库保存了完整的
A和B两个 2MB 副本。 - 物理上:Git 可能通过 Packfile 将
A和B压缩为 Delta(比如合计 2.1MB),但具体效果取决于文件内容是否适合 Delta。
若文件改动幅度大(如完全替换图片内容),Delta 压缩可能无效 ⇒ 物理存储接近 4MB。
4. Git 的优化配置
用户可通过 .gitattributes 文件定义某些二进制文件类型是否启用 Delta 压缩:
# 示例:对 .psd 文件禁用 Delta 压缩(默认可能已经禁用)
*.psd -delta
# 对某些二进制格式(如压缩文件)启用 Delta 压缩
*.zip diff=delta
# Git 配置后运行垃圾回收强制压缩
git gc --aggressive
但这并不保证显著节省空间,作用因文件类型而异。
5. 性能与替代方案
问题:频繁修改二进制文件的 Git 仓库可能迅速膨胀(如每次提交多出数百 MB),影响同步速度和存储成本。
推荐解决方案:
-
Git LFS (Large File Storage) :
-
原理:将二进制文件存储在远程服务器(如 GitHub LFS),Git 仓库仅保存文本指针文件(约几 KB)。
-
示例:安装 Git LFS 后,配置追踪二进制文件类型:
git lfs install git lfs track "*.mp4" git add .gitattributes git commit -m "Track MP4 files with Git LFS" -
优势:仓库体积可控,支持按需下载大文件。
-
-
独立文件存储系统:
- 如通过对象存储(Amazon S3)、NAS 存储文件,仅在仓库中存储路径或链接。
总结
- Git 不是简单复制二进制文件:在底层会尝试通过 Packfile 和 Delta 压缩节省空间,但对大多数二进制格式效果有限。
- 不适用高频二进制修改场景:若需频繁修改二进制文件,应避免依赖原生 Git,选用 Git LFS 或分离存储。
- 关键区别对比:
| 场景 | 纯 Git 处理二进制文件 | 文本文件处理 |
|---|---|---|
| 逻辑存储 | 每个版本独立存储完整二进制 Blob | 类似二进制,但优化更显着 |
| 物理优化 | 部分获益于 Delta 压缩,但效率低下 | Delta 压缩高效,空间占用低 |
| 推荐管理方案 | 使用 Git LFS 或分离存储系统 | 原生 Git 即可 |
通过结合 Git 的特性与专用工具,可以最大限度地平衡二进制文件的版本管理效率和存储成本。