登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go Badger 怎么使用前缀迭代器读取一组记录

来源:17golang原创

时间:2026-10-13 16:57:18 345浏览 收藏

Go Badger 读取同一业务前缀下的一组记录,可以在只读事务中创建迭代器,用 Seek(prefix) 定位起点,再以 ValidForPrefix(prefix) 约束循环。需要把记录带出事务时,每条键和值分别用 KeyCopy(nil)、ValueCopy(nil) 复制,并在事务回调内关闭迭代器。下面用租户订单键说明这几个约束为什么要一起出现。

官方地址:https://github.com/dgraph-io/badger

订单列表为什么混入其他记录

假设业务把订单保存为 tenant:42:order:001、tenant:42:order:002,同一数据库还保存用户资料和其他租户的订单。页面只想列出租户42的订单,却使用 tenant:42 作为扫描前缀。这不是数据库识别错了租户,而是查询给出的字节范围比业务范围更宽。

前缀匹配不理解“租户编号”这个概念。tenant:420:order:001 同样以 tenant:42 开头;tenant:42:profile 也会匹配。把完整业务段及尾部分隔符纳入前缀,使用 tenant:42:order:,才能表达这个例子的订单集合。

先检查写入端与读取端是否使用同一套键格式,而不是急着调整预取参数。分隔符是键设计的一部分;若字段允许包含冒号,还需要约定编码或转义,不能只靠字符串拼接保证业务隔离。

Seek与循环条件分别约束什么

正向迭代时,Seek负责寻找目标键或其后的第一个键,ValidForPrefix负责检查当前位置是否仍属于目标集合。只调用Seek却用不带业务范围的Valid作为结束条件,尤其在没有设置Prefix选项时,可能继续读到后面的其他键。

IteratorOptions.Prefix 可以限定迭代范围;这里仍保留显式的ValidForPrefix,让循环本身清楚表达查询条件。默认选项作为起点,不开启Reverse或AllVersions,避免把逆序定位、历史版本读取混入这个基础例子。相关语义见 Badger迭代器接口 与 迭代选项。

订单业务前缀与迭代范围的静态关系
原创静态结构图:业务前缀、定位起点与循环边界的关系,不是运行截图。

在只读事务内返回独立记录

以下函数放在业务包中,接收调用方已经打开的数据库。它只做前缀查询,不负责打开、关闭数据库,也不解析订单内容。空前缀被主动拒绝,避免调用方漏传参数后变成无范围扫描。

package orders

import (
    "fmt"

    "github.com/dgraph-io/badger/v4"
)

// Record拥有独立的键和值副本。
type Record struct {
    Key   []byte
    Value []byte
}

// ReadPrefix只读取正向前缀集合,不返回失败时的部分结果。
func ReadPrefix(db *badger.DB, prefix []byte) ([]Record, error) {
    if db == nil {
        return nil, fmt.Errorf("数据库不能为空")
    }
    if len(prefix) == 0 {
        return nil, fmt.Errorf("查询前缀不能为空")
    }
    // 本次调用固定前缀;调用方仍不应并发修改输入。
    p := append([]byte(nil), prefix...)
    rows := make([]Record, 0)

    err := db.View(func(txn *badger.Txn) error {
        opts := badger.DefaultIteratorOptions
        opts.Prefix = p
        opts.PrefetchValues = true
        it := txn.NewIterator(opts)
        // 在事务回调结束前释放迭代器。
        defer it.Close()

        for it.Seek(p); it.ValidForPrefix(p); it.Next() {
            item := it.Item()
            key := item.KeyCopy(nil)
            value, err := item.ValueCopy(nil)
            if err != nil {
                return fmt.Errorf("读取键 %q 的值: %w", key, err)
            }
            // 每条记录使用独立切片,避免共享复用缓冲区。
            rows = append(rows, Record{Key: key, Value: value})
        }
        return nil
    })
    if err != nil {
        return nil, err
    }
    return rows, nil
}

这里刻意没有把Item指针存进结果。迭代器会复用Item,当前项不能被当作永久记录保存;Key、Value也有有效期限制。复制发生在当前迭代和事务内部,结果切片才不依赖随后Next或事务结束时的借用内存。接口说明见 Item文档。

另一个容易漏掉的细节是复用目标缓冲区:如果不断把同一个buf交给ValueCopy,再把返回切片直接追加到rows,多条记录可能共享底层数组。一次性收集全部记录时,传nil换取每条独立副本更容易维护。它会增加内存占用,不是零拷贝方案。

事务内Item和事务外Record的所有权关系
原创静态结构图:借用对象与独立结果副本的边界,不代表实测日志。

用相邻键检查结果边界

给读取函数传入 []byte("tenant:42:order:")。下面是供读者准备样本时使用的预期表,不是已经执行过的输出:

样本键应否返回原因
tenant:42:order:001是完整匹配订单前缀
tenant:42:order:002是同一集合中的另一个键
tenant:42:profile否业务段不同
tenant:420:order:001否租户段不同
tenant:43:order:001否相邻但不属于目标前缀

准备这些已提交的样本后,逐一比对结果键,再在函数返回后读取结果值,确认调用方拿到的是Record而不是Item。没有匹配记录时,函数应返回长度为零的切片和nil错误;读取某项失败时则应收到错误,不能把已收集的一部分当成完整订单列表。业务若希望允许部分结果,需要另行设计明确的完成标志,不能悄悄改变本函数契约。

键顺序是字节序,不是自然数字顺序。若订单号不补零,编号10可能排在编号2前面。这种顺序问题要在键编码或结果排序中解决,不能让Seek承担业务排序。

只读键和大结果集如何取舍

只做键清单、存在性判断等任务时,可把PrefetchValues设为false,并移除ValueCopy及值字段收集。这个开关表示不预取值,不表示以后禁止读取值;如果循环又调用ValueCopy,仍然会读取所需值。是否预取应取决于这次查询是否确实需要值,而不是一律关闭。

上述函数会把整个前缀集合保存在内存中。订单数量或值体积很大时,更合适的是带上限的查询或事务内逐条消费;逐条消费的回调也不能把借用切片直接留到事务外。上限查询只返回部分集合,应在接口中说明限制;它不是完整分页协议,更不能仅靠上一页长度判断数据再无变化。

读取期间不要关闭数据库,不要让多个goroutine共享同一个迭代器。数据库生命周期应由外层统一管理,而这里的defer只管理本次迭代器。

常见疑问与收尾检查

没有一条键等于prefix,Seek还有效吗?

有效。订单键通常比prefix更长,正向Seek能够找到其后的候选键;候选是否匹配,由循环的前缀条件判断。

没有匹配结果为什么不返回ErrKeyNotFound?

这是集合查询而不是单键Get。本函数把空集合视为正常结果;调用方可以依据len(rows)显示“暂无订单”。

把Reverse改成true就能倒着读取吗?

不能直接照搬这个定位方式。逆序Seek的定位方向不同,前缀本身不是该范围的最后一个键;需要单独设计上界和逆序起点。本文函数保持默认正向。

排查前缀扫描时,依次看键编码、定位起点、循环范围和结果所有权。完整业务前缀控制“读哪些”,KeyCopy与ValueCopy控制“如何保留”,迭代器关闭控制“何时释放”。把这三类约束分别写在对应位置,代码比依赖隐含约定更清楚。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>