golang文件存储纠删码实现

简介: // Verify(shards [][]byte) (bool, error)。每个分片都是[]byte类型,分片集合就是[][]byte类型,传入所有分片,如果有任意的分片数据错误,就返回false。

一般我们存储简单处理就是写三副本,但是三副本的成本太大了,采用纠删码可以比较好的降低存储空间的成本,具体看下golang中的代码实现。

// 纠删码测试
// 将一个文件拆分成10分,删除其中的任意三分,尝试还原文件
// 这边需要注意的一点是文件的拆分后的顺序和还原的顺序是相关的,顺序错误是无法还原的

// 其中Encoder接口有以下几个关键的函数。
// Verify(shards [][]byte) (bool, error)。每个分片都是[]byte类型,分片集合就是[][]byte类型,传入所有分片,如果有任意的分片数据错误,就返回false。
// Split(data []byte) ([][]byte, error)。将原始数据按照规定的分片数进行切分。注意:数据没有经过拷贝,所以修改分片也就是修改原数据。
// Reconstruct(shards [][]byte) error。  这个函数会根据shards中完整的分片,重建其他损坏的分片。
// Join(dst io.Writer, shards [][]byte, outSize int) error。将shards合并成完整的原始数据并写入dst这个Writer中。


package main

import (
    "flag"
    "fmt"
    "io/ioutil"
    "os"
    "strings"

    "github.com/klauspost/reedsolomon"
    "github.com/qtj/gosdk/file"
)

定义和初始化验证一些参数

var (
    srcFile      string // 原始文件
    dstDir       string // 目标目录
    recoverName  string // 还原后的文件名称
    dataShards   int    // 数据分片
    parityShards int    // 校验分片
    oper         string // 操作动作
)

func init() {
    flag.StringVar(&srcFile, "srcFile", "qtjErasureCode.exe", "原始文件名称")
    flag.StringVar(&dstDir, "dstDir", "dstDir", "目标目录")
    flag.StringVar(&recoverName, "recoverName", "recoverName", "还原后的文件名称")
    flag.StringVar(&oper, "oper", "", "split和recover二选一,split会将一个文件拆分成类似10个数据文件和3和校验文件,recover的时候可以删除目标目录下的三个文件做还原即可")
    flag.IntVar(&dataShards, "dataShards", 10, "数据分片个数")
    flag.IntVar(&parityShards, "parityShards", 3, "校验分片个数")
    flag.Parse()
}

主调用

// qtjErasureCode.exe -oper=split先将文件拆分
// 删除当前目录下的子文件夹dstDir里面的任意三个文件
// qtjErasureCode.exe -oper=recover -recoverName="recover.exe" 将文件还原
// 最后比对md5发现是一致的
func main() {
    if !strings.Contains(dstDir, "/") && !strings.Contains(dstDir, "\\") {
        dstDir = file.GetCurDir() + dstDir + "/"
    }
    if oper == "split" {
        file.RemoveDirTree(dstDir)
        file.CreateDirTree(dstDir)
        if err := splitFile(); err != nil {
            fmt.Println(err)
            return
        }
    } else if oper == "recover" {
        if err := recoverFile(); err != nil {
            fmt.Println(err)
            return
        }
    } else {
        fmt.Println("错误的操作动作参数,oper必须为split或者recover")
        return
    }
}

还原文件验证

func recoverFile() error {
    if recoverName == "" {
        return fmt.Errorf("还原后的文件名称%s不能为空", recoverName)
    }

    // 数据分10片和校验3片
    enc, err := reedsolomon.New(dataShards, parityShards)
    if err != nil {
        return fmt.Errorf("创建数据分片和校验分片失败,%s", err.Error())
    }

    shards := make([][]byte, dataShards+parityShards)
    for i := range shards {
        splitName := fmt.Sprintf("%ssplit%010d", dstDir, i)
        // 不管文件是否存在,需要保留原先的顺序
        if shards[i], err = ioutil.ReadFile(splitName); err != nil {
            fmt.Printf("读取文件[%s]失败,%s\n", splitName, err.Error())
        }
        fmt.Println(splitName)
    }

    ok, err := enc.Verify(shards)
    if ok {
        fmt.Println("非常好,数据块和校验块都完整")
    } else {
        if err = enc.Reconstruct(shards); err != nil {
            return fmt.Errorf("重建其他损坏的分片失败,%s", err.Error())
        }

        if ok, err = enc.Verify(shards); err != nil {
            return fmt.Errorf("数据块校验失败2,%s", err.Error())
        }
        if !ok {
            return fmt.Errorf("重建其他损坏的分片后数据还是不完整,文件损坏")
        }

    }
    f, err := os.Create(recoverName)
    if err != nil {
        return fmt.Errorf("创建还原文件[%s]失败,%s", recoverName, err.Error())
    }
    // 这部分的大小决定了还原后的大小和原先的是不是一致的,不然使用md5比对或者大小都是不一样的
    // 实际生产需要一开始就拆分文件时候就记录总的大小
    //if err = enc.Join(f, shards, len(shards[0])*dataShards); err != nil {
    _, ln, err := file.GetFileLenAndMd5(srcFile)
    if err != nil {
        return fmt.Errorf("计算原始文件[%s]大小失败,%s", srcFile, err.Error())
    }
    if err = enc.Join(f, shards, int(ln)); err != nil {
        return fmt.Errorf("写还原文件[%s]失败,%s", recoverFile(), err.Error())
    }
    return nil
}

分隔文件处理

func splitFile() error {
    // 数据分10片和校验3片
    enc, err := reedsolomon.New(dataShards, parityShards)
    if err != nil {
        return fmt.Errorf("创建数据分片和校验分片失败,%s", err.Error())
    }

    bigfile, err := ioutil.ReadFile(srcFile)
    if err != nil {
        return fmt.Errorf("读取原始文件[%s]失败,%s", srcFile, err.Error())
    }

    // 将原始数据按照规定的分片数进行切分
    shards, err := enc.Split(bigfile)
    if err != nil {
        return fmt.Errorf("针对原始文件[%s]拆分成数据[%d]块,校验[%d]块失败,%s", srcFile, dataShards, parityShards, err.Error())
    }

    // 编码校验块
    if err = enc.Encode(shards); err != nil {
        return fmt.Errorf("编码校验块失败,%s", err.Error())
    }
    for i := range shards {
        splitName := fmt.Sprintf("%ssplit%010d", dstDir, i)
        fmt.Println(splitName)
        if err = file.SaveFile(shards[i], splitName); err != nil {
            return fmt.Errorf("原始文件[%s]拆分文件[%s]写失败,%s", srcFile, splitName, err.Error())
        }
    }

    return nil
}
目录
相关文章
golang操作文件
1、读取文件信息: /* 读取文件信息 */ func readFile(path string) string { fi, err := os.Open(path) if err != nil { panic(err) } defer fi.
1375 0
|
16天前
|
Go
go语言中的数据类型
go语言中的数据类型
13 0
|
21天前
|
Go 开发者
掌握Go语言:Go语言结构体,精准封装数据,高效管理实体对象(22)
掌握Go语言:Go语言结构体,精准封装数据,高效管理实体对象(22)
|
21天前
|
安全 Go
掌握Go语言:Go语言通道,并发编程的利器与应用实例(20)
掌握Go语言:Go语言通道,并发编程的利器与应用实例(20)
|
21天前
|
存储 缓存 安全
掌握Go语言:Go语言中的字典魔法,高效数据检索与应用实例解析(18)
掌握Go语言:Go语言中的字典魔法,高效数据检索与应用实例解析(18)
|
3天前
|
数据采集 存储 Go
使用Go语言和chromedp库下载Instagram图片:简易指南
Go语言爬虫示例使用chromedp库下载Instagram图片,关键步骤包括设置代理IP、创建带代理的浏览器上下文及执行任务,如导航至用户页面、截图并存储图片。代码中新增`analyzeAndStoreImage`函数对图片进行分析和分类后存储。注意Instagram的反爬策略可能需要代码适时调整。
使用Go语言和chromedp库下载Instagram图片:简易指南
|
21天前
|
存储 安全 Go
掌握Go语言:Go语言类型转换,无缝处理数据类型、接口和自定义类型的转换细节解析(29)
掌握Go语言:Go语言类型转换,无缝处理数据类型、接口和自定义类型的转换细节解析(29)
|
1天前
|
程序员 Go API
【Go语言快速上手(二)】 分支与循环&函数讲解
【Go语言快速上手(二)】 分支与循环&函数讲解
|
1天前
|
Go
Golang深入浅出之-Go语言基础语法:变量声明与赋值
【4月更文挑战第20天】本文介绍了Go语言中变量声明与赋值的基础知识,包括使用`var`关键字和简短声明`:=`的方式,以及多变量声明与赋值。强调了变量作用域、遮蔽、初始化与零值的重要性,并提醒读者注意类型推断时的一致性。了解这些概念有助于避免常见错误,提高编程技能和面试表现。
16 0