目标:有2个的文件,每个有3TB左右,每一行代表一个文件,现在需要对比2个文件中有哪些不一致。源端3TB,目标端3TB,需要找出源端中存在,但是目标端不存在的数据。
红色代表源端,橙色代表目标端,找到箭头所指的虚线部分
go的编译
go build -o 【输出文件名称】 【go语言路径.go】
- 对原端和目标端文件进行检测,看看是否有序 源代码
Go
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
// checkFileOrdered 快速检测文件有序性(字典序升序)
// 检测到1条无序记录立即返回,极低内存占用,支持1TB+超大文件
func checkFileOrdered(filePath string) (
isOrdered bool,
prevLine string, // 无序的上一行内容
currentLine string, // 无序的当前行内容
prevLineNum int, // 无序的上一行号
currentLineNum int, // 无序的当前行号
err error,
) {
// 以只读模式打开文件,优化超大文件读取性能
file, err := os.OpenFile(filePath, os.O_RDONLY, 0444)
if err != nil {
return false, "", "", 0, 0, fmt.Errorf("打开文件失败: %w", err)
}
defer file.Close() // 延迟关闭文件,避免资源泄露
// 配置4MB大缓冲区,减少系统调用,提升超大文件读取效率
bufSize := 4 * 1024 * 1024
scanner := bufio.NewScanner(file)
buf := make([]byte, bufSize)
scanner.Buffer(buf, bufSize)
var (
previousLine string
lineCount int // 有效行号计数器
firstValid bool = true // 是否是第一个有效行
)
// 逐行扫描,不加载全文件,内存占用可控
for scanner.Scan() {
rawLine := scanner.Text()
// 去除行首尾空白字符,确保比较准确性(可按需删除此行)
processedLine := strings.TrimSpace(rawLine)
// 跳过空行(有效行不包含空行,可按需删除此判断)
if processedLine == "" {
continue
}
lineCount++ // 有效行号递增
// 第一行有效行,仅记录不比较
if firstValid {
previousLine = processedLine
firstValid = false
continue
}
// 字符串字典序比较,检测是否无序(升序规则)
if strings.Compare(previousLine, processedLine) > 0 {
// 检测到无序,立即返回结果,终止后续扫描
return false, previousLine, processedLine, lineCount - 1, lineCount, nil
}
// 有序则更新上一行内容,继续扫描
previousLine = processedLine
}
// 检查扫描过程中的IO错误
if err := scanner.Err(); err != nil {
return false, "", "", 0, 0, fmt.Errorf("读取文件失败: %w", err)
}
// 遍历完成未检测到无序,说明文件有序
return true, "", "", 0, 0, nil
}
func main() {
// 检查命令行参数数量,确保传入了文件名称
if len(os.Args) < 2 {
fmt.Println("使用方法:./check <文件名称>")
fmt.Println("示例:./check source.txt")
os.Exit(1) // 参数不足,异常退出
}
// 获取命令行传入的文件名称(os.Args[0]是程序名,os.Args[1]是第一个参数即文件名称)
fileName := os.Args[1]
fmt.Printf("开始检测文件「%s」的有序性(缓冲区大小:4MB)\n", fileName)
// 执行有序性检测
isOrdered, prevLine, currLine, prevNum, currNum, err := checkFileOrdered(fileName)
if err != nil {
fmt.Printf("检测失败:%v\n", err)
os.Exit(1)
}
// 输出检测结果
if isOrdered {
fmt.Printf("检测完成:文件「%s」所有有效行均按字典序升序排列,无无序记录\n", fileName)
return
}
// 检测到无序,输出详情并立即退出
fmt.Printf("检测到无序记录,立即退出!\n")
fmt.Printf("第%d行:「%s」\n", prevNum, prevLine)
fmt.Printf("第%d行:「%s」\n", currNum, currLine)
fmt.Printf("问题:第%d行字典序大于第%d行,违反升序排列规则\n", prevNum, currNum)
os.Exit(1)
}
使用:
./check_sort 【文件路径】
2.若无序则进行排序(多路归并) 源代码
注意
// 每个临时小文件的最大行数(可调整:内存越大,该值可设越大,效率越高)800w行分一个文件,可以修改
根据自己的改动,800w的文件大小应该小于可用内存值,并且留20%的冗余
Go
package main
import (
"bufio"
"container/heap"
"flag"
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"sync"
)
// 配置项(可根据你的机器内存调整)
const (
// 每个临时小文件的最大行数(可调整:内存越大,该值可设越大,效率越高)800w行分一个文件,可以修改
maxTempFileLines = 80000000
// 临时文件前缀(用于后续清理和识别)
tempFilePrefix = "tmp_file_sort_"
)
// 命令行参数
var (
inputFileName string
outputFileName string
tempDir string // 新增:临时文件目录参数
)
// 优先级队列元素:用于多路归并
type HeapElement struct {
line string // 行内容
fileIdx int // 对应临时文件的索引
scanner *bufio.Scanner // 对应临时文件的扫描器
file *os.File // 保存文件句柄,用于后续关闭
valid bool // 该元素是否有效(是否还有未读取的行)
}
// 优先级队列(最小堆):按行内容字典序排序
type PriorityQueue []*HeapElement
func (pq PriorityQueue) Len() int { return len(pq) }
func (pq PriorityQueue) Less(i, j int) bool {
// 按UTF-8字典序比较,保留特殊字符排序规则
return pq[i].line < pq[j].line
}
func (pq PriorityQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
}
func (pq *PriorityQueue) Push(x interface{}) {
elem := x.(*HeapElement)
*pq = append(*pq, elem)
}
func (pq *PriorityQueue) Pop() interface{} {
old := *pq
n := len(old)
elem := old[n-1]
*pq = old[0 : n-1]
return elem
}
func init() {
// 解析命令行参数
flag.StringVar(&inputFileName, "f", "", "输入文件名称(必填,支持1TB超大文件)")
flag.StringVar(&outputFileName, "o", "", "输出文件名称(可选,默认:原文件名_sorted.后缀)")
flag.StringVar(&tempDir, "t", "", "临时文件存储目录(可选,默认当前工作目录)") // 新增:-t 参数指定临时目录
flag.Parse()
// 兼容直接传递输入文件名的格式(./程序名 输入文件名)
if inputFileName == "" && len(flag.Args()) > 0 {
inputFileName = flag.Args()[0]
}
// 参数校验
if inputFileName == "" {
fmt.Fprintf(os.Stderr, "使用方法1:%s 输入文件名 [输出文件名]\n", os.Args[0])
fmt.Fprintf(os.Stderr, "使用方法2:%s -f 输入文件名 -o 输出文件名 -t 临时文件目录\n", os.Args[0])
fmt.Fprintf(os.Stderr, "参数说明:-t 指定临时文件存放路径,若不指定则使用当前工作目录\n")
os.Exit(1)
}
// 自动生成输出文件名(若未指定)
if outputFileName == "" {
lastDotIndex := strings.LastIndex(inputFileName, ".")
if lastDotIndex == -1 {
outputFileName = inputFileName + "_sorted"
} else {
outputFileName = inputFileName[:lastDotIndex] + "_sorted" + inputFileName[lastDotIndex:]
}
}
// 可选:校验指定的临时目录是否存在(若用户指定了临时目录)
if tempDir != "" {
info, err := os.Stat(tempDir)
if err != nil {
// 目录不存在,报错并提示
fmt.Fprintf(os.Stderr, "指定的临时目录 %s 不存在,请先创建该目录\n", tempDir)
os.Exit(1)
}
if !info.IsDir() {
// 路径存在,但不是目录
fmt.Fprintf(os.Stderr, "指定的路径 %s 不是一个有效目录\n", tempDir)
os.Exit(1)
}
}
}
func main() {
// 步骤1:拆分大文件为多个排序后的临时小文件
tempFilePaths, err := splitAndSortLargeFile(inputFileName)
if err != nil {
fmt.Fprintf(os.Stderr, "拆分并排序大文件失败:%v\n", err)
cleanTempFiles(tempFilePaths) // 清理临时文件
os.Exit(1)
}
defer cleanTempFiles(tempFilePaths) // 程序结束后自动清理临时文件
// 步骤2:多路归并所有临时小文件,生成最终有序文件
err = mergeSortedTempFiles(tempFilePaths, outputFileName)
if err != nil {
fmt.Fprintf(os.Stderr, "归并临时文件失败:%v\n", err)
cleanTempFiles(tempFilePaths)
os.Exit(1)
}
fmt.Printf("超大文件排序完成!\n原文件:%s\n输出文件:%s\n临时文件目录:%s\n临时文件已自动清理\n",
inputFileName, outputFileName, getTempDirDisplay())
}
// getTempDirDisplay 获取临时目录显示字符串(兼容默认目录)
func getTempDirDisplay() string {
if tempDir != "" {
return tempDir
}
// 获取当前工作目录(用于显示)
wd, err := os.Getwd()
if err != nil {
return "当前工作目录(获取失败)"
}
return wd
}
// splitAndSortLargeFile 拆分大文件为多个排序后的临时小文件
func splitAndSortLargeFile(inputFile string) ([]string, error) {
var tempFilePaths []string
file, err := os.Open(inputFile)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
var lines []string
tempFileIdx := 0
// 逐行读取,积累到指定行数后生成排序后的临时文件
for scanner.Scan() {
lines = append(lines, scanner.Text())
// 达到临时文件最大行数,生成并排序临时文件
if len(lines) >= maxTempFileLines {
tempFilePath, err := createSortedTempFile(lines, tempFileIdx)
if err != nil {
return tempFilePaths, err
}
tempFilePaths = append(tempFilePaths, tempFilePath)
// 重置切片,继续积累下一批行
lines = lines[:0]
tempFileIdx++
}
}
// 处理最后一批不足maxTempFileLines的行
if len(lines) > 0 {
tempFilePath, err := createSortedTempFile(lines, tempFileIdx)
if err != nil {
return tempFilePaths, err
}
tempFilePaths = append(tempFilePaths, tempFilePath)
}
// 检查扫描错误
if err := scanner.Err(); err != nil {
return tempFilePaths, err
}
return tempFilePaths, nil
}
// createSortedTempFile 对行切片排序并写入临时文件(支持自定义临时目录)
func createSortedTempFile(lines []string, idx int) (string, error) {
// 排序行(内存排序,此时lines大小在内存承受范围内)
sort.Strings(lines)
// 拼接临时文件路径(兼容自定义临时目录)
var tempFilePath string
if tempDir != "" {
// 使用 filepath.Join 自动适配 Windows(\)和 Linux/Mac(/)的路径分隔符
tempFileName := fmt.Sprintf("%s%d.tmp", tempFilePrefix, idx)
tempFilePath = filepath.Join(tempDir, tempFileName)
} else {
// 未指定临时目录,使用当前工作目录
tempFilePath = fmt.Sprintf("%s%d.tmp", tempFilePrefix, idx)
}
// 创建临时文件
file, err := os.Create(tempFilePath)
if err != nil {
return "", err
}
defer file.Close()
writer := bufio.NewWriter(file)
defer writer.Flush()
// 写入排序后的行
for _, line := range lines {
_, err := writer.WriteString(line + "\n")
if err != nil {
os.Remove(tempFilePath) // 写入失败,删除临时文件
return "", err
}
}
return tempFilePath, nil
}
// mergeSortedTempFiles 多路归并所有排序后的临时文件
func mergeSortedTempFiles(tempFilePaths []string, outputFile string) error {
if len(tempFilePaths) == 0 {
return fmt.Errorf("无临时文件可归并")
}
// 打开所有临时文件,并初始化扫描器和文件句柄
var heapElements []*HeapElement
for idx, path := range tempFilePaths {
// 1. 打开文件,保存文件句柄
file, err := os.Open(path)
if err != nil {
return err
}
// 2. 初始化扫描器
scanner := bufio.NewScanner(file)
// 3. 读取每个临时文件的第一行
if scanner.Scan() {
elem := &HeapElement{
line: scanner.Text(),
fileIdx: idx,
scanner: scanner,
file: file,
valid: true,
}
heapElements = append(heapElements, elem)
} else {
file.Close() // 无数据,直接关闭文件
if err := scanner.Err(); err != nil {
return err
}
}
}
// 初始化优先级队列(最小堆)
pq := &PriorityQueue{}
heap.Init(pq)
for _, elem := range heapElements {
heap.Push(pq, elem)
}
// 创建输出文件
outFile, err := os.Create(outputFile)
if err != nil {
return err
}
defer outFile.Close()
writer := bufio.NewWriter(outFile)
defer writer.Flush()
// 多路归并核心逻辑:不断弹出堆顶最小元素,写入输出文件,再从对应文件读取下一行入堆
for pq.Len() > 0 {
// 弹出堆顶最小元素
topElem := heap.Pop(pq).(*HeapElement)
if !topElem.valid {
continue
}
// 写入输出文件
_, err := writer.WriteString(topElem.line + "\n")
if err != nil {
return err
}
// 从当前临时文件读取下一行
scanner := topElem.scanner
if scanner.Scan() {
// 更新元素内容,重新入堆
topElem.line = scanner.Text()
heap.Push(pq, topElem)
} else {
// 该临时文件已读取完毕,关闭文件(使用保存的 file 句柄)
if topElem.file != nil {
topElem.file.Close()
}
// 检查扫描错误
if err := scanner.Err(); err != nil {
return err
}
}
}
return nil
}
// cleanTempFiles 清理所有临时文件
func cleanTempFiles(tempFilePaths []string) {
var wg sync.WaitGroup
for _, path := range tempFilePaths {
wg.Add(1)
go func(p string) {
defer wg.Done()
if err := os.Remove(p); err != nil {
fmt.Fprintf(os.Stderr, "清理临时文件 %s 失败:%v\n", p, err)
}
}(path)
}
wg.Wait()
}
使用:
nohup exe_sort_bigdata -f 【待排序文件地址】 -o 【输出文件地址】 -t 【磁盘存储临时文件地址】 &
3.比对程序(双指针)
原数据例子是
aaaaa/asadad/adadasd.txt,1300000
bbb/asdad/asdggbg.txt,1231233
类似这种,前面是文件路径,后面是文件更新的时间戳。
本次会判断文件路径相等 ,且源端时间戳小于目标端时间戳 。作为正确的一行。否则会被输出到2个不同的文件,文件路径不同的文件,和文件路径相同但是时间戳不同的文件
ps:如果源端时间大于目标,那就说明是源端更新了数据,但是目标端用的是旧的数据。
Go
package main
import (
"bufio"
"fmt"
"os"
"runtime"
"strconv"
"strings"
"sync"
)
// FileData 存储文件名和时间戳(完全保留原有结构体)
type FileData struct {
Name string
Timestamp int64
}
// 定义流式读取的行数据通道(用于传递单行文本质,不缓存全量数据)
type LineData struct {
FileData FileData
Err error
Done bool // 标记当前文件读取完成
}
// 流式解析已排序的文件(进一步优化内存占用,无全量缓存)
func streamParseSortedFile(filePath string, lineChan chan<- LineData, wg *sync.WaitGroup) {
defer func() {
lineChan <- LineData{Done: true} // 发送读取完成标记
wg.Done()
// 主动触发垃圾回收,释放当前协程内存
runtime.GC()
}()
f, err := os.Open(filePath)
if err != nil {
lineChan <- LineData{Err: fmt.Errorf("打开%s失败: %v", filePath, err)}
return
}
defer f.Close()
// 缩小读取缓冲区:从1MB改为256KB,进一步降低内存占用
scanner := bufio.NewScanner(f)
buf := make([]byte, 256*1024) // 256KB缓冲区,足够高效读取且内存友好
scanner.Buffer(buf, 256*1024)
// 复用临时变量:避免循环内重复创建变量导致内存累积
var (
line string
parts []string
ts int64
)
for scanner.Scan() {
line = strings.TrimSpace(scanner.Text())
if line == "" {
continue
}
// 分割文件名和时间戳(原有逻辑不变,复用parts切片)
parts = strings.SplitN(line, ",", 2)
if len(parts) != 2 {
fmt.Printf("警告:%s 无效行,跳过 -> %s\n", filePath, line)
// 重置parts,避免残留数据占用内存
parts = nil
continue
}
// 转换时间戳(原有逻辑不变,复用ts变量)
ts, err = strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
if err != nil {
fmt.Printf("警告:%s 无效时间戳,跳过 -> %s\n", filePath, line)
// 重置parts和ts,释放临时内存
parts = nil
ts = 0
continue
}
// 逐行发送数据,发送后立即重置临时变量
lineChan <- LineData{
FileData: FileData{
Name: strings.TrimSpace(parts[0]),
Timestamp: ts,
},
}
// 主动重置临时变量,避免内存泄漏和累积
parts = nil
ts = 0
line = ""
}
if err := scanner.Err(); err != nil {
lineChan <- LineData{Err: err}
}
// 再次触发垃圾回收
runtime.GC()
}
// 双指针对比核心逻辑(完全保留业务逻辑,优化内存占用)
func compareAndExport(sourcePath, targetPath, outputPrefix string) {
// 严格限制CPU核心数≤12
runtime.GOMAXPROCS(12)
// 关键优化:缩小通道缓冲区(从100改为20),大幅降低内存占用
// 通道缓冲区越小,缓存的行数据越少,内存占用越低
sourceChan := make(chan LineData, 20)
targetChan := make(chan LineData, 20)
var wg sync.WaitGroup
// 启动协程流式解析文件(无全量内存加载)
wg.Add(2)
go streamParseSortedFile(sourcePath, sourceChan, &wg)
go streamParseSortedFile(targetPath, targetChan, &wg)
// 拼接输出文件名(原有逻辑不变)
valueErrFileName := fmt.Sprintf("%s_value_mismatch.txt", outputPrefix)
timeErrFileName := fmt.Sprintf("%s_time_mismatch.txt", outputPrefix)
// 创建输出文件(原有逻辑不变)
valueErrFile, err := os.Create(valueErrFileName)
if err != nil {
fmt.Printf("创建值差异文件失败: %v\n", err)
return
}
defer valueErrFile.Close()
timeErrFile, err := os.Create(timeErrFileName)
if err != nil {
fmt.Printf("创建时间差异文件失败: %v\n", err)
return
}
defer timeErrFile.Close()
// 缩小写入缓冲区:从1MB改为256KB,降低内存占用
valueWriter := bufio.NewWriterSize(valueErrFile, 256*1024)
defer valueWriter.Flush()
timeWriter := bufio.NewWriterSize(timeErrFile, 256*1024)
defer timeWriter.Flush()
// 流式数据变量(仅缓存单条数据,无批量缓存)
var (
srcData FileData
tgtData FileData
hasSrc bool
hasTgt bool
srcDone bool
tgtDone bool
srcErr error
tgtErr error
)
// 预读取第一行数据(原有逻辑不变)
select {
case line := <-sourceChan:
if line.Err != nil {
srcErr = line.Err
} else if !line.Done {
srcData = line.FileData
hasSrc = true
} else {
srcDone = true
}
}
select {
case line := <-targetChan:
if line.Err != nil {
tgtErr = line.Err
} else if !line.Done {
tgtData = line.FileData
hasTgt = true
} else {
tgtDone = true
}
}
// 处理解析错误(原有逻辑不变)
if srcErr != nil {
fmt.Printf("解析源文件失败: %v\n", srcErr)
return
}
if tgtErr != nil {
fmt.Printf("解析目标文件失败: %v\n", tgtErr)
return
}
// 双指针流式遍历(完全保留业务逻辑,无内存累积)
for !srcDone || !tgtDone || hasSrc || hasTgt {
// 补充未读取的数据
if !hasSrc && !srcDone {
select {
case line := <-sourceChan:
if line.Err != nil {
srcErr = line.Err
srcDone = true
} else if !line.Done {
srcData = line.FileData
hasSrc = true
} else {
srcDone = true
}
}
}
if !hasTgt && !tgtDone {
select {
case line := <-targetChan:
if line.Err != nil {
tgtErr = line.Err
tgtDone = true
} else if !line.Done {
tgtData = line.FileData
hasTgt = true
} else {
tgtDone = true
}
}
}
// 处理错误(原有逻辑不变)
if srcErr != nil {
fmt.Printf("解析源文件失败: %v\n", srcErr)
return
}
if tgtErr != nil {
fmt.Printf("解析目标文件失败: %v\n", tgtErr)
return
}
// 双指针对比逻辑(完全和原有一致,无业务修改)
switch {
// 1. 两个文件都有数据可对比
case hasSrc && hasTgt:
src := srcData
tgt := tgtData
switch {
// 文件名相同:对比时间戳
case src.Name == tgt.Name:
if src.Timestamp > tgt.Timestamp {
line := fmt.Sprintf("%s,%d | target时间戳: %d\n", src.Name, src.Timestamp, tgt.Timestamp)
_, err := timeWriter.WriteString(line)
if err != nil {
fmt.Printf("写入时间差异文件失败: %v\n", err)
return
}
// 写入后立即刷新(可选,避免缓冲区累积过多数据)
_ = timeWriter.Flush()
}
// 释放当前数据,重置标记
srcData = FileData{} // 主动清空,释放内存
tgtData = FileData{}
hasSrc = false
hasTgt = false
// source文件名更小:写入值差异文件
case src.Name < tgt.Name:
line := fmt.Sprintf("%s,%d\n", src.Name, src.Timestamp)
_, err := valueWriter.WriteString(line)
if err != nil {
fmt.Printf("写入值差异文件失败: %v\n", err)
return
}
// 写入后立即刷新(可选)
_ = valueWriter.Flush()
// 释放当前数据,重置标记
srcData = FileData{}
hasSrc = false
// target文件名更小:忽略
default:
// 释放当前数据,重置标记
tgtData = FileData{}
hasTgt = false
}
// 2. 仅source有剩余数据:写入值差异文件
case hasSrc && (!srcDone || tgtDone):
line := fmt.Sprintf("%s,%d\n", srcData.Name, srcData.Timestamp)
_, err := valueWriter.WriteString(line)
if err != nil {
fmt.Printf("写入值差异文件失败: %v\n", err)
return
}
_ = valueWriter.Flush()
// 释放当前数据
srcData = FileData{}
hasSrc = false
// 3. 仅target有剩余数据:忽略
case hasTgt && (!tgtDone || srcDone):
// 释放当前数据
tgtData = FileData{}
hasTgt = false
// 4. 无数据可处理,退出循环
default:
break
}
// 循环内定期触发垃圾回收,防止内存累积
runtime.GC()
}
// 等待协程结束,关闭通道
wg.Wait()
close(sourceChan)
close(targetChan)
// 最终触发一次垃圾回收
runtime.GC()
fmt.Printf("对比完成!\n")
fmt.Printf("值不符合(source有,target无):%s\n", valueErrFileName)
fmt.Printf("时间不符合(source时间 > target时间):%s\n", timeErrFileName)
}
func main() {
// 检查命令行参数(原有逻辑不变)
if len(os.Args) != 4 {
fmt.Println("使用方法:./compare [source.txt] [target.txt] [输出前缀]")
fmt.Println("示例:./compare source.txt target.txt rs")
return
}
// 强制触发一次初始垃圾回收,释放启动时的冗余内存
runtime.GC()
// 获取参数并执行对比
sourceFile := os.Args[1]
targetFile := os.Args[2]
outputPrefix := os.Args[3]
compareAndExport(sourceFile, targetFile, outputPrefix)
}
使用:
./diff_tos2 【源端文件】 【目标端文件】 【结果输出前缀】