up
This commit is contained in:
+115
-9
@@ -15,6 +15,8 @@ import (
|
||||
"log" // 日志输出
|
||||
"os" // 操作系统功能(创建目录等)
|
||||
"path/filepath" // 路径拼接
|
||||
"sync" // 互斥锁(保护写缓冲)
|
||||
"time" // bbolt 超时配置和写缓冲定时器
|
||||
|
||||
"github.com/andybalholm/brotli" // Brotli 无损压缩库(用于压缩存储数据)
|
||||
bolt "go.etcd.io/bbolt" // BoltDB,纯 Go 嵌入式 KV 数据库
|
||||
@@ -44,10 +46,23 @@ var (
|
||||
bucketPriority = []byte("priority") // 优先爬取 URL bucket
|
||||
)
|
||||
|
||||
// writeOp 表示一个待写入的操作。
|
||||
type writeOp struct {
|
||||
opType int // 0 = set snippet, 1 = set site info
|
||||
key string // URL 或 host
|
||||
data []byte // marshalCompress 后的数据
|
||||
}
|
||||
|
||||
// DB 封装一个 bbolt 数据库,提供类型化的存取接口。
|
||||
// bbolt 内部已实现并发安全,无需额外加锁。
|
||||
type DB struct {
|
||||
db *bolt.DB // 底层 bbolt 数据库句柄
|
||||
|
||||
// 异步写缓冲:SetSnippet/SetSiteInfo 先写到内存,定期批量刷入 bbolt。
|
||||
writeBuf map[string]*writeOp // key → 待写入的操作
|
||||
writeBufMu sync.Mutex
|
||||
writeTicker *time.Ticker
|
||||
writeDone chan struct{}
|
||||
}
|
||||
|
||||
// Open 在指定目录路径下创建或打开 bbolt 数据库文件。
|
||||
@@ -60,7 +75,13 @@ func Open(dir string) (*DB, error) {
|
||||
// 拼接数据库文件路径:dir/sese.db
|
||||
path := filepath.Join(dir, "sese.db")
|
||||
// 打开/创建数据库文件,文件权限 0600(仅所有者可读写)
|
||||
db, err := bolt.Open(path, 0o600, nil)
|
||||
// NoSync: true — 不在每次写事务后 fsync,交由 OS 决定刷盘时机。
|
||||
// 在高并发写入场景下大幅减少磁盘 I/O 阻塞,代价是极端断电可能丢失最近几秒数据(可接受)。
|
||||
db, err := bolt.Open(path, 0o600, &bolt.Options{
|
||||
NoSync: true,
|
||||
Timeout: 5 * time.Second,
|
||||
PageSize: 4096,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("storage.Open bolt: %w", err)
|
||||
}
|
||||
@@ -79,8 +100,80 @@ func Open(dir string) (*DB, error) {
|
||||
return &DB{db: db}, nil
|
||||
}
|
||||
|
||||
// StartWriteFlusher 启动后台写缓冲定时刷盘 goroutine。
|
||||
func (d *DB) StartWriteFlusher() {
|
||||
d.writeBuf = make(map[string]*writeOp)
|
||||
d.writeTicker = time.NewTicker(2 * time.Second) // 每 2 秒刷一次
|
||||
d.writeDone = make(chan struct{})
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-d.writeTicker.C:
|
||||
d.flushWriteBuf()
|
||||
case <-d.writeDone:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// flushWriteBuf 将写缓冲中的所有待写入操作批量刷入 bbolt。
|
||||
func (d *DB) flushWriteBuf() {
|
||||
d.writeBufMu.Lock()
|
||||
if len(d.writeBuf) == 0 {
|
||||
d.writeBufMu.Unlock()
|
||||
return
|
||||
}
|
||||
// 快照并清空缓冲
|
||||
snapshot := d.writeBuf
|
||||
d.writeBuf = make(map[string]*writeOp)
|
||||
d.writeBufMu.Unlock()
|
||||
|
||||
// 预先按 bucket 分组
|
||||
snippets := make(map[string][]byte)
|
||||
siteInfos := make(map[string][]byte)
|
||||
for key, op := range snapshot {
|
||||
if op.opType == 0 {
|
||||
snippets[key] = op.data
|
||||
} else {
|
||||
siteInfos[key] = op.data
|
||||
}
|
||||
}
|
||||
|
||||
// 单个事务批量写入
|
||||
if err := d.db.Update(func(tx *bolt.Tx) error {
|
||||
if len(snippets) > 0 {
|
||||
b := tx.Bucket(bucketGate)
|
||||
for k, v := range snippets {
|
||||
if err := b.Put([]byte(k), v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(siteInfos) > 0 {
|
||||
b := tx.Bucket(bucketSiteGate)
|
||||
for k, v := range siteInfos {
|
||||
if err := b.Put([]byte(k), v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Printf("[storage] flushWriteBuf error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭底层 bbolt 数据库连接。
|
||||
// 先刷完写缓冲,再关闭 ticker,最后关闭数据库。
|
||||
func (d *DB) Close() error {
|
||||
if d.writeTicker != nil {
|
||||
d.writeTicker.Stop()
|
||||
}
|
||||
d.flushWriteBuf() // 最后刷一次,确保数据不丢失
|
||||
if d.writeDone != nil {
|
||||
close(d.writeDone)
|
||||
}
|
||||
return d.db.Close()
|
||||
}
|
||||
|
||||
@@ -279,15 +372,22 @@ func (d *DB) GetSnippet(url string) (*SnippetEntry, error) {
|
||||
return &entry, nil
|
||||
}
|
||||
|
||||
// SetSnippet 将某 URL 的摘要信息写入缓存(覆盖已有数据)。
|
||||
// SetSnippet 将某 URL 的摘要信息写入写缓冲(异步批量刷入磁盘)。
|
||||
func (d *DB) SetSnippet(url string, entry *SnippetEntry) error {
|
||||
data, err := marshalCompress(entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(bucketGate).Put([]byte(url), data)
|
||||
})
|
||||
d.writeBufMu.Lock()
|
||||
d.writeBuf["snippet:"+url] = &writeOp{opType: 0, key: url, data: data}
|
||||
// 缓冲过大时同步刷一次,防止内存膨胀
|
||||
if len(d.writeBuf) >= 5000 {
|
||||
d.writeBufMu.Unlock()
|
||||
d.flushWriteBuf()
|
||||
return nil
|
||||
}
|
||||
d.writeBufMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- 网站之门(SiteGate):网站元信息相关方法 ----
|
||||
@@ -333,15 +433,21 @@ func (d *DB) GetSiteInfo(host string) (*SiteInfo, error) {
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// SetSiteInfo 将某主机名的网站元信息写入存储(覆盖已有数据)。
|
||||
// SetSiteInfo 将网站元信息写入写缓冲(异步批量刷入磁盘)。
|
||||
func (d *DB) SetSiteInfo(host string, info *SiteInfo) error {
|
||||
data, err := marshalCompress(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return d.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket(bucketSiteGate).Put([]byte(host), data)
|
||||
})
|
||||
d.writeBufMu.Lock()
|
||||
d.writeBuf["site:"+host] = &writeOp{opType: 1, key: host, data: data}
|
||||
if len(d.writeBuf) >= 5000 {
|
||||
d.writeBufMu.Unlock()
|
||||
d.flushWriteBuf()
|
||||
return nil
|
||||
}
|
||||
d.writeBufMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForEachSite 遍历所有网站元信息条目,对每个条目调用 fn。
|
||||
|
||||
Reference in New Issue
Block a user