Initial commit: Pipeline Database
This commit is contained in:
31
.gitattributes
vendored
Normal file
31
.gitattributes
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
# 文本文件统一使用 LF 换行符
|
||||
* text=auto eol=lf
|
||||
|
||||
# Go 源代码文件
|
||||
*.go text eol=lf
|
||||
go.mod text eol=lf
|
||||
go.sum text eol=lf
|
||||
|
||||
# 文档文件
|
||||
*.md text eol=lf
|
||||
*.txt text eol=lf
|
||||
README* text eol=lf
|
||||
|
||||
# Git 配置文件
|
||||
.gitignore text eol=lf
|
||||
.gitattributes text eol=lf
|
||||
|
||||
# 数据库文件(二进制)
|
||||
*.db binary
|
||||
*.database binary
|
||||
|
||||
# Go 编译产物(二进制)
|
||||
*.exe binary
|
||||
*.dll binary
|
||||
*.so binary
|
||||
*.dylib binary
|
||||
|
||||
# 语言检测
|
||||
*.go linguist-language=Go
|
||||
*.md linguist-documentation
|
||||
examples/ linguist-documentation
|
||||
237
.gitea/workflows/benchmark.yml
Normal file
237
.gitea/workflows/benchmark.yml
Normal file
@@ -0,0 +1,237 @@
|
||||
name: 性能基准测试
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
benchtime:
|
||||
description: '基准测试运行时间'
|
||||
required: false
|
||||
default: '10s'
|
||||
count:
|
||||
description: '基准测试运行次数'
|
||||
required: false
|
||||
default: '5'
|
||||
|
||||
jobs:
|
||||
benchmark:
|
||||
name: 完整性能基准测试
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
|
||||
- name: 系统信息
|
||||
run: |
|
||||
echo "=== 系统信息 ==="
|
||||
uname -a
|
||||
cat /proc/cpuinfo | grep "model name" | head -1
|
||||
cat /proc/meminfo | grep MemTotal
|
||||
df -h /
|
||||
|
||||
- name: Go 环境信息
|
||||
run: |
|
||||
echo "=== Go 环境信息 ==="
|
||||
go version
|
||||
go env
|
||||
|
||||
- name: 预热系统
|
||||
run: |
|
||||
echo "=== 系统预热 ==="
|
||||
go test -bench=BenchmarkPipelineDBOpen -benchtime=100ms -run=^$ ./...
|
||||
|
||||
- name: 数据库核心功能基准测试
|
||||
run: |
|
||||
echo "=== 数据库核心功能基准测试 ==="
|
||||
go test -bench=BenchmarkPipelineDB -benchmem -count=${{ github.event.inputs.count || '5' }} -benchtime=${{ github.event.inputs.benchtime || '2s' }} -run=^$ ./... | tee core_bench.txt
|
||||
|
||||
- name: 并发性能基准测试
|
||||
run: |
|
||||
echo "=== 并发性能基准测试 ==="
|
||||
go test -bench=BenchmarkConcurrent -benchmem -count=${{ github.event.inputs.count || '5' }} -benchtime=${{ github.event.inputs.benchtime || '2s' }} -run=^$ ./... | tee concurrent_bench.txt
|
||||
|
||||
- name: 存储引擎基准测试
|
||||
run: |
|
||||
echo "=== 存储引擎基准测试 ==="
|
||||
go test -bench=BenchmarkStorage -benchmem -count=${{ github.event.inputs.count || '5' }} -benchtime=${{ github.event.inputs.benchtime || '2s' }} -run=^$ ./... | tee storage_bench.txt
|
||||
|
||||
- name: 缓存系统基准测试
|
||||
run: |
|
||||
echo "=== 缓存系统基准测试 ==="
|
||||
go test -bench=BenchmarkCache -benchmem -count=${{ github.event.inputs.count || '5' }} -benchtime=${{ github.event.inputs.benchtime || '2s' }} -run=^$ ./... | tee cache_bench.txt
|
||||
|
||||
- name: 索引管理基准测试
|
||||
run: |
|
||||
echo "=== 索引管理基准测试 ==="
|
||||
go test -bench=BenchmarkIndex -benchmem -count=${{ github.event.inputs.count || '5' }} -benchtime=${{ github.event.inputs.benchtime || '2s' }} -run=^$ ./... | tee index_bench.txt
|
||||
|
||||
- name: 组管理基准测试
|
||||
run: |
|
||||
echo "=== 组管理基准测试 ==="
|
||||
go test -bench=BenchmarkGroup -benchmem -count=${{ github.event.inputs.count || '5' }} -benchtime=${{ github.event.inputs.benchtime || '2s' }} -run=^$ ./... | tee group_bench.txt
|
||||
|
||||
- name: Handler 处理器基准测试
|
||||
run: |
|
||||
echo "=== Handler 处理器基准测试 ==="
|
||||
go test -bench=BenchmarkHandler -benchmem -count=${{ github.event.inputs.count || '5' }} -benchtime=${{ github.event.inputs.benchtime || '2s' }} -run=^$ ./... | tee handler_bench.txt
|
||||
|
||||
- name: 高并发压力测试
|
||||
run: |
|
||||
echo "=== 高并发压力测试 ==="
|
||||
go test -bench=BenchmarkHighConcurrency -benchmem -count=3 -benchtime=5s -run=^$ ./... | tee high_concurrency_bench.txt
|
||||
|
||||
- name: 内存分析
|
||||
run: |
|
||||
echo "=== 内存分析 ==="
|
||||
go test -bench=BenchmarkConcurrentAcceptData -benchmem -memprofile=mem.prof -run=^$ ./...
|
||||
go tool pprof -text mem.prof | head -20
|
||||
|
||||
- name: CPU 分析
|
||||
run: |
|
||||
echo "=== CPU 分析 ==="
|
||||
go test -bench=BenchmarkConcurrentAcceptData -cpuprofile=cpu.prof -run=^$ ./...
|
||||
go tool pprof -text cpu.prof | head -20
|
||||
|
||||
- name: 生成性能报告
|
||||
run: |
|
||||
echo "=== 性能基准测试报告 ===" > benchmark_report.md
|
||||
echo "**测试时间:** $(date)" >> benchmark_report.md
|
||||
echo "**Go 版本:** $(go version)" >> benchmark_report.md
|
||||
echo "**系统信息:** $(uname -a)" >> benchmark_report.md
|
||||
echo "" >> benchmark_report.md
|
||||
|
||||
echo "## 📊 核心功能性能" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
cat core_bench.txt | grep "Benchmark" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
echo "" >> benchmark_report.md
|
||||
|
||||
echo "## 🚀 并发性能" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
cat concurrent_bench.txt | grep "Benchmark" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
echo "" >> benchmark_report.md
|
||||
|
||||
echo "## 💾 存储引擎性能" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
cat storage_bench.txt | grep "Benchmark" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
echo "" >> benchmark_report.md
|
||||
|
||||
echo "## ⚡ 缓存系统性能" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
cat cache_bench.txt | grep "Benchmark" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
echo "" >> benchmark_report.md
|
||||
|
||||
echo "## 🔍 索引管理性能" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
cat index_bench.txt | grep "Benchmark" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
echo "" >> benchmark_report.md
|
||||
|
||||
echo "## 📁 组管理性能" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
cat group_bench.txt | grep "Benchmark" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
echo "" >> benchmark_report.md
|
||||
|
||||
echo "## 🔧 Handler 处理器性能" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
cat handler_bench.txt | grep "Benchmark" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
echo "" >> benchmark_report.md
|
||||
|
||||
echo "## 💥 高并发压力测试" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
cat high_concurrency_bench.txt | grep "Benchmark" >> benchmark_report.md
|
||||
echo "\`\`\`" >> benchmark_report.md
|
||||
|
||||
cat benchmark_report.md
|
||||
|
||||
- name: 性能趋势分析
|
||||
run: |
|
||||
echo "=== 性能趋势分析 ==="
|
||||
|
||||
# 提取关键性能指标
|
||||
ACCEPT_DATA_QPS=$(cat core_bench.txt | grep "BenchmarkAcceptData" | awk '{print $3}' | tail -1)
|
||||
QUERY_QPS=$(cat concurrent_bench.txt | grep "BenchmarkConcurrentQuery" | awk '{print $3}' | tail -1)
|
||||
CACHE_OPS=$(cat cache_bench.txt | grep "BenchmarkCacheOperations" | awk '{print $3}' | tail -1)
|
||||
|
||||
echo "关键性能指标:"
|
||||
echo "- AcceptData QPS: $ACCEPT_DATA_QPS"
|
||||
echo "- 并发查询 QPS: $QUERY_QPS"
|
||||
echo "- 缓存操作 QPS: $CACHE_OPS"
|
||||
|
||||
# 性能阈值检查
|
||||
if [ ! -z "$ACCEPT_DATA_QPS" ] && [ "$ACCEPT_DATA_QPS" -lt 1000 ]; then
|
||||
echo "⚠️ AcceptData 性能低于预期阈值 (1000 ops/sec)"
|
||||
fi
|
||||
|
||||
if [ ! -z "$CACHE_OPS" ] && [ "$CACHE_OPS" -lt 5000000 ]; then
|
||||
echo "⚠️ 缓存操作性能低于预期阈值 (5M ops/sec)"
|
||||
fi
|
||||
|
||||
- name: 上传基准测试结果
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: benchmark-results-${{ github.run_number }}
|
||||
path: |
|
||||
*_bench.txt
|
||||
benchmark_report.md
|
||||
*.prof
|
||||
|
||||
- name: 上传性能分析文件
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: performance-profiles-${{ github.run_number }}
|
||||
path: |
|
||||
mem.prof
|
||||
cpu.prof
|
||||
|
||||
stress_test:
|
||||
name: 压力测试
|
||||
runs-on: ubuntu-latest
|
||||
needs: benchmark
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
|
||||
- name: 长时间压力测试
|
||||
timeout-minutes: 30
|
||||
run: |
|
||||
echo "=== 长时间压力测试 (20分钟) ==="
|
||||
timeout 1200s go test -bench=BenchmarkHighConcurrencyWrite -benchtime=20m -run=^$ ./... || true
|
||||
|
||||
- name: 内存泄漏检测
|
||||
run: |
|
||||
echo "=== 内存泄漏检测 ==="
|
||||
go test -bench=BenchmarkConcurrentAcceptData -benchtime=30s -memprofile=leak_test.prof -run=^$ ./...
|
||||
go tool pprof -alloc_space -text leak_test.prof | head -20
|
||||
|
||||
- name: 竞态条件检测
|
||||
run: |
|
||||
echo "=== 竞态条件检测 ==="
|
||||
go test -race -bench=BenchmarkConcurrent -benchtime=10s -run=^$ ./...
|
||||
|
||||
- name: 上传压力测试结果
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: stress-test-results-${{ github.run_number }}
|
||||
path: |
|
||||
leak_test.prof
|
||||
272
.gitea/workflows/ci.yml
Normal file
272
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,272 @@
|
||||
name: CI Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, develop ]
|
||||
pull_request:
|
||||
branches: [ main, develop ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: 测试和基准测试
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.24.x, 1.25.x]
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: 验证 Go 模块
|
||||
run: |
|
||||
go mod tidy
|
||||
git diff --exit-code go.mod go.sum
|
||||
|
||||
- name: 下载依赖
|
||||
run: go mod download
|
||||
|
||||
- name: 代码格式检查
|
||||
run: |
|
||||
if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then
|
||||
echo "以下文件需要格式化:"
|
||||
gofmt -s -l .
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 静态代码检查
|
||||
run: |
|
||||
go vet ./...
|
||||
|
||||
- name: 运行单元测试
|
||||
run: |
|
||||
go test -v -race -coverprofile=coverage.out ./...
|
||||
|
||||
- name: 生成测试覆盖率报告
|
||||
run: |
|
||||
go tool cover -html=coverage.out -o coverage.html
|
||||
go tool cover -func=coverage.out
|
||||
|
||||
- name: 运行基准测试
|
||||
run: |
|
||||
echo "=== 基础性能基准测试 ==="
|
||||
go test -bench=BenchmarkPipelineDB -benchmem -benchtime=1s -run=^$ ./...
|
||||
|
||||
echo "=== 并发性能基准测试 ==="
|
||||
go test -bench=BenchmarkConcurrent -benchmem -benchtime=500ms -run=^$ ./...
|
||||
|
||||
echo "=== 存储引擎基准测试 ==="
|
||||
go test -bench=BenchmarkStorage -benchmem -benchtime=500ms -run=^$ ./...
|
||||
|
||||
echo "=== 缓存系统基准测试 ==="
|
||||
go test -bench=BenchmarkCache -benchmem -benchtime=500ms -run=^$ ./...
|
||||
|
||||
echo "=== Handler 基准测试 ==="
|
||||
go test -bench=BenchmarkHandler -benchmem -benchtime=500ms -run=^$ ./...
|
||||
|
||||
- name: 运行示例程序
|
||||
run: |
|
||||
echo "=== 测试基础使用示例 ==="
|
||||
cd examples/basic-usage && timeout 10s go run main.go || true
|
||||
|
||||
echo "=== 测试组管理示例 ==="
|
||||
cd ../group-management && timeout 10s go run main.go || true
|
||||
|
||||
echo "=== 测试外部处理器示例 ==="
|
||||
cd ../external-handler && timeout 10s go run main.go || true
|
||||
|
||||
- name: 上传测试覆盖率
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: coverage-report-go${{ matrix.go-version }}
|
||||
path: |
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
- name: 检查测试覆盖率
|
||||
run: |
|
||||
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
|
||||
echo "测试覆盖率: ${COVERAGE}%"
|
||||
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
|
||||
echo "⚠️ 测试覆盖率低于 80%"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ 测试覆盖率达标"
|
||||
|
||||
build:
|
||||
name: 构建验证
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
goos: [linux, darwin, windows]
|
||||
goarch: [amd64, arm64]
|
||||
exclude:
|
||||
- goos: windows
|
||||
goarch: arm64
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
|
||||
- name: 构建二进制文件
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: |
|
||||
echo "构建 ${{ matrix.goos }}/${{ matrix.goarch }}"
|
||||
go build -v -ldflags="-s -w" ./...
|
||||
|
||||
- name: 构建示例程序
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
run: |
|
||||
cd examples/basic-usage && go build -v -ldflags="-s -w" .
|
||||
cd ../group-management && go build -v -ldflags="-s -w" .
|
||||
cd ../external-handler && go build -v -ldflags="-s -w" .
|
||||
cd ../data-analytics && go build -v -ldflags="-s -w" .
|
||||
cd ../concurrent-processing && go build -v -ldflags="-s -w" .
|
||||
cd ../high-concurrency && go build -v -ldflags="-s -w" .
|
||||
|
||||
performance:
|
||||
name: 性能回归测试
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
|
||||
- name: 运行性能基准测试
|
||||
run: |
|
||||
echo "=== 性能基准测试 - 当前分支 ==="
|
||||
go test -bench=. -benchmem -count=3 -benchtime=2s -run=^$ ./... > current_bench.txt
|
||||
|
||||
- name: 检出主分支
|
||||
run: |
|
||||
git fetch origin main
|
||||
git checkout origin/main
|
||||
|
||||
- name: 运行性能基准测试 - 主分支
|
||||
run: |
|
||||
echo "=== 性能基准测试 - 主分支 ==="
|
||||
go test -bench=. -benchmem -count=3 -benchtime=2s -run=^$ ./... > main_bench.txt || true
|
||||
|
||||
- name: 性能对比分析
|
||||
run: |
|
||||
echo "=== 性能对比报告 ==="
|
||||
echo "当前分支性能:"
|
||||
cat current_bench.txt | grep "Benchmark" | head -10
|
||||
echo ""
|
||||
echo "主分支性能:"
|
||||
cat main_bench.txt | grep "Benchmark" | head -10 || echo "主分支基准测试数据不可用"
|
||||
|
||||
- name: 上传性能报告
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: performance-report
|
||||
path: |
|
||||
current_bench.txt
|
||||
main_bench.txt
|
||||
|
||||
security:
|
||||
name: 安全扫描
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
|
||||
- name: 安装 gosec
|
||||
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
|
||||
- name: 运行安全扫描
|
||||
run: |
|
||||
gosec -fmt json -out gosec-report.json ./...
|
||||
gosec ./...
|
||||
|
||||
- name: 上传安全报告
|
||||
uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: security-report
|
||||
path: gosec-report.json
|
||||
|
||||
quality:
|
||||
name: 代码质量检查
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
|
||||
- name: 安装代码质量工具
|
||||
run: |
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
- name: 运行 staticcheck
|
||||
run: staticcheck ./...
|
||||
|
||||
- name: 运行 golangci-lint
|
||||
run: golangci-lint run ./...
|
||||
|
||||
- name: 检查循环复杂度
|
||||
run: |
|
||||
go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
|
||||
gocyclo -over 15 .
|
||||
|
||||
- name: 检查代码重复
|
||||
run: |
|
||||
go install github.com/mibk/dupl@latest
|
||||
dupl -threshold 100 .
|
||||
|
||||
notification:
|
||||
name: 通知
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test, build, security, quality]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: 构建状态通知
|
||||
run: |
|
||||
if [[ "${{ needs.test.result }}" == "success" && "${{ needs.build.result }}" == "success" && "${{ needs.security.result }}" == "success" && "${{ needs.quality.result }}" == "success" ]]; then
|
||||
echo "✅ 所有检查通过!"
|
||||
echo "STATUS=success" >> $GITHUB_ENV
|
||||
else
|
||||
echo "❌ 部分检查失败"
|
||||
echo "测试结果: ${{ needs.test.result }}"
|
||||
echo "构建结果: ${{ needs.build.result }}"
|
||||
echo "安全扫描: ${{ needs.security.result }}"
|
||||
echo "质量检查: ${{ needs.quality.result }}"
|
||||
echo "STATUS=failure" >> $GITHUB_ENV
|
||||
fi
|
||||
250
.gitea/workflows/release.yml
Normal file
250
.gitea/workflows/release.yml
Normal file
@@ -0,0 +1,250 @@
|
||||
name: 发布流程
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: 发布前测试
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
|
||||
- name: 运行完整测试套件
|
||||
run: |
|
||||
go test -v -race -coverprofile=coverage.out ./...
|
||||
|
||||
- name: 运行基准测试验证
|
||||
run: |
|
||||
go test -bench=. -benchtime=1s -run=^$ ./...
|
||||
|
||||
- name: 检查测试覆盖率
|
||||
run: |
|
||||
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
|
||||
echo "测试覆盖率: ${COVERAGE}%"
|
||||
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
|
||||
echo "❌ 测试覆盖率低于 80%,无法发布"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build:
|
||||
name: 构建发布包
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- goos: linux
|
||||
goarch: amd64
|
||||
- goos: linux
|
||||
goarch: arm64
|
||||
- goos: darwin
|
||||
goarch: amd64
|
||||
- goos: darwin
|
||||
goarch: arm64
|
||||
- goos: windows
|
||||
goarch: amd64
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 设置 Go 环境
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.24.x
|
||||
|
||||
- name: 获取版本信息
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "发布版本: $VERSION"
|
||||
|
||||
- name: 构建示例程序
|
||||
env:
|
||||
GOOS: ${{ matrix.goos }}
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
CGO_ENABLED: 0
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.VERSION }}
|
||||
LDFLAGS="-s -w -X main.version=$VERSION"
|
||||
|
||||
mkdir -p dist/${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
|
||||
# 构建所有示例程序
|
||||
cd examples/basic-usage
|
||||
go build -ldflags="$LDFLAGS" -o ../../dist/${{ matrix.goos }}_${{ matrix.goarch }}/basic-usage${{ matrix.goos == 'windows' && '.exe' || '' }} .
|
||||
|
||||
cd ../group-management
|
||||
go build -ldflags="$LDFLAGS" -o ../../dist/${{ matrix.goos }}_${{ matrix.goarch }}/group-management${{ matrix.goos == 'windows' && '.exe' || '' }} .
|
||||
|
||||
cd ../external-handler
|
||||
go build -ldflags="$LDFLAGS" -o ../../dist/${{ matrix.goos }}_${{ matrix.goarch }}/external-handler${{ matrix.goos == 'windows' && '.exe' || '' }} .
|
||||
|
||||
cd ../data-analytics
|
||||
go build -ldflags="$LDFLAGS" -o ../../dist/${{ matrix.goos }}_${{ matrix.goarch }}/data-analytics${{ matrix.goos == 'windows' && '.exe' || '' }} .
|
||||
|
||||
cd ../concurrent-processing
|
||||
go build -ldflags="$LDFLAGS" -o ../../dist/${{ matrix.goos }}_${{ matrix.goarch }}/concurrent-processing${{ matrix.goos == 'windows' && '.exe' || '' }} .
|
||||
|
||||
cd ../high-concurrency
|
||||
go build -ldflags="$LDFLAGS" -o ../../dist/${{ matrix.goos }}_${{ matrix.goarch }}/high-concurrency${{ matrix.goos == 'windows' && '.exe' || '' }} .
|
||||
|
||||
- name: 创建发布包
|
||||
run: |
|
||||
cd dist/${{ matrix.goos }}_${{ matrix.goarch }}
|
||||
|
||||
# 复制文档文件
|
||||
cp ../../README.md .
|
||||
cp ../../LICENSE .
|
||||
cp -r ../../examples .
|
||||
|
||||
# 创建压缩包
|
||||
if [ "${{ matrix.goos }}" = "windows" ]; then
|
||||
zip -r ../pipelinedb-${{ steps.version.outputs.VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}.zip .
|
||||
else
|
||||
tar -czf ../pipelinedb-${{ steps.version.outputs.VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz .
|
||||
fi
|
||||
|
||||
- name: 上传构建产物
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: pipelinedb-${{ steps.version.outputs.VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}
|
||||
path: dist/pipelinedb-${{ steps.version.outputs.VERSION }}-${{ matrix.goos }}-${{ matrix.goarch }}.*
|
||||
|
||||
release:
|
||||
name: 创建发布
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 获取版本信息
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 下载所有构建产物
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: release-assets
|
||||
|
||||
- name: 生成发布说明
|
||||
run: |
|
||||
VERSION=${{ steps.version.outputs.VERSION }}
|
||||
|
||||
cat > release_notes.md << EOF
|
||||
# Pipeline Database $VERSION
|
||||
|
||||
## 🚀 新特性
|
||||
|
||||
- 基于页面的高性能存储引擎
|
||||
- 支持分组数据管理和统计
|
||||
- Hot → Warm → Cold 三级数据状态流转
|
||||
- 自定义 Handler 接口支持
|
||||
- 高并发线程安全操作
|
||||
- 内置页面缓存系统
|
||||
|
||||
## 📊 性能指标
|
||||
|
||||
- 并发写入:~1,900 QPS
|
||||
- 缓存操作:~7,700,000 QPS
|
||||
- 索引操作:~6,200,000 QPS
|
||||
- 支持 TB 级数据存储
|
||||
|
||||
## 📦 下载
|
||||
|
||||
选择适合您系统的版本:
|
||||
|
||||
- **Linux (x64)**: pipelinedb-$VERSION-linux-amd64.tar.gz
|
||||
- **Linux (ARM64)**: pipelinedb-$VERSION-linux-arm64.tar.gz
|
||||
- **macOS (Intel)**: pipelinedb-$VERSION-darwin-amd64.tar.gz
|
||||
- **macOS (Apple Silicon)**: pipelinedb-$VERSION-darwin-arm64.tar.gz
|
||||
- **Windows (x64)**: pipelinedb-$VERSION-windows-amd64.zip
|
||||
|
||||
## 🛠️ 快速开始
|
||||
|
||||
\`\`\`bash
|
||||
# 安装
|
||||
go get code.tczkiot.com/wlw/pipelinedb@$VERSION
|
||||
|
||||
# 运行示例
|
||||
./basic-usage
|
||||
\`\`\`
|
||||
|
||||
## 📚 文档
|
||||
|
||||
- [README](https://code.tczkiot.com/wlw/pipelinedb/src/branch/main/README.md)
|
||||
- [示例代码](https://code.tczkiot.com/wlw/pipelinedb/src/branch/main/examples)
|
||||
- [API 文档](https://pkg.go.dev/code.tczkiot.com/wlw/pipelinedb)
|
||||
|
||||
## 🔄 更新日志
|
||||
|
||||
查看完整的更新日志和技术细节,请参考 [CHANGELOG.md](https://code.tczkiot.com/wlw/pipelinedb/src/branch/main/CHANGELOG.md)
|
||||
EOF
|
||||
|
||||
- name: 创建 GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
name: Pipeline Database ${{ steps.version.outputs.VERSION }}
|
||||
body_path: release_notes.md
|
||||
files: |
|
||||
release-assets/**/*.tar.gz
|
||||
release-assets/**/*.zip
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker:
|
||||
name: 构建 Docker 镜像
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 获取版本信息
|
||||
id: version
|
||||
run: |
|
||||
VERSION=${GITHUB_REF#refs/tags/}
|
||||
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 设置 Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: 登录 Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: 构建并推送 Docker 镜像
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: |
|
||||
bourdon/pipelinedb:${{ steps.version.outputs.VERSION }}
|
||||
bourdon/pipelinedb:latest
|
||||
labels: |
|
||||
org.opencontainers.image.title=Pipeline Database
|
||||
org.opencontainers.image.description=集成数据库存储和业务管道处理的一体化解决方案
|
||||
org.opencontainers.image.version=${{ steps.version.outputs.VERSION }}
|
||||
org.opencontainers.image.source=https://code.tczkiot.com/wlw/pipelinedb
|
||||
49
.gitignore
vendored
Normal file
49
.gitignore
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
# 数据库文件
|
||||
*.db
|
||||
*.database
|
||||
examples/**/*.db
|
||||
test_*.db
|
||||
*_test.db
|
||||
*_example.db
|
||||
benchmark_*.db
|
||||
|
||||
# Go 编译产物
|
||||
build/
|
||||
dist/
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.test
|
||||
*.out
|
||||
|
||||
# Go 性能分析文件
|
||||
*.prof
|
||||
cpu.prof
|
||||
mem.prof
|
||||
trace.out
|
||||
|
||||
# 测试覆盖率
|
||||
coverage.txt
|
||||
coverage.out
|
||||
*.cover
|
||||
|
||||
# 日志文件
|
||||
*.log
|
||||
debug.log
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
*.bak
|
||||
|
||||
# IDE 文件
|
||||
.vscode/
|
||||
.idea/
|
||||
.zed/
|
||||
*.swp
|
||||
*~
|
||||
|
||||
# 操作系统文件
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
153
.golangci.yml
Normal file
153
.golangci.yml
Normal file
@@ -0,0 +1,153 @@
|
||||
run:
|
||||
timeout: 5m
|
||||
issues-exit-code: 1
|
||||
tests: true
|
||||
skip-dirs:
|
||||
- vendor
|
||||
skip-files:
|
||||
- ".*\\.pb\\.go$"
|
||||
|
||||
output:
|
||||
format: colored-line-number
|
||||
print-issued-lines: true
|
||||
print-linter-name: true
|
||||
|
||||
linters-settings:
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
check-blank: true
|
||||
|
||||
gocyclo:
|
||||
min-complexity: 15
|
||||
|
||||
gofmt:
|
||||
simplify: true
|
||||
|
||||
goimports:
|
||||
local-prefixes: code.tczkiot.com/wlw/pipelinedb
|
||||
|
||||
golint:
|
||||
min-confidence: 0.8
|
||||
|
||||
govet:
|
||||
check-shadowing: true
|
||||
enable-all: true
|
||||
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
unused:
|
||||
check-exported: false
|
||||
|
||||
unparam:
|
||||
check-exported: false
|
||||
|
||||
nakedret:
|
||||
max-func-lines: 30
|
||||
|
||||
prealloc:
|
||||
simple: true
|
||||
range-loops: true
|
||||
for-loops: false
|
||||
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- experimental
|
||||
- opinionated
|
||||
- performance
|
||||
- style
|
||||
disabled-checks:
|
||||
- dupImport
|
||||
- ifElseChain
|
||||
- octalLiteral
|
||||
- whyNoLint
|
||||
- wrapperFunc
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- bodyclose
|
||||
- deadcode
|
||||
- depguard
|
||||
- dogsled
|
||||
- errcheck
|
||||
- exhaustive
|
||||
- exportloopref
|
||||
- funlen
|
||||
- gochecknoinits
|
||||
- goconst
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- gomnd
|
||||
- goprintffuncname
|
||||
- gosec
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- interfacer
|
||||
- lll
|
||||
- misspell
|
||||
- nakedret
|
||||
- noctx
|
||||
- nolintlint
|
||||
- rowserrcheck
|
||||
- scopelint
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- stylecheck
|
||||
- typecheck
|
||||
- unconvert
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
- whitespace
|
||||
|
||||
disable:
|
||||
- maligned # 已废弃
|
||||
- dupl # 允许一定程度的代码重复
|
||||
|
||||
issues:
|
||||
exclude-rules:
|
||||
# 排除测试文件的某些检查
|
||||
- path: _test\.go
|
||||
linters:
|
||||
- gomnd
|
||||
- funlen
|
||||
- gocyclo
|
||||
|
||||
# 排除示例文件的某些检查
|
||||
- path: examples/
|
||||
linters:
|
||||
- gomnd
|
||||
- funlen
|
||||
- errcheck
|
||||
|
||||
# 排除基准测试的某些检查
|
||||
- path: benchmark_test\.go
|
||||
linters:
|
||||
- gomnd
|
||||
- funlen
|
||||
- gocyclo
|
||||
|
||||
# 排除 main 函数的长度检查
|
||||
- text: "Function 'main' has too many statements"
|
||||
linters:
|
||||
- funlen
|
||||
|
||||
# 排除已知的误报
|
||||
- text: "G404: Use of weak random number generator"
|
||||
linters:
|
||||
- gosec
|
||||
path: _test\.go
|
||||
|
||||
exclude-use-default: false
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
new: false
|
||||
|
||||
severity:
|
||||
default-severity: error
|
||||
case-sensitive: false
|
||||
282
Makefile
Normal file
282
Makefile
Normal file
@@ -0,0 +1,282 @@
|
||||
# Pipeline Database - Makefile
|
||||
# ===================================
|
||||
|
||||
# 项目信息
|
||||
PROJECT_NAME := pipelinedb
|
||||
VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
|
||||
|
||||
# Go 工具
|
||||
GOCMD := go
|
||||
GOTEST := $(GOCMD) test
|
||||
GOMOD := $(GOCMD) mod
|
||||
GOFMT := gofmt
|
||||
GOVET := $(GOCMD) vet
|
||||
GOLINT := golangci-lint
|
||||
|
||||
# 目录定义
|
||||
COVERAGE_DIR := coverage
|
||||
EXAMPLES_DIR := examples
|
||||
|
||||
# 默认目标
|
||||
.PHONY: all
|
||||
all: deps fmt vet test
|
||||
|
||||
# ==================== 依赖管理 ====================
|
||||
|
||||
.PHONY: deps
|
||||
deps: ## 下载依赖
|
||||
@echo "📦 下载依赖..."
|
||||
$(GOMOD) download
|
||||
$(GOMOD) tidy
|
||||
|
||||
.PHONY: deps-update
|
||||
deps-update: ## 更新依赖
|
||||
@echo "🔄 更新依赖..."
|
||||
$(GOMOD) get -u ./...
|
||||
$(GOMOD) tidy
|
||||
|
||||
.PHONY: deps-verify
|
||||
deps-verify: ## 验证依赖
|
||||
@echo "✅ 验证依赖..."
|
||||
$(GOMOD) verify
|
||||
|
||||
# ==================== 代码格式化和检查 ====================
|
||||
|
||||
.PHONY: fmt
|
||||
fmt: ## 格式化代码
|
||||
@echo "🎨 格式化代码..."
|
||||
$(GOFMT) -s -w .
|
||||
$(GOCMD) mod tidy
|
||||
|
||||
.PHONY: fmt-check
|
||||
fmt-check: ## 检查代码格式
|
||||
@echo "🔍 检查代码格式..."
|
||||
@if [ -n "$$($(GOFMT) -s -l .)" ]; then \
|
||||
echo "❌ 以下文件需要格式化:"; \
|
||||
$(GOFMT) -s -l .; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "✅ 代码格式正确"
|
||||
|
||||
.PHONY: vet
|
||||
vet: ## 静态代码分析
|
||||
@echo "🔍 静态代码分析..."
|
||||
$(GOVET) ./...
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## 代码质量检查
|
||||
@echo "🔍 代码质量检查..."
|
||||
@if command -v $(GOLINT) >/dev/null 2>&1; then \
|
||||
$(GOLINT) run --timeout=5m; \
|
||||
else \
|
||||
echo "⚠️ golangci-lint 未安装,跳过 lint 检查"; \
|
||||
echo "安装命令: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
|
||||
fi
|
||||
|
||||
.PHONY: lint-install
|
||||
lint-install: ## 安装代码质量检查工具
|
||||
@echo "📥 安装 golangci-lint..."
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
|
||||
|
||||
# ==================== 测试 ====================
|
||||
|
||||
.PHONY: test
|
||||
test: ## 运行测试
|
||||
@echo "🧪 运行测试..."
|
||||
$(GOTEST) -v -race ./...
|
||||
|
||||
.PHONY: test-short
|
||||
test-short: ## 运行快速测试
|
||||
@echo "⚡ 运行快速测试..."
|
||||
$(GOTEST) -short -v ./...
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage: ## 生成测试覆盖率报告
|
||||
@echo "📊 生成测试覆盖率报告..."
|
||||
@mkdir -p $(COVERAGE_DIR)
|
||||
$(GOTEST) -race -coverprofile=$(COVERAGE_DIR)/coverage.out ./...
|
||||
$(GOCMD) tool cover -html=$(COVERAGE_DIR)/coverage.out -o $(COVERAGE_DIR)/coverage.html
|
||||
$(GOCMD) tool cover -func=$(COVERAGE_DIR)/coverage.out | grep total
|
||||
|
||||
.PHONY: test-coverage-ci
|
||||
test-coverage-ci: ## CI环境的覆盖率测试
|
||||
@echo "🤖 CI 覆盖率测试..."
|
||||
$(GOTEST) -race -coverprofile=coverage.out ./...
|
||||
@COVERAGE=$$($(GOCMD) tool cover -func=coverage.out | grep total | awk '{print $$3}' | sed 's/%//'); \
|
||||
echo "测试覆盖率: $${COVERAGE}%"; \
|
||||
if [ $$(echo "$${COVERAGE} < 80" | bc -l 2>/dev/null || echo "0") -eq 1 ]; then \
|
||||
echo "❌ 测试覆盖率低于 80%"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "✅ 测试覆盖率达标"
|
||||
|
||||
# ==================== 基准测试 ====================
|
||||
|
||||
.PHONY: bench
|
||||
bench: ## 运行基准测试
|
||||
@echo "⚡ 运行基准测试..."
|
||||
$(GOTEST) -bench=. -benchmem -run=^$$ ./...
|
||||
|
||||
.PHONY: bench-cpu
|
||||
bench-cpu: ## CPU性能分析
|
||||
@echo "🔥 CPU 性能分析..."
|
||||
@mkdir -p $(COVERAGE_DIR)
|
||||
$(GOTEST) -bench=BenchmarkConcurrentAcceptData -cpuprofile=$(COVERAGE_DIR)/cpu.prof -run=^$$ ./...
|
||||
$(GOCMD) tool pprof -text $(COVERAGE_DIR)/cpu.prof | head -20
|
||||
|
||||
.PHONY: bench-mem
|
||||
bench-mem: ## 内存性能分析
|
||||
@echo "💾 内存性能分析..."
|
||||
@mkdir -p $(COVERAGE_DIR)
|
||||
$(GOTEST) -bench=BenchmarkConcurrentAcceptData -memprofile=$(COVERAGE_DIR)/mem.prof -run=^$$ ./...
|
||||
$(GOCMD) tool pprof -text $(COVERAGE_DIR)/mem.prof | head -20
|
||||
|
||||
.PHONY: bench-concurrent
|
||||
bench-concurrent: ## 并发基准测试
|
||||
@echo "🚀 并发基准测试..."
|
||||
$(GOTEST) -bench=BenchmarkConcurrent -benchmem -benchtime=2s -run=^$$ ./...
|
||||
|
||||
.PHONY: bench-all
|
||||
bench-all: ## 完整基准测试套件
|
||||
@echo "📊 完整基准测试套件..."
|
||||
@echo "=== 核心功能基准测试 ==="
|
||||
$(GOTEST) -bench=BenchmarkPipelineDB -benchmem -benchtime=1s -run=^$$ ./...
|
||||
@echo "=== 并发性能基准测试 ==="
|
||||
$(GOTEST) -bench=BenchmarkConcurrent -benchmem -benchtime=1s -run=^$$ ./...
|
||||
@echo "=== 存储引擎基准测试 ==="
|
||||
$(GOTEST) -bench=BenchmarkStorage -benchmem -benchtime=1s -run=^$$ ./...
|
||||
@echo "=== 缓存系统基准测试 ==="
|
||||
$(GOTEST) -bench=BenchmarkCache -benchmem -benchtime=1s -run=^$$ ./...
|
||||
|
||||
# ==================== 示例运行 ====================
|
||||
|
||||
.PHONY: run-basic
|
||||
run-basic: ## 运行基础使用示例
|
||||
@echo "🎯 运行基础使用示例..."
|
||||
@cd $(EXAMPLES_DIR)/basic-usage && GOWORK=off $(GOCMD) run main.go
|
||||
|
||||
.PHONY: run-group
|
||||
run-group: ## 运行组管理示例
|
||||
@echo "🎯 运行组管理示例..."
|
||||
@cd $(EXAMPLES_DIR)/group-management && GOWORK=off $(GOCMD) run main.go
|
||||
|
||||
.PHONY: run-handler
|
||||
run-handler: ## 运行处理器示例
|
||||
@echo "🎯 运行处理器示例..."
|
||||
@cd $(EXAMPLES_DIR)/external-handler && GOWORK=off $(GOCMD) run main.go
|
||||
|
||||
# ==================== 安全和质量 ====================
|
||||
|
||||
.PHONY: security
|
||||
security: ## 安全扫描
|
||||
@echo "🔒 安全扫描..."
|
||||
@if command -v gosec >/dev/null 2>&1; then \
|
||||
gosec ./...; \
|
||||
else \
|
||||
echo "⚠️ gosec 未安装,跳过安全扫描"; \
|
||||
echo "安装命令: go install github.com/securego/gosec/v2/cmd/gosec@latest"; \
|
||||
fi
|
||||
|
||||
.PHONY: security-install
|
||||
security-install: ## 安装安全扫描工具
|
||||
@echo "📥 安装 gosec..."
|
||||
go install github.com/securego/gosec/v2/cmd/gosec@latest
|
||||
|
||||
.PHONY: complexity
|
||||
complexity: ## 检查代码复杂度
|
||||
@echo "🔍 检查代码复杂度..."
|
||||
@if command -v gocyclo >/dev/null 2>&1; then \
|
||||
gocyclo -over 15 .; \
|
||||
else \
|
||||
echo "⚠️ gocyclo 未安装,跳过复杂度检查"; \
|
||||
echo "安装命令: go install github.com/fzipp/gocyclo/cmd/gocyclo@latest"; \
|
||||
fi
|
||||
|
||||
.PHONY: tools-install
|
||||
tools-install: lint-install security-install ## 安装所有开发工具
|
||||
@echo "📥 安装开发工具..."
|
||||
go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
go install github.com/mibk/dupl@latest
|
||||
|
||||
# ==================== CI/CD 相关 ====================
|
||||
|
||||
.PHONY: ci
|
||||
ci: deps fmt-check vet test-coverage-ci ## CI 流水线
|
||||
@echo "🤖 CI 流水线完成"
|
||||
|
||||
.PHONY: pre-commit
|
||||
pre-commit: fmt vet test ## 提交前检查
|
||||
@echo "✅ 提交前检查完成"
|
||||
|
||||
# ==================== 发布相关 ====================
|
||||
|
||||
.PHONY: release
|
||||
release: ci ## 创建发布标签
|
||||
@echo "🏷️ 准备创建发布标签..."
|
||||
@if [ "$(VERSION)" = "dev" ]; then \
|
||||
echo "❌ 请先创建 Git 标签,例如: git tag v1.0.0"; \
|
||||
exit 1; \
|
||||
fi
|
||||
@echo "📋 当前版本: $(VERSION)"
|
||||
@echo "📊 运行最终检查..."
|
||||
@git status --porcelain | grep -q . && echo "❌ 工作目录不干净,请先提交所有更改" && exit 1 || true
|
||||
@echo "✅ 工作目录干净"
|
||||
@echo "🎉 发布标签 $(VERSION) 已准备就绪"
|
||||
@echo ""
|
||||
@echo "📦 发布信息:"
|
||||
@echo " 版本: $(VERSION)"
|
||||
@echo " 提交: $$(git rev-parse HEAD)"
|
||||
@echo " 分支: $$(git branch --show-current)"
|
||||
@echo " 时间: $$(date)"
|
||||
@echo ""
|
||||
@echo "🚀 要推送标签到远程仓库,请运行:"
|
||||
@echo " git push origin $(VERSION)"
|
||||
|
||||
.PHONY: tag
|
||||
tag: ## 创建新的版本标签
|
||||
@echo "🏷️ 创建新版本标签..."
|
||||
@read -p "请输入版本号 (例如 v1.0.0): " version; \
|
||||
if [ -z "$$version" ]; then \
|
||||
echo "❌ 版本号不能为空"; \
|
||||
exit 1; \
|
||||
fi; \
|
||||
echo "📋 创建标签: $$version"; \
|
||||
git tag -a "$$version" -m "Release $$version"; \
|
||||
echo "✅ 标签 $$version 已创建"; \
|
||||
echo "🚀 要推送标签,请运行: git push origin $$version"
|
||||
|
||||
# ==================== 清理 ====================
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## 清理临时文件
|
||||
@echo "🧹 清理临时文件..."
|
||||
@rm -rf $(COVERAGE_DIR)
|
||||
@rm -f coverage.out *.prof
|
||||
@find . -name "*.db" -type f -delete 2>/dev/null || true
|
||||
@find . -name "*.log" -type f -delete 2>/dev/null || true
|
||||
|
||||
# ==================== 开发辅助 ====================
|
||||
|
||||
.PHONY: dev-setup
|
||||
dev-setup: deps tools-install ## 开发环境设置
|
||||
@echo "🛠️ 开发环境设置完成"
|
||||
|
||||
.PHONY: help
|
||||
help: ## 显示帮助信息
|
||||
@echo "Pipeline Database - Makefile 帮助"
|
||||
@echo "======================================"
|
||||
@echo ""
|
||||
@echo "可用命令:"
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
@echo ""
|
||||
@echo "常用命令组合:"
|
||||
@echo " make dev-setup # 初始化开发环境"
|
||||
@echo " make pre-commit # 提交前检查"
|
||||
@echo " make ci # CI 流水线"
|
||||
@echo " make tag # 创建版本标签"
|
||||
@echo " make release # 发布检查"
|
||||
|
||||
# 默认显示帮助
|
||||
.DEFAULT_GOAL := help
|
||||
295
README.md
Normal file
295
README.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# Pipeline Database
|
||||
|
||||
[](https://golang.org/)
|
||||
[](LICENSE)
|
||||
[](https://code.tczkiot.com/wlw/pipelinedb)
|
||||
|
||||
一个集成了数据库存储和业务管道处理的一体化解决方案,专为数据管道处理场景设计。
|
||||
|
||||
## 🚀 核心特性
|
||||
|
||||
- **基于页面的存储引擎**:高效的数据存储和检索,4KB页面对齐
|
||||
- **分组数据管理**:支持按业务组织数据,独立管理和统计
|
||||
- **三级数据状态**:Hot(热)→ Warm(温)→ Cold(冷)的自动流转
|
||||
- **自定义处理器**:支持用户实现 Handler 接口定制业务逻辑
|
||||
- **高并发支持**:线程安全的索引和存储操作
|
||||
- **页面缓存**:内置缓存系统提高数据访问性能
|
||||
- **持久化存储**:数据安全持久保存到磁盘
|
||||
|
||||
## 📊 数据流转模型
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
|
||||
A[新数据接收] --> B[Hot<br/>热数据]
|
||||
B -->|预热处理<br/>Handler.WillWarm| C[Warm<br/>温数据]
|
||||
C -->|冷却处理<br/>Handler.WillCold| D[Cold<br/>冷数据]
|
||||
|
||||
style B fill:#ff9999
|
||||
style C fill:#ffcc99
|
||||
style D fill:#99ccff
|
||||
```
|
||||
|
||||
## 🛠️ 快速开始
|
||||
|
||||
### 安装
|
||||
|
||||
```bash
|
||||
go get code.tczkiot.com/wlw/pipelinedb
|
||||
```
|
||||
|
||||
### 基础使用
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"code.tczkiot.com/wlw/pipelinedb"
|
||||
)
|
||||
|
||||
// 实现自定义处理器
|
||||
type MyHandler struct{}
|
||||
|
||||
func (h *MyHandler) WillWarm(ctx context.Context, group string, data []byte) ([]byte, error) {
|
||||
// 预热处理逻辑:数据验证、转换等
|
||||
return append([]byte("processed_"), data...), nil
|
||||
}
|
||||
|
||||
func (h *MyHandler) WillCold(ctx context.Context, group string, data []byte) ([]byte, error) {
|
||||
// 冷却处理逻辑:数据压缩、归档等
|
||||
return append(data, []byte("_archived")...), nil
|
||||
}
|
||||
|
||||
func (h *MyHandler) OnComplete(ctx context.Context, group string) error {
|
||||
// 组完成回调:清理、通知等
|
||||
log.Printf("Group %s processing completed", group)
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 打开数据库
|
||||
pdb, err := pipelinedb.Open(pipelinedb.Options{
|
||||
Filename: "my_pipeline.db",
|
||||
Handler: &MyHandler{},
|
||||
Config: pipelinedb.DefaultConfig(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 启动管道处理
|
||||
pdb.Start()
|
||||
|
||||
// 接收数据
|
||||
id, err := pdb.AcceptData("user_events", []byte("user clicked button"), "metadata")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Data accepted with ID: %d", id)
|
||||
|
||||
// 查询数据
|
||||
pageReq := &pipelinedb.PageRequest{Page: 1, PageSize: 10}
|
||||
response, err := pdb.GetRecordsByGroup("user_events", pageReq)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, record := range response.Records {
|
||||
log.Printf("Record ID: %d, Status: %s, Data: %s",
|
||||
record.ID, record.Status, string(record.Data))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📚 API 文档
|
||||
|
||||
### 核心接口
|
||||
|
||||
#### Handler 接口
|
||||
|
||||
```go
|
||||
type Handler interface {
|
||||
// 预热处理回调:Hot -> Warm
|
||||
WillWarm(ctx context.Context, group string, data []byte) ([]byte, error)
|
||||
|
||||
// 冷却处理回调:Warm -> Cold
|
||||
WillCold(ctx context.Context, group string, data []byte) ([]byte, error)
|
||||
|
||||
// 组完成回调:所有数据处理完成
|
||||
OnComplete(ctx context.Context, group string) error
|
||||
}
|
||||
```
|
||||
|
||||
#### 主要方法
|
||||
|
||||
```go
|
||||
// 打开数据库
|
||||
func Open(opts Options) (*PipelineDB, error)
|
||||
|
||||
// 接收数据
|
||||
func (pdb *PipelineDB) AcceptData(group string, data []byte, metadata string) (int64, error)
|
||||
|
||||
// 按组查询数据(分页)
|
||||
func (pdb *PipelineDB) GetRecordsByGroup(group string, req *PageRequest) (*PageResponse, error)
|
||||
|
||||
// 获取统计信息
|
||||
func (pdb *PipelineDB) GetStats() *DatabaseStats
|
||||
|
||||
// 启动管道处理
|
||||
func (pdb *PipelineDB) Start()
|
||||
|
||||
// 停止数据库
|
||||
func (pdb *PipelineDB) Stop()
|
||||
```
|
||||
|
||||
### 配置选项
|
||||
|
||||
```go
|
||||
type Config struct {
|
||||
CacheSize int // 页缓存大小(页数)
|
||||
SyncWrites bool // 是否同步写入
|
||||
CreateIfMiss bool // 文件不存在时自动创建
|
||||
WarmInterval time.Duration // 预热间隔
|
||||
ProcessInterval time.Duration // 处理间隔
|
||||
BatchSize int // 批处理大小
|
||||
EnableMetrics bool // 启用性能指标
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 使用场景
|
||||
|
||||
### 数据管道处理
|
||||
- **ETL 流程**:数据提取、转换、加载
|
||||
- **数据清洗**:数据验证、格式化、去重
|
||||
- **数据转换**:格式转换、数据映射
|
||||
|
||||
### 业务流程管理
|
||||
- **订单处理**:订单创建 → 支付确认 → 发货处理
|
||||
- **用户行为分析**:事件收集 → 数据处理 → 报告生成
|
||||
- **审批流程**:申请提交 → 审核处理 → 结果通知
|
||||
|
||||
### 实时数据处理
|
||||
- **日志分析**:日志收集 → 解析处理 → 存储归档
|
||||
- **监控数据**:指标收集 → 聚合计算 → 告警处理
|
||||
- **消息队列**:消息接收 → 业务处理 → 确认回复
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
pipelinedb/
|
||||
├── README.md # 项目说明
|
||||
├── go.mod # Go 模块定义
|
||||
├── pipeline_db.go # 核心数据库实现
|
||||
├── storage.go # 存储引擎
|
||||
├── cache.go # 页面缓存
|
||||
├── index.go # 索引管理
|
||||
├── group_manager.go # 组管理器
|
||||
├── counter.go # ID 计数器
|
||||
├── examples/ # 使用示例
|
||||
│ ├── basic-usage/ # 基础使用示例
|
||||
│ ├── group-management/ # 组管理示例
|
||||
│ ├── external-handler/ # 自定义处理器示例
|
||||
│ ├── data-analytics/ # 数据分析示例
|
||||
│ ├── concurrent-processing/ # 并发处理示例
|
||||
│ └── high-concurrency/ # 高并发压力测试
|
||||
└── *_test.go # 测试文件
|
||||
```
|
||||
|
||||
## 🧪 运行示例
|
||||
|
||||
项目提供了多个完整的使用示例:
|
||||
|
||||
```bash
|
||||
# 基础使用示例
|
||||
cd examples/basic-usage && go run main.go
|
||||
|
||||
# 组管理示例
|
||||
cd examples/group-management && go run main.go
|
||||
|
||||
# 自定义处理器示例
|
||||
cd examples/external-handler && go run main.go
|
||||
|
||||
# 数据分析示例
|
||||
cd examples/data-analytics && go run main.go
|
||||
|
||||
# 并发处理示例
|
||||
cd examples/concurrent-processing && go run main.go
|
||||
|
||||
# 高并发压力测试
|
||||
cd examples/high-concurrency && go run main.go
|
||||
```
|
||||
|
||||
## 🔧 开发和测试
|
||||
|
||||
### 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
go test -v
|
||||
|
||||
# 运行特定测试
|
||||
go test -run TestPipelineDB -v
|
||||
|
||||
# 运行基准测试
|
||||
go test -bench=. -benchmem
|
||||
|
||||
# 生成测试覆盖率报告
|
||||
go test -cover -coverprofile=coverage.out
|
||||
go tool cover -html=coverage.out
|
||||
```
|
||||
|
||||
### 性能基准
|
||||
|
||||
在现代硬件上的典型性能表现:
|
||||
|
||||
- **写入性能**:~50,000 QPS
|
||||
- **读取性能**:~100,000 QPS
|
||||
- **内存使用**:~1MB(256页缓存)
|
||||
- **磁盘使用**:高效的页面存储,支持TB级数据
|
||||
|
||||
## 🏗️ 架构设计
|
||||
|
||||
### 存储层
|
||||
- **页面管理**:4KB 页面,支持链式扩展
|
||||
- **槽位目录**:变长记录存储,空间高效利用
|
||||
- **空闲页管理**:智能页面回收和重用
|
||||
|
||||
### 索引层
|
||||
- **B+树索引**:快速数据定位和范围查询
|
||||
- **分组索引**:独立的组级别索引管理
|
||||
- **内存索引**:热数据索引常驻内存
|
||||
|
||||
### 缓存层
|
||||
- **LRU 缓存**:最近最少使用页面淘汰策略
|
||||
- **写回缓存**:批量写入提高性能
|
||||
- **预读机制**:顺序访问优化
|
||||
|
||||
### 并发控制
|
||||
- **行级锁**:细粒度锁定,提高并发性能
|
||||
- **读写锁**:读操作并发,写操作独占
|
||||
- **无锁设计**:关键路径避免锁竞争
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支:`git checkout -b feature/amazing-feature`
|
||||
3. 提交更改:`git commit -m 'Add amazing feature'`
|
||||
4. 推送分支:`git push origin feature/amazing-feature`
|
||||
5. 提交 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
感谢所有为这个项目做出贡献的开发者!
|
||||
|
||||
---
|
||||
|
||||
**Pipeline Database** - 让数据管道处理更简单、更高效! 🚀
|
||||
716
benchmark_test.go
Normal file
716
benchmark_test.go
Normal file
@@ -0,0 +1,716 @@
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// BenchmarkPipelineDBOpen 基准测试:数据库打开
|
||||
func BenchmarkPipelineDBOpen(b *testing.B) {
|
||||
config := &Config{CacheSize: 50}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
tmpFile, err := os.CreateTemp("", "bench_open_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
tmpFile.Close()
|
||||
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
|
||||
pdb.Stop()
|
||||
os.Remove(tmpFile.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkAcceptData 基准测试:数据接收
|
||||
func BenchmarkAcceptData(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_accept_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 100}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
testData := []byte("benchmark test data for AcceptData performance")
|
||||
testMetadata := `{"source": "benchmark", "type": "test"}`
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
group := fmt.Sprintf("group_%d", i%10) // 10个不同的组
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetRecordsByGroup 基准测试:按组查询记录
|
||||
func BenchmarkGetRecordsByGroup(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_query_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 100}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 预插入测试数据
|
||||
testData := []byte("query benchmark data")
|
||||
testMetadata := `{"type": "benchmark"}`
|
||||
|
||||
groups := []string{"group_a", "group_b", "group_c"}
|
||||
for _, group := range groups {
|
||||
for i := 0; i < 100; i++ {
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageReq := &PageRequest{Page: 1, PageSize: 20}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
group := groups[i%len(groups)]
|
||||
_, err := pdb.GetRecordsByGroup(group, pageReq)
|
||||
if err != nil {
|
||||
b.Fatalf("GetRecordsByGroup failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkDataProcessing 基准测试:数据处理流程
|
||||
func BenchmarkDataProcessing(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_process_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 100}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
testData := []byte("processing benchmark data")
|
||||
testMetadata := `{"type": "benchmark"}`
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
group := fmt.Sprintf("group_%d", i%3)
|
||||
|
||||
// 接收数据
|
||||
recordID, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
|
||||
// 查询数据
|
||||
pageReq := &PageRequest{Page: 1, PageSize: 1}
|
||||
_, err = pdb.GetRecordsByGroup(group, pageReq)
|
||||
if err != nil {
|
||||
b.Fatalf("GetRecordsByGroup failed: %v", err)
|
||||
}
|
||||
|
||||
// 使用recordID避免未使用警告
|
||||
_ = recordID
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetStats 基准测试:获取统计信息
|
||||
func BenchmarkGetStats(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_stats_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 100}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 预插入测试数据
|
||||
testData := []byte("stats benchmark data")
|
||||
testMetadata := `{"type": "benchmark"}`
|
||||
|
||||
for i := 0; i < 500; i++ {
|
||||
group := fmt.Sprintf("group_%d", i%5)
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := pdb.GetStats()
|
||||
if err != nil {
|
||||
b.Fatalf("GetStats failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkCacheOperations 基准测试:缓存操作
|
||||
func BenchmarkCacheOperations(b *testing.B) {
|
||||
cache := NewPageCache(100)
|
||||
|
||||
// 准备测试数据
|
||||
testPageData := make([]byte, PageSize)
|
||||
for i := 0; i < PageSize; i++ {
|
||||
testPageData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
pageNo := uint16(i % 50) // 50个不同的页面
|
||||
|
||||
// 测试 Put 和 Get 操作
|
||||
cache.Put(pageNo, testPageData, false) // false表示非脏页
|
||||
_, found := cache.Get(pageNo)
|
||||
if !found {
|
||||
b.Fatalf("cache Get failed for page %d", pageNo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFreePageManager 基准测试:空闲页面管理
|
||||
func BenchmarkFreePageManager(b *testing.B) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 预分配一些页面
|
||||
for i := uint16(1); i <= 100; i++ {
|
||||
fpm.FreePage(i)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// 分配页面
|
||||
pageNo, ok := fpm.AllocPage()
|
||||
if !ok {
|
||||
// 重新添加页面
|
||||
fpm.FreePage(uint16(i%100 + 1))
|
||||
pageNo, ok = fpm.AllocPage()
|
||||
if !ok {
|
||||
b.Fatalf("AllocPage failed")
|
||||
}
|
||||
}
|
||||
|
||||
// 释放页面
|
||||
fpm.FreePage(pageNo)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGroupIndex 基准测试:组索引操作
|
||||
func BenchmarkGroupIndex(b *testing.B) {
|
||||
idx := NewGroupIndex("test_group")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
recordID := int64(i)
|
||||
pageNo := uint16(i % 100)
|
||||
slotNo := uint16(i % 10)
|
||||
|
||||
// 插入
|
||||
idx.Insert(recordID, pageNo, slotNo)
|
||||
|
||||
// 查询
|
||||
_, _, found := idx.Get(recordID)
|
||||
if !found {
|
||||
b.Fatalf("Get failed for record %d", recordID)
|
||||
}
|
||||
|
||||
// 删除(每10次删除一次,保持索引有数据)
|
||||
if i%10 == 0 {
|
||||
idx.Delete(recordID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkConcurrentAcceptData 基准测试:并发数据接收
|
||||
func BenchmarkConcurrentAcceptData(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_concurrent_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 200}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
testData := []byte("concurrent benchmark data")
|
||||
testMetadata := `{"type": "concurrent"}`
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
group := fmt.Sprintf("group_%d", i%5)
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkHandler 基准测试:数据处理器
|
||||
func BenchmarkHandler(b *testing.B) {
|
||||
handler := NewLoggingHandler("benchmark")
|
||||
ctx := context.Background()
|
||||
testData := []byte("handler benchmark data")
|
||||
group := "test_group"
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
// 测试预热处理
|
||||
_, err := handler.WillWarm(ctx, group, testData)
|
||||
if err != nil {
|
||||
b.Fatalf("WillWarm failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkStorageOperations 基准测试:存储层操作(从原来的文件移过来)
|
||||
func BenchmarkStorageOperations(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_storage_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 100}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 预分配页面
|
||||
pageNo, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
b.Fatalf("allocPage failed: %v", err)
|
||||
}
|
||||
|
||||
testData := []byte("storage benchmark data")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
id := int64(i)
|
||||
|
||||
// 插入记录
|
||||
slotNo, err := pdb.insertToPage(pageNo, id, testData)
|
||||
if err != nil {
|
||||
// 页面满了,分配新页面
|
||||
pageNo, err = pdb.allocPage()
|
||||
if err != nil {
|
||||
b.Fatalf("allocPage failed: %v", err)
|
||||
}
|
||||
slotNo, err = pdb.insertToPage(pageNo, id, testData)
|
||||
if err != nil {
|
||||
b.Fatalf("insertToPage failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 读取记录
|
||||
_, err = pdb.readRecord(pageNo, slotNo, id)
|
||||
if err != nil {
|
||||
b.Fatalf("readRecord failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 并发基准测试 ====================
|
||||
|
||||
// BenchmarkConcurrentQuery 基准测试:并发查询
|
||||
func BenchmarkConcurrentQuery(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_concurrent_query_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 200}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 预插入测试数据
|
||||
testData := []byte("concurrent query benchmark data")
|
||||
testMetadata := `{"type": "concurrent_query"}`
|
||||
groups := []string{"group_a", "group_b", "group_c", "group_d", "group_e"}
|
||||
|
||||
for _, group := range groups {
|
||||
for i := 0; i < 200; i++ {
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageReq := &PageRequest{Page: 1, PageSize: 10}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
group := groups[i%len(groups)]
|
||||
_, err := pdb.GetRecordsByGroup(group, pageReq)
|
||||
if err != nil {
|
||||
b.Fatalf("GetRecordsByGroup failed: %v", err)
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkConcurrentMixed 基准测试:并发读写混合操作
|
||||
func BenchmarkConcurrentMixed(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_concurrent_mixed_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 300}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
testData := []byte("concurrent mixed benchmark data")
|
||||
testMetadata := `{"type": "concurrent_mixed"}`
|
||||
groups := []string{"group_1", "group_2", "group_3"}
|
||||
|
||||
// 预插入一些数据用于查询
|
||||
for _, group := range groups {
|
||||
for i := 0; i < 50; i++ {
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageReq := &PageRequest{Page: 1, PageSize: 5}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
group := groups[i%len(groups)]
|
||||
|
||||
// 80% 写操作,20% 读操作
|
||||
if i%5 == 0 {
|
||||
// 读操作
|
||||
_, err := pdb.GetRecordsByGroup(group, pageReq)
|
||||
if err != nil {
|
||||
b.Fatalf("GetRecordsByGroup failed: %v", err)
|
||||
}
|
||||
} else {
|
||||
// 写操作
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkConcurrentStats 基准测试:并发统计查询
|
||||
func BenchmarkConcurrentStats(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_concurrent_stats_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 150}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 预插入测试数据
|
||||
testData := []byte("concurrent stats benchmark data")
|
||||
testMetadata := `{"type": "concurrent_stats"}`
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
group := fmt.Sprintf("group_%d", i%10)
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
_, err := pdb.GetStats()
|
||||
if err != nil {
|
||||
b.Fatalf("GetStats failed: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkConcurrentCache 基准测试:并发缓存操作
|
||||
func BenchmarkConcurrentCache(b *testing.B) {
|
||||
cache := NewPageCache(200)
|
||||
|
||||
// 准备测试数据
|
||||
testPageData := make([]byte, PageSize)
|
||||
for i := 0; i < PageSize; i++ {
|
||||
testPageData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
pageNo := uint16(i % 100) // 100个不同的页面
|
||||
|
||||
// 80% 读操作,20% 写操作
|
||||
if i%5 == 0 {
|
||||
// 写操作
|
||||
cache.Put(pageNo, testPageData, i%2 == 0) // 随机设置脏页标志
|
||||
} else {
|
||||
// 读操作
|
||||
_, _ = cache.Get(pageNo)
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkConcurrentIndexOperations 基准测试:并发索引操作
|
||||
func BenchmarkConcurrentIndexOperations(b *testing.B) {
|
||||
indexMgr := NewIndexManager()
|
||||
groups := []string{"idx_group_1", "idx_group_2", "idx_group_3", "idx_group_4"}
|
||||
|
||||
// 预创建索引
|
||||
for _, group := range groups {
|
||||
indexMgr.GetOrCreateIndex(group)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
group := groups[i%len(groups)]
|
||||
idx := indexMgr.GetOrCreateIndex(group)
|
||||
|
||||
recordID := int64(i)
|
||||
pageNo := uint16(i % 50)
|
||||
slotNo := uint16(i % 20)
|
||||
|
||||
// 70% 插入,20% 查询,10% 删除
|
||||
switch i % 10 {
|
||||
case 0: // 删除操作
|
||||
idx.Delete(recordID - 100) // 删除之前的记录
|
||||
case 1, 2: // 查询操作
|
||||
_, _, _ = idx.Get(recordID - 50) // 查询之前的记录
|
||||
default: // 插入操作
|
||||
idx.Insert(recordID, pageNo, slotNo)
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkHighConcurrencyWrite 基准测试:高并发写入
|
||||
func BenchmarkHighConcurrencyWrite(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_high_concurrency_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{
|
||||
CacheSize: 500, // 更大的缓存
|
||||
SyncWrites: false, // 异步写入提高性能
|
||||
}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
testData := []byte("high concurrency write benchmark data")
|
||||
testMetadata := `{"type": "high_concurrency"}`
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
// 设置更高的并发度
|
||||
b.SetParallelism(20)
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
group := fmt.Sprintf("hc_group_%d", i%20) // 20个不同的组
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkConcurrentGroupOperations 基准测试:并发组操作
|
||||
func BenchmarkConcurrentGroupOperations(b *testing.B) {
|
||||
tmpFile, err := os.CreateTemp("", "bench_concurrent_group_*.db")
|
||||
if err != nil {
|
||||
b.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 250}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
b.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
testData := []byte("concurrent group operations benchmark data")
|
||||
testMetadata := `{"type": "group_ops"}`
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
// 预插入一些数据,避免查询空组
|
||||
for i := 0; i < 15; i++ {
|
||||
group := fmt.Sprintf("grp_%d", i)
|
||||
for j := 0; j < 5; j++ {
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
group := fmt.Sprintf("grp_%d", i%15) // 15个不同的组
|
||||
|
||||
// 混合操作:写入、查询、统计
|
||||
switch i % 6 {
|
||||
case 0, 1, 2, 3: // 写入操作 (66.7%)
|
||||
_, err := pdb.AcceptData(group, testData, testMetadata)
|
||||
if err != nil {
|
||||
b.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
case 4: // 查询操作 (16.7%)
|
||||
pageReq := &PageRequest{Page: 1, PageSize: 3}
|
||||
_, err := pdb.GetRecordsByGroup(group, pageReq)
|
||||
if err != nil {
|
||||
b.Fatalf("GetRecordsByGroup failed: %v", err)
|
||||
}
|
||||
case 5: // 统计操作 (16.7%)
|
||||
_, err := pdb.GetStats()
|
||||
if err != nil {
|
||||
b.Fatalf("GetStats failed: %v", err)
|
||||
}
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
469
cache.go
Normal file
469
cache.go
Normal file
@@ -0,0 +1,469 @@
|
||||
// Package pipelinedb provides an integrated pipeline database system
|
||||
// 集成了数据库存储和业务管道处理的一体化解决方案
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// PageCache 实现了基于LRU(Least Recently Used)算法的页面缓存系统
|
||||
//
|
||||
// 核心设计思想:
|
||||
// 1. 使用HashMap提供O(1)的页面查找性能
|
||||
// 2. 使用双向链表维护页面的访问顺序(LRU顺序)
|
||||
// 3. 支持脏页标记和批量刷新机制
|
||||
// 4. 提供线程安全的并发访问控制
|
||||
// 5. 统计缓存命中率用于性能监控
|
||||
//
|
||||
// 适用场景:
|
||||
// - 数据库页面缓存
|
||||
// - 文件系统缓存
|
||||
// - 任何需要LRU淘汰策略的缓存系统
|
||||
type PageCache struct {
|
||||
capacity int // 缓存容量(最大页面数)
|
||||
cache map[uint16]*cacheEntry // 页号到缓存条目的映射,提供O(1)查找
|
||||
lru *list.List // LRU双向链表,维护访问顺序(头部=最近使用,尾部=最久未使用)
|
||||
mu sync.RWMutex // 读写锁,支持多读单写的并发控制
|
||||
hits int64 // 缓存命中次数统计
|
||||
misses int64 // 缓存未命中次数统计
|
||||
}
|
||||
|
||||
// cacheEntry 缓存条目,存储单个页面的完整信息
|
||||
//
|
||||
// 设计要点:
|
||||
// 1. pageNo: 页面编号,用于标识唯一页面
|
||||
// 2. page: 页面数据的完整副本,避免外部修改影响缓存
|
||||
// 3. dirty: 脏页标记,标识页面是否被修改过需要写回磁盘
|
||||
// 4. elem: 指向LRU链表中对应节点的指针,用于O(1)时间复杂度的链表操作
|
||||
type cacheEntry struct {
|
||||
pageNo uint16 // 页面编号(16位,支持65536个页面)
|
||||
page []byte // 页面数据副本(避免外部修改)
|
||||
dirty bool // 脏页标记(true=需要写回磁盘,false=与磁盘一致)
|
||||
elem *list.Element // LRU链表节点指针(用于快速移动和删除)
|
||||
}
|
||||
|
||||
// NewPageCache 创建一个新的LRU页面缓存实例
|
||||
//
|
||||
// 参数说明:
|
||||
// - capacity: 缓存容量,即最大可缓存的页面数量
|
||||
// 建议根据可用内存和页面大小来设置,例如:
|
||||
// 可用内存100MB,页面大小4KB,则capacity可设为25600
|
||||
//
|
||||
// 返回值:
|
||||
// - *PageCache: 初始化完成的页面缓存实例
|
||||
//
|
||||
// 初始化内容:
|
||||
// 1. 设置缓存容量
|
||||
// 2. 创建空的HashMap用于O(1)查找
|
||||
// 3. 创建空的双向链表用于LRU排序
|
||||
// 4. 统计计数器初始化为0(hits和misses)
|
||||
func NewPageCache(capacity int) *PageCache {
|
||||
return &PageCache{
|
||||
capacity: capacity, // 设置最大缓存页面数
|
||||
cache: make(map[uint16]*cacheEntry), // 初始化页号到缓存条目的映射
|
||||
lru: list.New(), // 初始化LRU双向链表
|
||||
// hits和misses会自动初始化为0
|
||||
}
|
||||
}
|
||||
|
||||
// Get 从缓存中获取指定页面的数据
|
||||
//
|
||||
// 核心算法:LRU缓存查找
|
||||
// 时间复杂度:O(1) - HashMap查找 + 双向链表移动
|
||||
// 空间复杂度:O(1) - 只创建页面数据副本
|
||||
//
|
||||
// 参数说明:
|
||||
// - pageNo: 要获取的页面编号(16位,范围0-65535)
|
||||
//
|
||||
// 返回值:
|
||||
// - []byte: 页面数据的副本(如果找到)
|
||||
// - bool: 是否在缓存中找到该页面
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取读锁(支持并发读取)
|
||||
// 2. 在HashMap中查找页面
|
||||
// 3. 如果找到:
|
||||
// a. 将页面移动到LRU链表头部(标记为最近使用)
|
||||
// b. 增加命中计数
|
||||
// c. 创建并返回页面数据副本(防止外部修改)
|
||||
// 4. 如果未找到:
|
||||
// a. 增加未命中计数
|
||||
// b. 返回nil和false
|
||||
//
|
||||
// 并发安全:使用读锁保护,支持多个goroutine同时读取
|
||||
func (pc *PageCache) Get(pageNo uint16) ([]byte, bool) {
|
||||
pc.mu.RLock() // 获取读锁,允许并发读取
|
||||
defer pc.mu.RUnlock() // 确保函数退出时释放锁
|
||||
|
||||
if entry, exists := pc.cache[pageNo]; exists {
|
||||
// 缓存命中:执行LRU更新和数据返回
|
||||
|
||||
// 将访问的页面移动到LRU链表头部
|
||||
// 这是LRU算法的核心:最近访问的页面不会被淘汰
|
||||
pc.lru.MoveToFront(entry.elem)
|
||||
|
||||
// 更新命中统计(用于计算缓存命中率)
|
||||
pc.hits++
|
||||
|
||||
// 创建页面数据副本并返回
|
||||
// 重要:返回副本而不是原始数据,防止外部修改影响缓存
|
||||
pageCopy := make([]byte, len(entry.page))
|
||||
copy(pageCopy, entry.page)
|
||||
return pageCopy, true
|
||||
}
|
||||
|
||||
// 缓存未命中:更新统计并返回失败
|
||||
pc.misses++
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Put 将页面数据放入缓存
|
||||
//
|
||||
// 核心算法:LRU缓存插入/更新
|
||||
// 时间复杂度:O(1) - HashMap操作 + 双向链表操作
|
||||
// 空间复杂度:O(1) - 创建页面数据副本
|
||||
//
|
||||
// 参数说明:
|
||||
// - pageNo: 页面编号(16位,范围0-65535)
|
||||
// - p: 页面数据(字节数组)
|
||||
// - dirty: 脏页标记(true=页面已修改需要写回,false=页面与磁盘一致)
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取写锁(独占访问)
|
||||
// 2. 检查页面是否已存在:
|
||||
// a. 如果存在:更新页面数据和脏页标记,移动到LRU头部
|
||||
// b. 如果不存在:检查容量,必要时淘汰LRU页面,然后插入新页面
|
||||
// 3. 所有操作都使用数据副本,确保缓存数据独立性
|
||||
//
|
||||
// 脏页处理:
|
||||
// - 如果新数据标记为dirty,则页面变为脏页
|
||||
// - 如果页面已经是脏页,则保持脏页状态(dirty标记具有粘性)
|
||||
// - 脏页在淘汰时需要写回磁盘
|
||||
//
|
||||
// 并发安全:使用写锁保护,确保缓存状态一致性
|
||||
func (pc *PageCache) Put(pageNo uint16, p []byte, dirty bool) {
|
||||
pc.mu.Lock() // 获取写锁,独占访问缓存
|
||||
defer pc.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 情况1:页面已存在,执行更新操作
|
||||
if entry, exists := pc.cache[pageNo]; exists {
|
||||
// 创建新的页面数据副本
|
||||
// 重要:使用副本避免外部修改影响缓存
|
||||
pageCopy := make([]byte, len(p))
|
||||
copy(pageCopy, p)
|
||||
|
||||
// 更新页面数据
|
||||
entry.page = pageCopy
|
||||
|
||||
// 更新脏页标记:一旦标记为脏页,就保持脏页状态
|
||||
// 使用OR操作确保脏页标记的粘性(sticky)
|
||||
entry.dirty = entry.dirty || dirty
|
||||
|
||||
// 将页面移动到LRU链表头部(标记为最近使用)
|
||||
pc.lru.MoveToFront(entry.elem)
|
||||
return
|
||||
}
|
||||
|
||||
// 情况2:页面不存在,需要插入新页面
|
||||
|
||||
// 检查缓存容量,如果已满则淘汰最久未使用的页面
|
||||
if len(pc.cache) >= pc.capacity {
|
||||
pc.evictLRU()
|
||||
}
|
||||
|
||||
// 创建页面数据副本
|
||||
pageCopy := make([]byte, len(p))
|
||||
copy(pageCopy, p)
|
||||
|
||||
// 创建新的缓存条目
|
||||
entry := &cacheEntry{
|
||||
pageNo: pageNo, // 设置页面编号
|
||||
page: pageCopy, // 设置页面数据副本
|
||||
dirty: dirty, // 设置脏页标记
|
||||
}
|
||||
|
||||
// 将新条目添加到LRU链表头部(最近使用位置)
|
||||
// 同时将链表元素指针保存到条目中,用于后续的O(1)操作
|
||||
entry.elem = pc.lru.PushFront(entry)
|
||||
|
||||
// 将新条目添加到HashMap中,建立页号到条目的映射
|
||||
pc.cache[pageNo] = entry
|
||||
}
|
||||
|
||||
// evictLRU 淘汰最久未使用的页面
|
||||
//
|
||||
// 核心算法:LRU淘汰策略
|
||||
// 时间复杂度:O(1) - 直接访问链表尾部
|
||||
// 空间复杂度:O(1) - 只释放一个页面的空间
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 检查LRU链表是否为空
|
||||
// 2. 获取链表尾部元素(最久未使用的页面)
|
||||
// 3. 检查页面是否为脏页:
|
||||
// a. 如果是脏页:需要写回磁盘(当前实现中暂时跳过)
|
||||
// b. 如果不是脏页:直接淘汰
|
||||
// 4. 从HashMap和LRU链表中移除页面
|
||||
//
|
||||
// 脏页处理:
|
||||
// - 脏页表示页面数据与磁盘不一致,淘汰前必须写回
|
||||
// - 当前实现中暂时跳过写回操作(标记为TODO)
|
||||
// - 生产环境中应该实现写回机制或者抛出错误
|
||||
//
|
||||
// 注意事项:
|
||||
// - 该方法假设调用者已经持有写锁
|
||||
// - 该方法不检查容量,由调用者确保需要淘汰
|
||||
func (pc *PageCache) evictLRU() {
|
||||
// 安全检查:如果链表为空,直接返回
|
||||
if pc.lru.Len() == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取LRU链表尾部元素(最久未使用的页面)
|
||||
elem := pc.lru.Back()
|
||||
if elem == nil {
|
||||
return // 双重检查,防止并发问题
|
||||
}
|
||||
|
||||
// 从链表元素中提取缓存条目
|
||||
if elem.Value == nil {
|
||||
return // 防止并发竞争导致的nil值
|
||||
}
|
||||
entry := elem.Value.(*cacheEntry)
|
||||
pageNo := entry.pageNo
|
||||
|
||||
// 脏页处理:检查是否需要写回磁盘
|
||||
if entry.dirty {
|
||||
// TODO: 实现脏页写回机制
|
||||
// 在生产环境中,这里应该:
|
||||
// 1. 调用写回函数将页面数据写入磁盘
|
||||
// 2. 处理写回失败的情况(重试或报错)
|
||||
// 3. 只有写回成功后才能淘汰页面
|
||||
//
|
||||
// 示例实现:
|
||||
// if err := pc.writeBackFunc(pageNo, entry.page); err != nil {
|
||||
// // 处理写回失败,可能需要保留页面或记录错误
|
||||
// return
|
||||
// }
|
||||
}
|
||||
|
||||
// 从HashMap中移除页面映射
|
||||
delete(pc.cache, pageNo)
|
||||
|
||||
// 从LRU链表中移除页面节点
|
||||
pc.lru.Remove(elem)
|
||||
}
|
||||
|
||||
// Invalidate 从缓存中强制移除指定页面
|
||||
//
|
||||
// 使用场景:
|
||||
// - 页面数据已知损坏,需要强制从缓存中移除
|
||||
// - 外部直接修改了磁盘数据,缓存数据已过期
|
||||
// - 内存压力大,需要主动释放特定页面
|
||||
//
|
||||
// 核心算法:直接删除
|
||||
// 时间复杂度:O(1) - HashMap删除 + 链表删除
|
||||
// 空间复杂度:O(1) - 释放一个页面的空间
|
||||
//
|
||||
// 参数说明:
|
||||
// - pageNo: 要移除的页面编号
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取写锁(独占访问)
|
||||
// 2. 检查页面是否存在于缓存中
|
||||
// 3. 如果存在:从HashMap和LRU链表中移除
|
||||
// 4. 如果不存在:静默忽略(幂等操作)
|
||||
//
|
||||
// 注意事项:
|
||||
// - 该操作不检查脏页状态,直接丢弃数据
|
||||
// - 如果页面是脏页,未保存的修改将丢失
|
||||
// - 适用于确定不需要保存数据的场景
|
||||
//
|
||||
// 并发安全:使用写锁保护,确保操作原子性
|
||||
func (pc *PageCache) Invalidate(pageNo uint16) {
|
||||
pc.mu.Lock() // 获取写锁,独占访问缓存
|
||||
defer pc.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 检查页面是否存在于缓存中
|
||||
if entry, exists := pc.cache[pageNo]; exists {
|
||||
// 从HashMap中移除页面映射
|
||||
delete(pc.cache, pageNo)
|
||||
|
||||
// 从LRU链表中移除页面节点
|
||||
pc.lru.Remove(entry.elem)
|
||||
|
||||
// 注意:这里不检查脏页状态,直接丢弃
|
||||
// 如果需要保护脏页,应该在删除前检查entry.dirty
|
||||
}
|
||||
// 如果页面不存在,静默忽略(幂等操作)
|
||||
}
|
||||
|
||||
// Flush 将所有脏页刷新到磁盘
|
||||
//
|
||||
// 使用场景:
|
||||
// - 数据库关闭前确保所有修改都已保存
|
||||
// - 定期检查点,将内存中的修改持久化
|
||||
// - 内存压力大,主动释放脏页占用的内存
|
||||
//
|
||||
// 核心算法:遍历所有缓存条目,写回脏页
|
||||
// 时间复杂度:O(n) - n为缓存中的页面数量
|
||||
// 空间复杂度:O(1) - 不创建额外的数据结构
|
||||
//
|
||||
// 参数说明:
|
||||
// - writeFunc: 写回函数,由调用者提供具体的磁盘写入逻辑
|
||||
// 函数签名:func(pageNo uint16, p []byte) error
|
||||
// 参数:pageNo=页面编号,p=页面数据
|
||||
// 返回:error=写入错误(nil表示成功)
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 第一个写回失败的错误(如果有)
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取写锁(独占访问)
|
||||
// 2. 遍历所有缓存条目
|
||||
// 3. 对于每个脏页:
|
||||
// a. 调用writeFunc写回磁盘
|
||||
// b. 如果写回成功:清除脏页标记
|
||||
// c. 如果写回失败:立即返回错误
|
||||
// 4. 所有脏页写回成功后返回nil
|
||||
//
|
||||
// 错误处理:
|
||||
// - 遇到第一个写回错误时立即停止并返回
|
||||
// - 已成功写回的页面会清除脏页标记
|
||||
// - 失败的页面保持脏页状态,可以稍后重试
|
||||
//
|
||||
// 并发安全:使用写锁保护,确保刷新过程中缓存状态一致
|
||||
func (pc *PageCache) Flush(writeFunc func(pageNo uint16, p []byte) error) error {
|
||||
pc.mu.Lock() // 获取写锁,独占访问缓存
|
||||
defer pc.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 遍历所有缓存条目,查找脏页
|
||||
for pageNo, entry := range pc.cache {
|
||||
// 只处理脏页(已修改但未写回磁盘的页面)
|
||||
if entry.dirty {
|
||||
// 调用外部提供的写回函数
|
||||
if err := writeFunc(pageNo, entry.page); err != nil {
|
||||
// 写回失败:立即返回错误
|
||||
// 注意:已成功写回的页面会保持clean状态
|
||||
return err
|
||||
}
|
||||
|
||||
// 写回成功:清除脏页标记
|
||||
entry.dirty = false
|
||||
}
|
||||
}
|
||||
|
||||
// 所有脏页都成功写回
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stats 获取缓存性能统计信息
|
||||
//
|
||||
// 用途:
|
||||
// - 监控缓存性能和效果
|
||||
// - 调优缓存大小和策略
|
||||
// - 诊断性能问题
|
||||
//
|
||||
// 核心算法:简单的统计计算
|
||||
// 时间复杂度:O(1) - 直接读取计数器
|
||||
// 空间复杂度:O(1) - 不创建额外数据
|
||||
//
|
||||
// 返回值:
|
||||
// - hits: 缓存命中次数(成功从缓存获取页面的次数)
|
||||
// - misses: 缓存未命中次数(需要从磁盘加载页面的次数)
|
||||
// - hitRate: 缓存命中率(hits / (hits + misses))
|
||||
//
|
||||
// 命中率解读:
|
||||
// - 0.0 - 1.0:命中率范围
|
||||
// - 0.9+:优秀的缓存效果
|
||||
// - 0.7-0.9:良好的缓存效果
|
||||
// - 0.5-0.7:一般的缓存效果
|
||||
// - <0.5:较差的缓存效果,可能需要增加缓存大小
|
||||
//
|
||||
// 并发安全:使用读锁保护,支持并发读取统计信息
|
||||
func (pc *PageCache) Stats() (hits int64, misses int64, hitRate float64) {
|
||||
pc.mu.RLock() // 获取读锁,允许并发读取
|
||||
defer pc.mu.RUnlock() // 确保函数退出时释放锁
|
||||
|
||||
// 读取命中和未命中计数
|
||||
hits = pc.hits
|
||||
misses = pc.misses
|
||||
total := hits + misses
|
||||
|
||||
// 计算命中率,避免除零错误
|
||||
if total > 0 {
|
||||
hitRate = float64(hits) / float64(total)
|
||||
} else {
|
||||
hitRate = 0.0 // 没有访问记录时命中率为0
|
||||
}
|
||||
|
||||
return hits, misses, hitRate
|
||||
}
|
||||
|
||||
// Size 获取当前缓存中的页面数量
|
||||
//
|
||||
// 用途:
|
||||
// - 监控缓存使用情况
|
||||
// - 检查缓存是否接近容量上限
|
||||
// - 内存使用量估算
|
||||
//
|
||||
// 核心算法:直接返回HashMap大小
|
||||
// 时间复杂度:O(1) - 直接读取map长度
|
||||
// 空间复杂度:O(1) - 不创建额外数据
|
||||
//
|
||||
// 返回值:
|
||||
// - int: 当前缓存中的页面数量(0 <= size <= capacity)
|
||||
//
|
||||
// 使用示例:
|
||||
// - 内存使用量 ≈ Size() * 页面大小
|
||||
// - 缓存利用率 = Size() / Capacity()
|
||||
//
|
||||
// 并发安全:使用读锁保护,支持并发读取
|
||||
func (pc *PageCache) Size() int {
|
||||
pc.mu.RLock() // 获取读锁,允许并发读取
|
||||
defer pc.mu.RUnlock() // 确保函数退出时释放锁
|
||||
|
||||
// 返回HashMap中的条目数量,即缓存的页面数
|
||||
return len(pc.cache)
|
||||
}
|
||||
|
||||
// Clear 清空整个缓存
|
||||
//
|
||||
// 使用场景:
|
||||
// - 系统重启或重新初始化
|
||||
// - 内存压力极大,需要释放所有缓存
|
||||
// - 测试环境中重置缓存状态
|
||||
// - 数据库结构发生重大变化
|
||||
//
|
||||
// 核心算法:重新初始化所有数据结构
|
||||
// 时间复杂度:O(1) - 直接创建新的数据结构
|
||||
// 空间复杂度:O(1) - 释放所有缓存数据
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取写锁(独占访问)
|
||||
// 2. 重新创建空的HashMap
|
||||
// 3. 重新创建空的LRU链表
|
||||
// 4. 重置所有统计计数器
|
||||
//
|
||||
// 注意事项:
|
||||
// - 该操作会丢失所有脏页数据
|
||||
// - 清空后所有页面访问都会导致缓存未命中
|
||||
// - 适用于确定不需要保存任何缓存数据的场景
|
||||
//
|
||||
// 并发安全:使用写锁保护,确保清空操作原子性
|
||||
func (pc *PageCache) Clear() {
|
||||
pc.mu.Lock() // 获取写锁,独占访问缓存
|
||||
defer pc.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 重新创建空的HashMap,释放所有页面数据
|
||||
pc.cache = make(map[uint16]*cacheEntry)
|
||||
|
||||
// 重新创建空的LRU链表,释放所有链表节点
|
||||
pc.lru = list.New()
|
||||
|
||||
// 重置统计计数器
|
||||
pc.hits = 0
|
||||
pc.misses = 0
|
||||
|
||||
// 注意:capacity保持不变,因为它是缓存的配置参数
|
||||
}
|
||||
541
cache_test.go
Normal file
541
cache_test.go
Normal file
@@ -0,0 +1,541 @@
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestNewPageCache 测试页面缓存的创建
|
||||
func TestNewPageCache(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
capacity int
|
||||
wantSize int
|
||||
}{
|
||||
{
|
||||
name: "创建小容量缓存",
|
||||
capacity: 10,
|
||||
wantSize: 0,
|
||||
},
|
||||
{
|
||||
name: "创建中等容量缓存",
|
||||
capacity: 100,
|
||||
wantSize: 0,
|
||||
},
|
||||
{
|
||||
name: "创建大容量缓存",
|
||||
capacity: 1000,
|
||||
wantSize: 0,
|
||||
},
|
||||
{
|
||||
name: "创建零容量缓存",
|
||||
capacity: 0,
|
||||
wantSize: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cache := NewPageCache(tt.capacity)
|
||||
|
||||
if cache == nil {
|
||||
t.Fatal("NewPageCache returned nil")
|
||||
}
|
||||
|
||||
if cache.capacity != tt.capacity {
|
||||
t.Errorf("capacity = %d, want %d", cache.capacity, tt.capacity)
|
||||
}
|
||||
|
||||
if cache.Size() != tt.wantSize {
|
||||
t.Errorf("initial size = %d, want %d", cache.Size(), tt.wantSize)
|
||||
}
|
||||
|
||||
// 验证统计信息初始状态
|
||||
hits, misses, hitRate := cache.Stats()
|
||||
if hits != 0 || misses != 0 || hitRate != 0.0 {
|
||||
t.Errorf("initial stats: hits=%d, misses=%d, hitRate=%f, want all zero", hits, misses, hitRate)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageCachePutGet 测试基本的Put和Get操作
|
||||
func TestPageCachePutGet(t *testing.T) {
|
||||
cache := NewPageCache(10)
|
||||
|
||||
// 测试数据
|
||||
testData := []byte("test page data")
|
||||
pageNo := uint16(1)
|
||||
|
||||
// 测试Put操作
|
||||
cache.Put(pageNo, testData, false)
|
||||
|
||||
// 验证缓存大小
|
||||
if cache.Size() != 1 {
|
||||
t.Errorf("size after put = %d, want 1", cache.Size())
|
||||
}
|
||||
|
||||
// 测试Get操作
|
||||
data, found := cache.Get(pageNo)
|
||||
if !found {
|
||||
t.Fatal("Get returned false, want true")
|
||||
}
|
||||
|
||||
if string(data) != string(testData) {
|
||||
t.Errorf("Get returned %s, want %s", string(data), string(testData))
|
||||
}
|
||||
|
||||
// 验证统计信息
|
||||
hits, misses, hitRate := cache.Stats()
|
||||
if hits != 1 || misses != 0 {
|
||||
t.Errorf("stats after get: hits=%d, misses=%d, want hits=1, misses=0", hits, misses)
|
||||
}
|
||||
if hitRate != 1.0 {
|
||||
t.Errorf("hit rate = %f, want 1.0", hitRate)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageCacheGetMiss 测试缓存未命中的情况
|
||||
func TestPageCacheGetMiss(t *testing.T) {
|
||||
cache := NewPageCache(10)
|
||||
|
||||
// 尝试获取不存在的页面
|
||||
data, found := cache.Get(999)
|
||||
if found {
|
||||
t.Error("Get returned true for non-existent page, want false")
|
||||
}
|
||||
if data != nil {
|
||||
t.Error("Get returned non-nil data for non-existent page, want nil")
|
||||
}
|
||||
|
||||
// 验证统计信息
|
||||
hits, misses, hitRate := cache.Stats()
|
||||
if hits != 0 || misses != 1 {
|
||||
t.Errorf("stats after miss: hits=%d, misses=%d, want hits=0, misses=1", hits, misses)
|
||||
}
|
||||
if hitRate != 0.0 {
|
||||
t.Errorf("hit rate = %f, want 0.0", hitRate)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageCacheLRUEviction 测试LRU淘汰机制
|
||||
func TestPageCacheLRUEviction(t *testing.T) {
|
||||
cache := NewPageCache(3) // 小容量缓存便于测试淘汰
|
||||
|
||||
// 添加3个页面,填满缓存
|
||||
for i := uint16(1); i <= 3; i++ {
|
||||
data := []byte("page " + string(rune('0'+i)))
|
||||
cache.Put(i, data, false)
|
||||
}
|
||||
|
||||
// 验证缓存已满
|
||||
if cache.Size() != 3 {
|
||||
t.Errorf("size after filling cache = %d, want 3", cache.Size())
|
||||
}
|
||||
|
||||
// 访问页面1,使其成为最近使用的
|
||||
cache.Get(1)
|
||||
|
||||
// 添加第4个页面,应该淘汰页面2(最久未使用)
|
||||
cache.Put(4, []byte("page 4"), false)
|
||||
|
||||
// 验证缓存大小仍为3
|
||||
if cache.Size() != 3 {
|
||||
t.Errorf("size after eviction = %d, want 3", cache.Size())
|
||||
}
|
||||
|
||||
// 验证页面1仍然存在(最近访问过)
|
||||
_, found := cache.Get(1)
|
||||
if !found {
|
||||
t.Error("page 1 was evicted, but it should be kept (recently accessed)")
|
||||
}
|
||||
|
||||
// 验证页面2被淘汰
|
||||
_, found = cache.Get(2)
|
||||
if found {
|
||||
t.Error("page 2 should be evicted")
|
||||
}
|
||||
|
||||
// 验证页面3和4仍然存在
|
||||
_, found = cache.Get(3)
|
||||
if !found {
|
||||
t.Error("page 3 should still exist")
|
||||
}
|
||||
|
||||
_, found = cache.Get(4)
|
||||
if !found {
|
||||
t.Error("page 4 should exist (just added)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageCacheDirtyPages 测试脏页管理
|
||||
func TestPageCacheDirtyPages(t *testing.T) {
|
||||
cache := NewPageCache(10)
|
||||
|
||||
// 添加一个干净页面
|
||||
cache.Put(1, []byte("clean page"), false)
|
||||
|
||||
// 添加一个脏页面
|
||||
cache.Put(2, []byte("dirty page"), true)
|
||||
|
||||
// 更新页面1为脏页
|
||||
cache.Put(1, []byte("updated page"), true)
|
||||
|
||||
// 验证缓存大小
|
||||
if cache.Size() != 2 {
|
||||
t.Errorf("size = %d, want 2", cache.Size())
|
||||
}
|
||||
|
||||
// 测试Flush操作
|
||||
flushedPages := make(map[uint16][]byte)
|
||||
flushFunc := func(pageNo uint16, data []byte) error {
|
||||
flushedPages[pageNo] = make([]byte, len(data))
|
||||
copy(flushedPages[pageNo], data)
|
||||
return nil
|
||||
}
|
||||
|
||||
err := cache.Flush(flushFunc)
|
||||
if err != nil {
|
||||
t.Errorf("Flush returned error: %v", err)
|
||||
}
|
||||
|
||||
// 验证脏页被刷新
|
||||
if len(flushedPages) != 2 {
|
||||
t.Errorf("flushed %d pages, want 2", len(flushedPages))
|
||||
}
|
||||
|
||||
if string(flushedPages[1]) != "updated page" {
|
||||
t.Errorf("flushed page 1 = %s, want 'updated page'", string(flushedPages[1]))
|
||||
}
|
||||
|
||||
if string(flushedPages[2]) != "dirty page" {
|
||||
t.Errorf("flushed page 2 = %s, want 'dirty page'", string(flushedPages[2]))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageCacheInvalidate 测试页面失效操作
|
||||
func TestPageCacheInvalidate(t *testing.T) {
|
||||
cache := NewPageCache(10)
|
||||
|
||||
// 添加几个页面
|
||||
cache.Put(1, []byte("page 1"), false)
|
||||
cache.Put(2, []byte("page 2"), true)
|
||||
cache.Put(3, []byte("page 3"), false)
|
||||
|
||||
// 验证初始状态
|
||||
if cache.Size() != 3 {
|
||||
t.Errorf("initial size = %d, want 3", cache.Size())
|
||||
}
|
||||
|
||||
// 使页面2失效
|
||||
cache.Invalidate(2)
|
||||
|
||||
// 验证页面2被移除
|
||||
if cache.Size() != 2 {
|
||||
t.Errorf("size after invalidate = %d, want 2", cache.Size())
|
||||
}
|
||||
|
||||
_, found := cache.Get(2)
|
||||
if found {
|
||||
t.Error("invalidated page 2 should not be found")
|
||||
}
|
||||
|
||||
// 验证其他页面仍然存在
|
||||
_, found = cache.Get(1)
|
||||
if !found {
|
||||
t.Error("page 1 should still exist")
|
||||
}
|
||||
|
||||
_, found = cache.Get(3)
|
||||
if !found {
|
||||
t.Error("page 3 should still exist")
|
||||
}
|
||||
|
||||
// 测试失效不存在的页面(应该是幂等操作)
|
||||
cache.Invalidate(999)
|
||||
if cache.Size() != 2 {
|
||||
t.Errorf("size after invalidating non-existent page = %d, want 2", cache.Size())
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageCacheClear 测试清空缓存操作
|
||||
func TestPageCacheClear(t *testing.T) {
|
||||
cache := NewPageCache(10)
|
||||
|
||||
// 添加一些页面
|
||||
for i := uint16(1); i <= 5; i++ {
|
||||
cache.Put(i, []byte("page"), false)
|
||||
}
|
||||
|
||||
// 产生一些统计数据
|
||||
cache.Get(1)
|
||||
cache.Get(999) // miss
|
||||
|
||||
// 验证初始状态
|
||||
if cache.Size() != 5 {
|
||||
t.Errorf("initial size = %d, want 5", cache.Size())
|
||||
}
|
||||
|
||||
hits, misses, _ := cache.Stats()
|
||||
if hits == 0 && misses == 0 {
|
||||
t.Error("should have some stats before clear")
|
||||
}
|
||||
|
||||
// 清空缓存
|
||||
cache.Clear()
|
||||
|
||||
// 验证缓存被清空
|
||||
if cache.Size() != 0 {
|
||||
t.Errorf("size after clear = %d, want 0", cache.Size())
|
||||
}
|
||||
|
||||
// 验证统计信息被重置
|
||||
hits, misses, hitRate := cache.Stats()
|
||||
if hits != 0 || misses != 0 || hitRate != 0.0 {
|
||||
t.Errorf("stats after clear: hits=%d, misses=%d, hitRate=%f, want all zero", hits, misses, hitRate)
|
||||
}
|
||||
|
||||
// 验证页面不再存在
|
||||
_, found := cache.Get(1)
|
||||
if found {
|
||||
t.Error("page should not exist after clear")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageCacheDataIsolation 测试数据隔离(副本机制)
|
||||
func TestPageCacheDataIsolation(t *testing.T) {
|
||||
cache := NewPageCache(10)
|
||||
|
||||
// 原始数据
|
||||
originalData := []byte("original data")
|
||||
pageNo := uint16(1)
|
||||
|
||||
// 存储数据
|
||||
cache.Put(pageNo, originalData, false)
|
||||
|
||||
// 修改原始数据
|
||||
originalData[0] = 'X'
|
||||
|
||||
// 获取缓存数据
|
||||
cachedData, found := cache.Get(pageNo)
|
||||
if !found {
|
||||
t.Fatal("page not found in cache")
|
||||
}
|
||||
|
||||
// 验证缓存数据未被修改
|
||||
if cachedData[0] == 'X' {
|
||||
t.Error("cached data was modified when original data changed")
|
||||
}
|
||||
|
||||
if string(cachedData) != "original data" {
|
||||
t.Errorf("cached data = %s, want 'original data'", string(cachedData))
|
||||
}
|
||||
|
||||
// 修改获取到的数据
|
||||
cachedData[0] = 'Y'
|
||||
|
||||
// 再次获取,验证缓存中的数据未被修改
|
||||
cachedData2, found := cache.Get(pageNo)
|
||||
if !found {
|
||||
t.Fatal("page not found in cache")
|
||||
}
|
||||
|
||||
if cachedData2[0] == 'Y' {
|
||||
t.Error("cached data was modified when returned data changed")
|
||||
}
|
||||
|
||||
if string(cachedData2) != "original data" {
|
||||
t.Errorf("cached data = %s, want 'original data'", string(cachedData2))
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageCacheConcurrency 测试并发安全性
|
||||
func TestPageCacheConcurrency(t *testing.T) {
|
||||
cache := NewPageCache(100)
|
||||
|
||||
const numGoroutines = 10
|
||||
const numOperations = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 启动多个goroutine进行并发操作
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < numOperations; j++ {
|
||||
pageNo := uint16(id*numOperations + j)
|
||||
data := []byte("data from goroutine " + string(rune('0'+id)))
|
||||
|
||||
// 并发Put操作
|
||||
cache.Put(pageNo, data, j%2 == 0) // 一半是脏页
|
||||
|
||||
// 并发Get操作
|
||||
cache.Get(pageNo)
|
||||
|
||||
// 偶尔进行Invalidate操作
|
||||
if j%10 == 0 {
|
||||
cache.Invalidate(pageNo)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// 同时进行统计查询
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 50; i++ {
|
||||
cache.Stats()
|
||||
cache.Size()
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待所有goroutine完成
|
||||
wg.Wait()
|
||||
|
||||
// 验证缓存仍然可用
|
||||
cache.Put(9999, []byte("final test"), false)
|
||||
data, found := cache.Get(9999)
|
||||
if !found || string(data) != "final test" {
|
||||
t.Error("cache corrupted after concurrent operations")
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageCacheStatsAccuracy 测试统计信息的准确性
|
||||
func TestPageCacheStatsAccuracy(t *testing.T) {
|
||||
cache := NewPageCache(10)
|
||||
|
||||
// 执行一系列操作
|
||||
cache.Put(1, []byte("page 1"), false)
|
||||
cache.Put(2, []byte("page 2"), false)
|
||||
|
||||
// 2次命中
|
||||
cache.Get(1)
|
||||
cache.Get(2)
|
||||
|
||||
// 3次未命中
|
||||
cache.Get(3)
|
||||
cache.Get(4)
|
||||
cache.Get(5)
|
||||
|
||||
// 1次命中
|
||||
cache.Get(1)
|
||||
|
||||
// 验证统计信息
|
||||
hits, misses, hitRate := cache.Stats()
|
||||
|
||||
expectedHits := int64(3)
|
||||
expectedMisses := int64(3)
|
||||
expectedHitRate := float64(3) / float64(6)
|
||||
|
||||
if hits != expectedHits {
|
||||
t.Errorf("hits = %d, want %d", hits, expectedHits)
|
||||
}
|
||||
|
||||
if misses != expectedMisses {
|
||||
t.Errorf("misses = %d, want %d", misses, expectedMisses)
|
||||
}
|
||||
|
||||
if hitRate != expectedHitRate {
|
||||
t.Errorf("hit rate = %f, want %f", hitRate, expectedHitRate)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageCacheUpdateExisting 测试更新已存在页面
|
||||
func TestPageCacheUpdateExisting(t *testing.T) {
|
||||
cache := NewPageCache(10)
|
||||
|
||||
pageNo := uint16(1)
|
||||
|
||||
// 添加初始页面
|
||||
cache.Put(pageNo, []byte("original"), false)
|
||||
|
||||
// 验证初始数据
|
||||
data, found := cache.Get(pageNo)
|
||||
if !found || string(data) != "original" {
|
||||
t.Errorf("initial data = %s, want 'original'", string(data))
|
||||
}
|
||||
|
||||
// 更新页面数据
|
||||
cache.Put(pageNo, []byte("updated"), true)
|
||||
|
||||
// 验证缓存大小没有增加
|
||||
if cache.Size() != 1 {
|
||||
t.Errorf("size after update = %d, want 1", cache.Size())
|
||||
}
|
||||
|
||||
// 验证数据已更新
|
||||
data, found = cache.Get(pageNo)
|
||||
if !found || string(data) != "updated" {
|
||||
t.Errorf("updated data = %s, want 'updated'", string(data))
|
||||
}
|
||||
|
||||
// 验证脏页标记
|
||||
flushedCount := 0
|
||||
flushFunc := func(pageNo uint16, data []byte) error {
|
||||
flushedCount++
|
||||
return nil
|
||||
}
|
||||
|
||||
cache.Flush(flushFunc)
|
||||
if flushedCount != 1 {
|
||||
t.Errorf("flushed %d pages, want 1 (dirty page)", flushedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPageCacheGet 性能测试:Get操作
|
||||
func BenchmarkPageCacheGet(b *testing.B) {
|
||||
cache := NewPageCache(1000)
|
||||
|
||||
// 预填充缓存
|
||||
for i := uint16(0); i < 1000; i++ {
|
||||
cache.Put(i, []byte("benchmark data"), false)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.Get(uint16(i % 1000))
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPageCachePut 性能测试:Put操作
|
||||
func BenchmarkPageCachePut(b *testing.B) {
|
||||
cache := NewPageCache(1000)
|
||||
data := []byte("benchmark data")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.Put(uint16(i%1000), data, false)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkPageCacheConcurrentAccess 性能测试:并发访问
|
||||
func BenchmarkPageCacheConcurrentAccess(b *testing.B) {
|
||||
cache := NewPageCache(1000)
|
||||
data := []byte("benchmark data")
|
||||
|
||||
// 预填充缓存
|
||||
for i := uint16(0); i < 1000; i++ {
|
||||
cache.Put(i, data, false)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
if i%2 == 0 {
|
||||
cache.Get(uint16(i % 1000))
|
||||
} else {
|
||||
cache.Put(uint16(i%1000), data, false)
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
182
examples/README.md
Normal file
182
examples/README.md
Normal file
@@ -0,0 +1,182 @@
|
||||
# Pipeline Database V4 示例
|
||||
|
||||
本目录包含了 Pipeline Database V4 的各种使用示例,展示了不同场景下的应用方法。
|
||||
|
||||
## 示例列表
|
||||
|
||||
### 1. 基础使用 (`basic-usage/`)
|
||||
演示 Pipeline Database 的基本功能:
|
||||
- 数据库初始化和配置
|
||||
- 数据插入和查询
|
||||
- 基本统计信息获取
|
||||
- **Handler**: `LoggingHandler` (简单日志处理)
|
||||
|
||||
### 2. 组管理 (`group-management/`)
|
||||
展示分组管理功能:
|
||||
- 多组数据管理
|
||||
- 按组查询和统计
|
||||
- 跨组数据分析
|
||||
- **Handler**: `ExampleHandler` (完整示例处理器)
|
||||
|
||||
### 3. 外部处理器 (`external-handler/`)
|
||||
演示自定义外部处理器:
|
||||
- 实现 Handler 接口
|
||||
- 热数据预热处理
|
||||
- 温数据冷却处理
|
||||
- 组完成回调
|
||||
- **Handler**: 自定义 `EmailHandler`
|
||||
|
||||
### 4. 数据分析 (`data-analytics/`)
|
||||
展示数据分析和报告功能:
|
||||
- 用户行为分析
|
||||
- 统计报告生成
|
||||
- 数据可视化
|
||||
- **Handler**: `ExampleHandler` (数据分析处理器)
|
||||
|
||||
### 5. 并发处理 (`concurrent-processing/`)
|
||||
演示并发数据处理:
|
||||
- 多生产者并发写入
|
||||
- 实时统计监控
|
||||
- 并发查询测试
|
||||
- **Handler**: `ExampleHandler` (并发处理器)
|
||||
|
||||
### 6. 高并发压力测试 (`high-concurrency/`)
|
||||
演示极限并发性能:
|
||||
- 50个写入goroutine + 20个读取goroutine
|
||||
- 目标1000写入QPS + 500读取QPS
|
||||
- 30秒压力测试
|
||||
- 性能评估和报告
|
||||
- **Handler**: `LoggingHandler` (轻量级处理)
|
||||
|
||||
## Handler 配置说明
|
||||
|
||||
所有示例现在都配置了适当的 Handler,避免了空指针异常:
|
||||
|
||||
### LoggingHandler
|
||||
- **用途**: 简单的日志输出,适合基础示例
|
||||
- **特点**: 轻量级,不做复杂处理
|
||||
- **使用场景**: 基础使用、高并发测试
|
||||
|
||||
### ExampleHandler
|
||||
- **用途**: 完整的示例处理器,包含详细日志
|
||||
- **特点**: 展示完整的处理流程
|
||||
- **使用场景**: 组管理、数据分析、并发处理
|
||||
|
||||
### 自定义Handler
|
||||
- **用途**: 特定业务逻辑处理
|
||||
- **特点**: 根据具体需求实现
|
||||
- **使用场景**: 外部处理器示例
|
||||
|
||||
## 运行示例
|
||||
|
||||
每个示例都是独立的,可以直接运行:
|
||||
|
||||
```bash
|
||||
# 基础使用示例
|
||||
```
|
||||
|
||||
2. **运行任意示例**:
|
||||
```bash
|
||||
cd basic-usage
|
||||
go run main.go
|
||||
```
|
||||
|
||||
3. **查看输出**: 每个示例都会产生详细的输出,展示各个步骤的执行过程
|
||||
|
||||
## 📚 示例特点
|
||||
|
||||
### 🎯 实用性
|
||||
- 每个示例都模拟真实的业务场景
|
||||
- 包含完整的错误处理
|
||||
- 提供详细的日志输出
|
||||
|
||||
### 🔧 可定制性
|
||||
- 易于修改和扩展
|
||||
- 清晰的代码结构
|
||||
- 丰富的注释说明
|
||||
|
||||
### 📊 教育性
|
||||
- 渐进式学习路径
|
||||
- 从基础到高级功能
|
||||
- 最佳实践演示
|
||||
|
||||
## 💡 使用建议
|
||||
|
||||
### 学习路径
|
||||
1. **新手**: 从 `basic-usage` 开始
|
||||
2. **进阶**: 学习 `external-handler` 和 `group-management`
|
||||
3. **高级**: 研究 `concurrent-processing` 和 `data-analytics`
|
||||
|
||||
### 生产环境适配
|
||||
- 修改数据库文件路径
|
||||
- 调整缓存大小配置
|
||||
- 实现自定义的外部处理器
|
||||
- 添加适当的监控和日志
|
||||
|
||||
### 性能优化
|
||||
- 根据数据量调整 `CacheSize`
|
||||
- 合理设计组结构
|
||||
- 优化查询分页大小
|
||||
- 监控系统资源使用
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 数据库配置
|
||||
```go
|
||||
config := &pipelinedb.Config{
|
||||
CacheSize: 100, // 缓存页面数量
|
||||
}
|
||||
```
|
||||
|
||||
### 打开选项
|
||||
```go
|
||||
pdb, err := pipelinedb.Open(pipelinedb.Options{
|
||||
Filename: "database.db", // 数据库文件路径
|
||||
Config: config, // 配置选项
|
||||
Handler: yourHandler, // 外部处理器(可选)
|
||||
Logger: yourLogger, // 日志器(可选)
|
||||
})
|
||||
```
|
||||
|
||||
## 🐛 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **文件权限错误**
|
||||
- 确保对数据库文件目录有写权限
|
||||
- 检查临时文件创建权限
|
||||
|
||||
2. **内存不足**
|
||||
- 减少 `CacheSize` 配置
|
||||
- 优化数据处理逻辑
|
||||
|
||||
3. **并发冲突**
|
||||
- 检查并发访问模式
|
||||
- 确保正确的错误处理
|
||||
|
||||
### 调试技巧
|
||||
- 启用详细日志输出
|
||||
- 使用分步调试
|
||||
- 监控系统资源使用
|
||||
- 检查数据库统计信息
|
||||
|
||||
## 📞 获取帮助
|
||||
|
||||
如果你在使用示例时遇到问题:
|
||||
|
||||
1. 查看示例代码中的注释
|
||||
2. 检查错误信息和日志输出
|
||||
3. 参考 Pipeline Database V4 的文档
|
||||
4. 在项目仓库提交 Issue
|
||||
|
||||
## 🎉 贡献
|
||||
|
||||
欢迎贡献新的示例!请确保:
|
||||
- 代码清晰易懂
|
||||
- 包含详细注释
|
||||
- 模拟真实场景
|
||||
- 添加到此 README
|
||||
|
||||
---
|
||||
|
||||
**Pipeline Database V4** - 高性能的管道式数据库系统
|
||||
9
examples/basic-usage/go.mod
Normal file
9
examples/basic-usage/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module basic-usage
|
||||
|
||||
go 1.24.0
|
||||
|
||||
replace code.tczkiot.com/wlw/pipelinedb => ../../
|
||||
|
||||
require code.tczkiot.com/wlw/pipelinedb v0.0.0-00010101000000-000000000000
|
||||
|
||||
require github.com/google/btree v1.1.3 // indirect
|
||||
2
examples/basic-usage/go.sum
Normal file
2
examples/basic-usage/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
121
examples/basic-usage/main.go
Normal file
121
examples/basic-usage/main.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// 演示 Pipeline Database 的基础使用
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"code.tczkiot.com/wlw/pipelinedb"
|
||||
"code.tczkiot.com/wlw/pipelinedb/examples/common"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建临时数据库文件
|
||||
dbFile := "basic_example.db"
|
||||
defer os.Remove(dbFile) // 清理临时文件
|
||||
|
||||
// 确保文件可以创建
|
||||
if _, err := os.Create(dbFile); err != nil {
|
||||
log.Fatalf("创建数据库文件失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("🚀 Pipeline Database 基础使用示例")
|
||||
fmt.Println("================================")
|
||||
|
||||
// 1. 打开数据库
|
||||
fmt.Println("\n📂 步骤1: 打开数据库")
|
||||
config := &pipelinedb.Config{
|
||||
CacheSize: 100, // 缓存100个页面
|
||||
}
|
||||
|
||||
// 创建处理器
|
||||
handler := common.NewLoggingHandler()
|
||||
|
||||
pdb, err := pipelinedb.Open(pipelinedb.Options{
|
||||
Filename: dbFile,
|
||||
Config: config,
|
||||
Handler: handler,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("打开数据库失败: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
fmt.Printf("✅ 数据库已打开: %s\n", dbFile)
|
||||
|
||||
// 2. 接收数据
|
||||
fmt.Println("\n📥 步骤2: 接收数据")
|
||||
testData := []struct {
|
||||
group string
|
||||
data []byte
|
||||
metadata string
|
||||
}{
|
||||
{"用户行为", []byte("用户点击了首页按钮"), `{"timestamp": "2024-01-01T10:00:00Z", "user_id": "user123"}`},
|
||||
{"用户行为", []byte("用户浏览了产品页面"), `{"timestamp": "2024-01-01T10:01:00Z", "user_id": "user123"}`},
|
||||
{"系统日志", []byte("系统启动完成"), `{"timestamp": "2024-01-01T09:00:00Z", "level": "info"}`},
|
||||
{"系统日志", []byte("数据库连接建立"), `{"timestamp": "2024-01-01T09:01:00Z", "level": "info"}`},
|
||||
{"错误日志", []byte("网络连接超时"), `{"timestamp": "2024-01-01T10:30:00Z", "level": "error"}`},
|
||||
}
|
||||
|
||||
var recordIDs []int64
|
||||
for _, item := range testData {
|
||||
recordID, err := pdb.AcceptData(item.group, item.data, item.metadata)
|
||||
if err != nil {
|
||||
log.Fatalf("接收数据失败: %v", err)
|
||||
}
|
||||
recordIDs = append(recordIDs, recordID)
|
||||
fmt.Printf(" ✅ 记录 %d: [%s] %s\n", recordID, item.group, string(item.data))
|
||||
}
|
||||
|
||||
// 3. 查询数据
|
||||
fmt.Println("\n🔍 步骤3: 查询数据")
|
||||
|
||||
// 按组查询
|
||||
groups := []string{"用户行为", "系统日志", "错误日志"}
|
||||
for _, group := range groups {
|
||||
fmt.Printf("\n📋 查询组: %s\n", group)
|
||||
|
||||
pageReq := &pipelinedb.PageRequest{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
response, err := pdb.GetRecordsByGroup(group, pageReq)
|
||||
if err != nil {
|
||||
fmt.Printf(" ❌ 查询失败: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" 📊 总记录数: %d, 当前页: %d/%d\n",
|
||||
response.TotalCount, response.Page, response.TotalPages)
|
||||
|
||||
for _, record := range response.Records {
|
||||
fmt.Printf(" 📄 ID:%d [%s] %s (创建时间: %s)\n",
|
||||
record.ID, record.Status, string(record.Data),
|
||||
record.CreatedAt.Format("15:04:05"))
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 获取统计信息
|
||||
fmt.Println("\n📊 步骤4: 获取统计信息")
|
||||
stats, err := pdb.GetStats()
|
||||
if err != nil {
|
||||
log.Fatalf("获取统计信息失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("📈 数据库统计信息:\n")
|
||||
fmt.Printf(" 总记录数: %d\n", stats.TotalRecords)
|
||||
fmt.Printf(" 总组数: %d\n", len(stats.GroupStats))
|
||||
|
||||
fmt.Println("\n📋 各组统计:")
|
||||
for group, groupStats := range stats.GroupStats {
|
||||
fmt.Printf(" %s:\n", group)
|
||||
fmt.Printf(" 热数据: %d\n", groupStats.HotRecords)
|
||||
fmt.Printf(" 温数据: %d\n", groupStats.WarmRecords)
|
||||
fmt.Printf(" 冷数据: %d\n", groupStats.ColdRecords)
|
||||
fmt.Printf(" 总计: %d\n", groupStats.TotalRecords)
|
||||
}
|
||||
|
||||
fmt.Println("\n🎉 基础使用示例完成!")
|
||||
}
|
||||
72
examples/common/handler.go
Normal file
72
examples/common/handler.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// 通用示例处理器
|
||||
package common
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
)
|
||||
|
||||
// ExampleHandler 示例外部处理器
|
||||
type ExampleHandler struct {
|
||||
name string
|
||||
}
|
||||
|
||||
// NewExampleHandler 创建示例处理器
|
||||
func NewExampleHandler(name string) *ExampleHandler {
|
||||
return &ExampleHandler{
|
||||
name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// WillWarm 热数据预热处理
|
||||
func (h *ExampleHandler) WillWarm(ctx context.Context, group string, data []byte) ([]byte, error) {
|
||||
log.Printf("🔥 [%s] 预热处理, 组=%s, 数据大小=%d bytes", h.name, group, len(data))
|
||||
|
||||
// 示例:可以在这里进行数据预处理、验证、转换等
|
||||
// 这里简单返回原数据
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// WillCold 温数据冷却处理
|
||||
func (h *ExampleHandler) WillCold(ctx context.Context, group string, data []byte) ([]byte, error) {
|
||||
log.Printf("❄️ [%s] 冷却处理, 组=%s, 数据大小=%d bytes", h.name, group, len(data))
|
||||
|
||||
// 示例:可以在这里进行数据压缩、归档、清理等
|
||||
// 这里简单返回原数据
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// OnComplete 组完成回调
|
||||
func (h *ExampleHandler) OnComplete(ctx context.Context, group string) error {
|
||||
log.Printf("🎉 [%s] 组完成回调, 组=%s - 所有数据已处理完成", h.name, group)
|
||||
|
||||
// 示例:可以在这里进行组级别的清理、通知、统计等
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoggingHandler 简单的日志处理器(用于不需要复杂处理的示例)
|
||||
type LoggingHandler struct{}
|
||||
|
||||
// NewLoggingHandler 创建日志处理器
|
||||
func NewLoggingHandler() *LoggingHandler {
|
||||
return &LoggingHandler{}
|
||||
}
|
||||
|
||||
// WillWarm 热数据预热处理
|
||||
func (h *LoggingHandler) WillWarm(ctx context.Context, group string, data []byte) ([]byte, error) {
|
||||
fmt.Printf("🔥 数据预热: 组[%s] %d bytes\n", group, len(data))
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// WillCold 温数据冷却处理
|
||||
func (h *LoggingHandler) WillCold(ctx context.Context, group string, data []byte) ([]byte, error) {
|
||||
fmt.Printf("❄️ 数据冷却: 组[%s] %d bytes\n", group, len(data))
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// OnComplete 组完成回调
|
||||
func (h *LoggingHandler) OnComplete(ctx context.Context, group string) error {
|
||||
fmt.Printf("✅ 组完成: [%s]\n", group)
|
||||
return nil
|
||||
}
|
||||
9
examples/concurrent-processing/go.mod
Normal file
9
examples/concurrent-processing/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module concurrent-processing
|
||||
|
||||
go 1.24.0
|
||||
|
||||
replace code.tczkiot.com/wlw/pipelinedb => ../../
|
||||
|
||||
require code.tczkiot.com/wlw/pipelinedb v0.0.0-00010101000000-000000000000
|
||||
|
||||
require github.com/google/btree v1.1.3 // indirect
|
||||
2
examples/concurrent-processing/go.sum
Normal file
2
examples/concurrent-processing/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
229
examples/concurrent-processing/main.go
Normal file
229
examples/concurrent-processing/main.go
Normal file
@@ -0,0 +1,229 @@
|
||||
// 演示并发数据处理
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"code.tczkiot.com/wlw/pipelinedb"
|
||||
"code.tczkiot.com/wlw/pipelinedb/examples/common"
|
||||
)
|
||||
|
||||
// 模拟不同类型的数据源
|
||||
type DataSource struct {
|
||||
name string
|
||||
group string
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 创建临时数据库文件
|
||||
dbFile := "concurrent_example.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
// 确保文件可以创建
|
||||
if _, err := os.Create(dbFile); err != nil {
|
||||
log.Fatalf("创建数据库文件失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("🚀 并发处理示例")
|
||||
fmt.Println("================")
|
||||
|
||||
// 配置数据库
|
||||
fmt.Println("\n📂 步骤1: 配置数据库")
|
||||
config := &pipelinedb.Config{
|
||||
CacheSize: 200, // 增大缓存以支持并发
|
||||
}
|
||||
|
||||
// 创建处理器
|
||||
handler := common.NewExampleHandler("并发处理")
|
||||
|
||||
pdb, err := pipelinedb.Open(pipelinedb.Options{
|
||||
Filename: dbFile,
|
||||
Config: config,
|
||||
Handler: handler,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("打开数据库失败: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
fmt.Println("✅ 数据库已配置")
|
||||
|
||||
// 定义数据源
|
||||
dataSources := []DataSource{
|
||||
{"Web服务器", "访问日志", 100 * time.Millisecond},
|
||||
{"API网关", "API调用", 150 * time.Millisecond},
|
||||
{"数据库", "慢查询", 300 * time.Millisecond},
|
||||
{"缓存系统", "缓存命中", 50 * time.Millisecond},
|
||||
{"消息队列", "消息处理", 200 * time.Millisecond},
|
||||
}
|
||||
|
||||
// 启动并发数据生产者
|
||||
fmt.Println("\n🏭 步骤2: 启动并发数据生产者")
|
||||
var wg sync.WaitGroup
|
||||
stopChan := make(chan bool)
|
||||
|
||||
// 统计计数器
|
||||
var totalRecords int64
|
||||
var mu sync.Mutex
|
||||
|
||||
for i, source := range dataSources {
|
||||
wg.Add(1)
|
||||
go func(id int, src DataSource) {
|
||||
defer wg.Done()
|
||||
|
||||
fmt.Printf("🔄 启动生产者 %d: %s (组: %s)\n", id+1, src.name, src.group)
|
||||
|
||||
count := 0
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
fmt.Printf("🛑 生产者 %d (%s) 停止,共生产 %d 条记录\n",
|
||||
id+1, src.name, count)
|
||||
return
|
||||
default:
|
||||
// 生成模拟数据
|
||||
data := fmt.Sprintf("[%s] 时间戳: %d, 随机值: %d",
|
||||
src.name, time.Now().Unix(), rand.Intn(1000))
|
||||
|
||||
metadata := fmt.Sprintf(`{"source": "%s", "producer_id": %d, "sequence": %d}`,
|
||||
src.name, id+1, count+1)
|
||||
|
||||
// 发送数据
|
||||
recordID, err := pdb.AcceptData(src.group, []byte(data), metadata)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 生产者 %d 发送失败: %v\n", id+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 更新计数
|
||||
mu.Lock()
|
||||
totalRecords++
|
||||
count++
|
||||
mu.Unlock()
|
||||
|
||||
if count%10 == 0 {
|
||||
fmt.Printf("📊 生产者 %d (%s) 已生产 %d 条记录,最新ID: %d\n",
|
||||
id+1, src.name, count, recordID)
|
||||
}
|
||||
|
||||
// 按间隔休眠
|
||||
time.Sleep(src.interval)
|
||||
}
|
||||
}
|
||||
}(i, source)
|
||||
}
|
||||
|
||||
// 启动统计监控
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
// 获取实时统计
|
||||
stats, err := pdb.GetStats()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 获取统计失败: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("\n📈 实时统计 (时间: %s)\n", time.Now().Format("15:04:05"))
|
||||
fmt.Printf(" 数据库总记录: %d\n", stats.TotalRecords)
|
||||
fmt.Printf(" 生产者总计数: %d\n", totalRecords)
|
||||
|
||||
for group, groupStats := range stats.GroupStats {
|
||||
fmt.Printf(" [%s] 热:%d 温:%d 冷:%d 总:%d\n",
|
||||
group, groupStats.HotRecords, groupStats.WarmRecords,
|
||||
groupStats.ColdRecords, groupStats.TotalRecords)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 运行一段时间
|
||||
fmt.Println("\n⏳ 步骤3: 运行并发处理")
|
||||
fmt.Println("正在并发处理数据,运行10秒...")
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
|
||||
// 停止所有生产者
|
||||
fmt.Println("\n🛑 步骤4: 停止生产者")
|
||||
close(stopChan)
|
||||
wg.Wait()
|
||||
|
||||
// 最终统计
|
||||
fmt.Println("\n📊 步骤5: 最终统计")
|
||||
finalStats, err := pdb.GetStats()
|
||||
if err != nil {
|
||||
log.Fatalf("获取最终统计失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("🎯 最终结果:\n")
|
||||
fmt.Printf(" 总记录数: %d\n", finalStats.TotalRecords)
|
||||
fmt.Printf(" 总组数: %d\n", len(finalStats.GroupStats))
|
||||
|
||||
fmt.Println("\n📋 各组详细统计:")
|
||||
for group, groupStats := range finalStats.GroupStats {
|
||||
fmt.Printf(" %s:\n", group)
|
||||
fmt.Printf(" 热数据: %d (%.1f%%)\n",
|
||||
groupStats.HotRecords, float64(groupStats.HotRecords)/float64(groupStats.TotalRecords)*100)
|
||||
fmt.Printf(" 温数据: %d (%.1f%%)\n",
|
||||
groupStats.WarmRecords, float64(groupStats.WarmRecords)/float64(groupStats.TotalRecords)*100)
|
||||
fmt.Printf(" 冷数据: %d (%.1f%%)\n",
|
||||
groupStats.ColdRecords, float64(groupStats.ColdRecords)/float64(groupStats.TotalRecords)*100)
|
||||
fmt.Printf(" 总计: %d\n", groupStats.TotalRecords)
|
||||
}
|
||||
|
||||
// 测试并发查询
|
||||
fmt.Println("\n🔍 步骤6: 测试并发查询")
|
||||
var queryWg sync.WaitGroup
|
||||
|
||||
for _, source := range dataSources {
|
||||
queryWg.Add(1)
|
||||
go func(group string) {
|
||||
defer queryWg.Done()
|
||||
|
||||
pageReq := &pipelinedb.PageRequest{
|
||||
Page: 1,
|
||||
PageSize: 5,
|
||||
}
|
||||
|
||||
response, err := pdb.GetRecordsByGroup(group, pageReq)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ 查询组 %s 失败: %v\n", group, err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("🔍 组 [%s] 查询结果: %d/%d 条记录\n",
|
||||
group, len(response.Records), response.TotalCount)
|
||||
|
||||
for i, record := range response.Records {
|
||||
if i < 2 { // 只显示前2条
|
||||
dataStr := string(record.Data)
|
||||
if len(dataStr) > 50 {
|
||||
dataStr = dataStr[:50]
|
||||
}
|
||||
fmt.Printf(" 📄 ID:%d [%s] %s...\n",
|
||||
record.ID, record.Status, dataStr)
|
||||
}
|
||||
}
|
||||
}(source.group)
|
||||
}
|
||||
|
||||
queryWg.Wait()
|
||||
|
||||
fmt.Println("\n🎉 并发处理示例完成!")
|
||||
fmt.Println("💡 提示: 这个示例展示了Pipeline Database在高并发场景下的稳定性")
|
||||
}
|
||||
9
examples/data-analytics/go.mod
Normal file
9
examples/data-analytics/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module data-analytics
|
||||
|
||||
go 1.24.0
|
||||
|
||||
replace code.tczkiot.com/wlw/pipelinedb => ../../
|
||||
|
||||
require code.tczkiot.com/wlw/pipelinedb v0.0.0-00010101000000-000000000000
|
||||
|
||||
require github.com/google/btree v1.1.3 // indirect
|
||||
2
examples/data-analytics/go.sum
Normal file
2
examples/data-analytics/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
304
examples/data-analytics/main.go
Normal file
304
examples/data-analytics/main.go
Normal file
@@ -0,0 +1,304 @@
|
||||
// 演示数据分析和报告功能
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"code.tczkiot.com/wlw/pipelinedb"
|
||||
"code.tczkiot.com/wlw/pipelinedb/examples/common"
|
||||
)
|
||||
|
||||
// UserEvent 用户事件结构
|
||||
type UserEvent struct {
|
||||
UserID string `json:"user_id"`
|
||||
Action string `json:"action"`
|
||||
Page string `json:"page"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Value float64 `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// AnalyticsReport 分析报告
|
||||
type AnalyticsReport struct {
|
||||
TotalEvents int `json:"total_events"`
|
||||
UniqueUsers int `json:"unique_users"`
|
||||
TopActions []ActionCount `json:"top_actions"`
|
||||
TopPages []PageCount `json:"top_pages"`
|
||||
UserActivity map[string]int `json:"user_activity"`
|
||||
HourlyActivity map[int]int `json:"hourly_activity"`
|
||||
TotalValue float64 `json:"total_value"`
|
||||
}
|
||||
|
||||
type ActionCount struct {
|
||||
Action string `json:"action"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
type PageCount struct {
|
||||
Page string `json:"page"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 创建临时数据库文件
|
||||
dbFile := "analytics_example.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
// 确保文件可以创建
|
||||
if _, err := os.Create(dbFile); err != nil {
|
||||
log.Fatalf("创建数据库文件失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("🚀 数据分析示例")
|
||||
fmt.Println("================")
|
||||
|
||||
// 配置数据库
|
||||
fmt.Println("\n📂 步骤1: 配置数据库")
|
||||
config := &pipelinedb.Config{
|
||||
CacheSize: 100,
|
||||
}
|
||||
|
||||
// 创建处理器
|
||||
handler := common.NewExampleHandler("数据分析")
|
||||
|
||||
pdb, err := pipelinedb.Open(pipelinedb.Options{
|
||||
Filename: dbFile,
|
||||
Config: config,
|
||||
Handler: handler,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("打开数据库失败: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
fmt.Println("✅ 数据库已配置")
|
||||
|
||||
// 生成模拟用户行为数据
|
||||
fmt.Println("\n📊 步骤2: 生成模拟用户行为数据")
|
||||
|
||||
users := []string{"user001", "user002", "user003", "user004", "user005"}
|
||||
actions := []string{"页面访问", "按钮点击", "表单提交", "文件下载", "搜索"}
|
||||
pages := []string{"/home", "/products", "/about", "/contact", "/login", "/checkout"}
|
||||
|
||||
// 生成1000个随机事件
|
||||
events := make([]UserEvent, 1000)
|
||||
baseTime := time.Now().Add(-24 * time.Hour) // 从24小时前开始
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
event := UserEvent{
|
||||
UserID: users[rand.Intn(len(users))],
|
||||
Action: actions[rand.Intn(len(actions))],
|
||||
Page: pages[rand.Intn(len(pages))],
|
||||
Timestamp: baseTime.Add(time.Duration(i) * time.Minute),
|
||||
}
|
||||
|
||||
// 为某些动作添加价值
|
||||
if event.Action == "表单提交" || event.Action == "文件下载" {
|
||||
event.Value = rand.Float64() * 100
|
||||
}
|
||||
|
||||
events[i] = event
|
||||
}
|
||||
|
||||
fmt.Printf("✅ 生成了 %d 个用户事件\n", len(events))
|
||||
|
||||
// 将事件存储到数据库
|
||||
fmt.Println("\n💾 步骤3: 存储事件数据")
|
||||
|
||||
for i, event := range events {
|
||||
// 序列化事件数据
|
||||
eventData, err := json.Marshal(event)
|
||||
if err != nil {
|
||||
log.Fatalf("序列化事件失败: %v", err)
|
||||
}
|
||||
|
||||
// 创建元数据
|
||||
metadata := fmt.Sprintf(`{"user_id": "%s", "action": "%s", "page": "%s"}`,
|
||||
event.UserID, event.Action, event.Page)
|
||||
|
||||
// 根据动作类型分组
|
||||
group := "用户行为"
|
||||
if event.Action == "搜索" {
|
||||
group = "搜索行为"
|
||||
} else if event.Value > 0 {
|
||||
group = "有价值行为"
|
||||
}
|
||||
|
||||
recordID, err := pdb.AcceptData(group, eventData, metadata)
|
||||
if err != nil {
|
||||
log.Fatalf("存储事件失败: %v", err)
|
||||
}
|
||||
|
||||
if (i+1)%200 == 0 {
|
||||
fmt.Printf(" 📝 已存储 %d/%d 个事件,最新ID: %d\n", i+1, len(events), recordID)
|
||||
}
|
||||
}
|
||||
|
||||
// 数据分析
|
||||
fmt.Println("\n🔍 步骤4: 执行数据分析")
|
||||
|
||||
report := &AnalyticsReport{
|
||||
UserActivity: make(map[string]int),
|
||||
HourlyActivity: make(map[int]int),
|
||||
}
|
||||
|
||||
// 获取所有组的数据
|
||||
groups := []string{"用户行为", "搜索行为", "有价值行为"}
|
||||
allEvents := []UserEvent{}
|
||||
|
||||
for _, group := range groups {
|
||||
fmt.Printf("📋 分析组: %s\n", group)
|
||||
|
||||
pageReq := &pipelinedb.PageRequest{
|
||||
Page: 1,
|
||||
PageSize: 1000, // 获取所有数据
|
||||
}
|
||||
|
||||
response, err := pdb.GetRecordsByGroup(group, pageReq)
|
||||
if err != nil {
|
||||
fmt.Printf(" ❌ 查询组失败: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" 📊 找到 %d 条记录\n", len(response.Records))
|
||||
|
||||
// 解析事件数据
|
||||
for _, record := range response.Records {
|
||||
var event UserEvent
|
||||
if err := json.Unmarshal(record.Data, &event); err != nil {
|
||||
continue
|
||||
}
|
||||
allEvents = append(allEvents, event)
|
||||
}
|
||||
}
|
||||
|
||||
// 分析数据
|
||||
fmt.Println("\n📈 步骤5: 生成分析报告")
|
||||
|
||||
report.TotalEvents = len(allEvents)
|
||||
|
||||
// 统计唯一用户
|
||||
uniqueUsers := make(map[string]bool)
|
||||
actionCounts := make(map[string]int)
|
||||
pageCounts := make(map[string]int)
|
||||
|
||||
for _, event := range allEvents {
|
||||
// 唯一用户
|
||||
uniqueUsers[event.UserID] = true
|
||||
|
||||
// 用户活跃度
|
||||
report.UserActivity[event.UserID]++
|
||||
|
||||
// 动作统计
|
||||
actionCounts[event.Action]++
|
||||
|
||||
// 页面统计
|
||||
pageCounts[event.Page]++
|
||||
|
||||
// 小时活跃度
|
||||
hour := event.Timestamp.Hour()
|
||||
report.HourlyActivity[hour]++
|
||||
|
||||
// 总价值
|
||||
report.TotalValue += event.Value
|
||||
}
|
||||
|
||||
report.UniqueUsers = len(uniqueUsers)
|
||||
|
||||
// 排序Top动作
|
||||
for action, count := range actionCounts {
|
||||
report.TopActions = append(report.TopActions, ActionCount{
|
||||
Action: action,
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
sort.Slice(report.TopActions, func(i, j int) bool {
|
||||
return report.TopActions[i].Count > report.TopActions[j].Count
|
||||
})
|
||||
|
||||
// 排序Top页面
|
||||
for page, count := range pageCounts {
|
||||
report.TopPages = append(report.TopPages, PageCount{
|
||||
Page: page,
|
||||
Count: count,
|
||||
})
|
||||
}
|
||||
sort.Slice(report.TopPages, func(i, j int) bool {
|
||||
return report.TopPages[i].Count > report.TopPages[j].Count
|
||||
})
|
||||
|
||||
// 显示报告
|
||||
fmt.Println("\n📋 数据分析报告")
|
||||
fmt.Println("================")
|
||||
|
||||
fmt.Printf("📊 总体统计:\n")
|
||||
fmt.Printf(" 总事件数: %d\n", report.TotalEvents)
|
||||
fmt.Printf(" 唯一用户数: %d\n", report.UniqueUsers)
|
||||
fmt.Printf(" 总价值: %.2f\n", report.TotalValue)
|
||||
fmt.Printf(" 平均每用户事件: %.1f\n", float64(report.TotalEvents)/float64(report.UniqueUsers))
|
||||
|
||||
fmt.Printf("\n🔥 热门动作 (Top 5):\n")
|
||||
for i, action := range report.TopActions {
|
||||
if i >= 5 {
|
||||
break
|
||||
}
|
||||
percentage := float64(action.Count) / float64(report.TotalEvents) * 100
|
||||
fmt.Printf(" %d. %s: %d 次 (%.1f%%)\n",
|
||||
i+1, action.Action, action.Count, percentage)
|
||||
}
|
||||
|
||||
fmt.Printf("\n📄 热门页面 (Top 5):\n")
|
||||
for i, page := range report.TopPages {
|
||||
if i >= 5 {
|
||||
break
|
||||
}
|
||||
percentage := float64(page.Count) / float64(report.TotalEvents) * 100
|
||||
fmt.Printf(" %d. %s: %d 次 (%.1f%%)\n",
|
||||
i+1, page.Page, page.Count, percentage)
|
||||
}
|
||||
|
||||
fmt.Printf("\n👥 用户活跃度:\n")
|
||||
for userID, count := range report.UserActivity {
|
||||
fmt.Printf(" %s: %d 次事件\n", userID, count)
|
||||
}
|
||||
|
||||
// 数据库统计
|
||||
fmt.Println("\n💾 数据库统计")
|
||||
stats, err := pdb.GetStats()
|
||||
if err != nil {
|
||||
log.Fatalf("获取数据库统计失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("📈 存储统计:\n")
|
||||
fmt.Printf(" 数据库总记录: %d\n", stats.TotalRecords)
|
||||
fmt.Printf(" 总组数: %d\n", len(stats.GroupStats))
|
||||
|
||||
for group, groupStats := range stats.GroupStats {
|
||||
fmt.Printf(" [%s]: 热:%d 温:%d 冷:%d\n",
|
||||
group, groupStats.HotRecords, groupStats.WarmRecords, groupStats.ColdRecords)
|
||||
}
|
||||
|
||||
// 导出报告
|
||||
fmt.Println("\n📤 步骤6: 导出分析报告")
|
||||
reportJSON, err := json.MarshalIndent(report, "", " ")
|
||||
if err != nil {
|
||||
log.Fatalf("序列化报告失败: %v", err)
|
||||
}
|
||||
|
||||
reportFile := "analytics_report.json"
|
||||
err = os.WriteFile(reportFile, reportJSON, 0644)
|
||||
if err != nil {
|
||||
log.Fatalf("写入报告文件失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("✅ 分析报告已导出到: %s\n", reportFile)
|
||||
defer os.Remove(reportFile) // 清理示例文件
|
||||
|
||||
fmt.Println("\n🎉 数据分析示例完成!")
|
||||
fmt.Println("💡 提示: 这个示例展示了如何使用Pipeline Database进行复杂的数据分析")
|
||||
}
|
||||
9
examples/external-handler/go.mod
Normal file
9
examples/external-handler/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module external-handler
|
||||
|
||||
go 1.24.0
|
||||
|
||||
replace code.tczkiot.com/wlw/pipelinedb => ../../
|
||||
|
||||
require code.tczkiot.com/wlw/pipelinedb v0.0.0-00010101000000-000000000000
|
||||
|
||||
require github.com/google/btree v1.1.3 // indirect
|
||||
2
examples/external-handler/go.sum
Normal file
2
examples/external-handler/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
179
examples/external-handler/main.go
Normal file
179
examples/external-handler/main.go
Normal file
@@ -0,0 +1,179 @@
|
||||
// 演示如何使用外部处理器进行数据处理
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.tczkiot.com/wlw/pipelinedb"
|
||||
)
|
||||
|
||||
// EmailProcessor 邮件处理器示例
|
||||
type EmailProcessor struct {
|
||||
name string
|
||||
processed int
|
||||
}
|
||||
|
||||
func NewEmailProcessor(name string) *EmailProcessor {
|
||||
return &EmailProcessor{
|
||||
name: name,
|
||||
processed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// WillWarm 预热阶段:验证邮件格式
|
||||
func (ep *EmailProcessor) WillWarm(ctx context.Context, group string, data []byte) ([]byte, error) {
|
||||
fmt.Printf("🔥 [%s] 预热处理 - 组: %s\n", ep.name, group)
|
||||
|
||||
email := string(data)
|
||||
|
||||
// 简单的邮件格式验证
|
||||
if !strings.Contains(email, "@") {
|
||||
return nil, fmt.Errorf("无效的邮件格式: %s", email)
|
||||
}
|
||||
|
||||
// 标准化邮件地址(转为小写)
|
||||
normalizedEmail := strings.ToLower(strings.TrimSpace(email))
|
||||
|
||||
fmt.Printf(" 📧 邮件验证通过: %s -> %s\n", email, normalizedEmail)
|
||||
|
||||
// 模拟处理时间
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
|
||||
return []byte(normalizedEmail), nil
|
||||
}
|
||||
|
||||
// WillCold 冷却阶段:发送邮件通知
|
||||
func (ep *EmailProcessor) WillCold(ctx context.Context, group string, data []byte) ([]byte, error) {
|
||||
fmt.Printf("❄️ [%s] 冷却处理 - 组: %s\n", ep.name, group)
|
||||
|
||||
email := string(data)
|
||||
|
||||
// 模拟发送邮件
|
||||
fmt.Printf(" 📮 发送邮件到: %s\n", email)
|
||||
fmt.Printf(" 📝 邮件内容: 您的数据已成功处理完成\n")
|
||||
|
||||
// 模拟发送时间
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
ep.processed++
|
||||
fmt.Printf(" ✅ 邮件发送成功 (已处理 %d 封邮件)\n", ep.processed)
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// OnComplete 完成回调:统计处理结果
|
||||
func (ep *EmailProcessor) OnComplete(ctx context.Context, group string) error {
|
||||
fmt.Printf("🎉 [%s] 组处理完成 - 组: %s\n", ep.name, group)
|
||||
fmt.Printf(" 📊 本组共处理邮件: %d 封\n", ep.processed)
|
||||
|
||||
// 重置计数器
|
||||
ep.processed = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 创建临时数据库文件
|
||||
dbFile := "handler_example.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
// 确保文件可以创建
|
||||
if _, err := os.Create(dbFile); err != nil {
|
||||
log.Fatalf("创建数据库文件失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("🚀 外部处理器示例")
|
||||
fmt.Println("==================")
|
||||
|
||||
// 创建邮件处理器
|
||||
emailHandler := NewEmailProcessor("邮件处理器")
|
||||
|
||||
// 打开数据库并配置处理器
|
||||
fmt.Println("\n📂 步骤1: 配置数据库和处理器")
|
||||
config := &pipelinedb.Config{
|
||||
CacheSize: 50,
|
||||
}
|
||||
|
||||
pdb, err := pipelinedb.Open(pipelinedb.Options{
|
||||
Filename: dbFile,
|
||||
Config: config,
|
||||
Handler: emailHandler,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("打开数据库失败: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
fmt.Printf("✅ 数据库已配置外部处理器: %s\n", emailHandler.name)
|
||||
|
||||
// 接收邮件数据
|
||||
fmt.Println("\n📥 步骤2: 接收邮件数据")
|
||||
emails := []string{
|
||||
"user1@example.com",
|
||||
"USER2@EXAMPLE.COM", // 测试大小写转换
|
||||
"user3@test.org",
|
||||
"invalid-email", // 测试无效邮件
|
||||
"admin@company.net",
|
||||
}
|
||||
|
||||
for i, email := range emails {
|
||||
fmt.Printf("\n📧 处理邮件 %d: %s\n", i+1, email)
|
||||
|
||||
recordID, err := pdb.AcceptData("邮件处理", []byte(email),
|
||||
fmt.Sprintf(`{"batch": %d, "source": "web_form"}`, i+1))
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf(" ❌ 接收失败: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" ✅ 记录ID: %d\n", recordID)
|
||||
}
|
||||
|
||||
// 等待处理完成
|
||||
fmt.Println("\n⏳ 步骤3: 等待处理完成")
|
||||
fmt.Println("正在处理数据,请稍候...")
|
||||
|
||||
// 给处理器一些时间来处理数据
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 查看处理结果
|
||||
fmt.Println("\n🔍 步骤4: 查看处理结果")
|
||||
pageReq := &pipelinedb.PageRequest{
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
}
|
||||
|
||||
response, err := pdb.GetRecordsByGroup("邮件处理", pageReq)
|
||||
if err != nil {
|
||||
log.Fatalf("查询记录失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("📊 处理结果统计:\n")
|
||||
fmt.Printf(" 总记录数: %d\n", response.TotalCount)
|
||||
|
||||
statusCount := make(map[pipelinedb.DataStatus]int)
|
||||
for _, record := range response.Records {
|
||||
statusCount[record.Status]++
|
||||
}
|
||||
|
||||
fmt.Printf(" 状态分布:\n")
|
||||
fmt.Printf(" 热数据 (hot): %d\n", statusCount[pipelinedb.StatusHot])
|
||||
fmt.Printf(" 温数据 (warm): %d\n", statusCount[pipelinedb.StatusWarm])
|
||||
fmt.Printf(" 冷数据 (cold): %d\n", statusCount[pipelinedb.StatusCold])
|
||||
|
||||
// 显示详细记录
|
||||
fmt.Println("\n📋 详细记录:")
|
||||
for _, record := range response.Records {
|
||||
fmt.Printf(" ID:%d [%s] %s\n",
|
||||
record.ID, record.Status, string(record.Data))
|
||||
}
|
||||
|
||||
fmt.Println("\n🎉 外部处理器示例完成!")
|
||||
fmt.Println("💡 提示: 查看上面的日志,可以看到数据如何通过预热->冷却的完整流程")
|
||||
}
|
||||
9
examples/group-management/go.mod
Normal file
9
examples/group-management/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module group-management
|
||||
|
||||
go 1.24.0
|
||||
|
||||
replace code.tczkiot.com/wlw/pipelinedb => ../../
|
||||
|
||||
require code.tczkiot.com/wlw/pipelinedb v0.0.0-00010101000000-000000000000
|
||||
|
||||
require github.com/google/btree v1.1.3 // indirect
|
||||
2
examples/group-management/go.sum
Normal file
2
examples/group-management/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
231
examples/group-management/main.go
Normal file
231
examples/group-management/main.go
Normal file
@@ -0,0 +1,231 @@
|
||||
// 演示组管理功能
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.tczkiot.com/wlw/pipelinedb"
|
||||
"code.tczkiot.com/wlw/pipelinedb/examples/common"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 创建临时数据库文件
|
||||
dbFile := "group_example.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
// 确保文件可以创建
|
||||
if _, err := os.Create(dbFile); err != nil {
|
||||
log.Fatalf("创建数据库文件失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("🚀 组管理示例")
|
||||
fmt.Println("==============")
|
||||
|
||||
// 配置数据库
|
||||
fmt.Println("\n📂 步骤1: 配置数据库")
|
||||
config := &pipelinedb.Config{
|
||||
CacheSize: 50,
|
||||
}
|
||||
|
||||
// 创建处理器
|
||||
handler := common.NewExampleHandler("组管理示例")
|
||||
|
||||
pdb, err := pipelinedb.Open(pipelinedb.Options{
|
||||
Filename: dbFile,
|
||||
Config: config,
|
||||
Handler: handler,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("打开数据库失败: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
fmt.Println("✅ 数据库已配置")
|
||||
|
||||
// 创建多个组的数据
|
||||
fmt.Println("\n📊 步骤2: 创建多组数据")
|
||||
|
||||
groups := map[string][]string{
|
||||
"订单处理": {
|
||||
"新订单创建: 订单号 #12345",
|
||||
"支付确认: 订单号 #12345",
|
||||
"库存检查: 商品 SKU-001",
|
||||
"发货准备: 订单号 #12345",
|
||||
"物流跟踪: 快递单号 SF123456",
|
||||
},
|
||||
"用户管理": {
|
||||
"用户注册: user@example.com",
|
||||
"邮箱验证: user@example.com",
|
||||
"个人资料更新: 用户ID 1001",
|
||||
"密码修改: 用户ID 1001",
|
||||
"账户注销: 用户ID 1002",
|
||||
},
|
||||
"系统监控": {
|
||||
"CPU使用率: 85%",
|
||||
"内存使用率: 70%",
|
||||
"磁盘空间: 剩余 20GB",
|
||||
"网络延迟: 50ms",
|
||||
"数据库连接数: 150",
|
||||
},
|
||||
"安全审计": {
|
||||
"登录尝试: IP 192.168.1.100",
|
||||
"权限检查: 用户ID 1001",
|
||||
"异常访问: IP 10.0.0.1",
|
||||
"数据访问: 表 users",
|
||||
"API调用: /api/v1/users",
|
||||
},
|
||||
}
|
||||
|
||||
// 插入数据到各个组
|
||||
for group, messages := range groups {
|
||||
fmt.Printf("\n📝 插入数据到组: %s\n", group)
|
||||
|
||||
for i, message := range messages {
|
||||
metadata := fmt.Sprintf(`{"sequence": %d, "priority": "normal"}`, i+1)
|
||||
|
||||
recordID, err := pdb.AcceptData(group, []byte(message), metadata)
|
||||
if err != nil {
|
||||
log.Fatalf("插入数据失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf(" ✅ 记录 %d: %s\n", recordID, message)
|
||||
}
|
||||
}
|
||||
|
||||
// 查看初始统计
|
||||
fmt.Println("\n📊 步骤3: 查看初始统计")
|
||||
stats, err := pdb.GetStats()
|
||||
if err != nil {
|
||||
log.Fatalf("获取统计失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("📈 数据库统计:\n")
|
||||
fmt.Printf(" 总记录数: %d\n", stats.TotalRecords)
|
||||
fmt.Printf(" 总组数: %d\n", len(stats.GroupStats))
|
||||
|
||||
for group, groupStats := range stats.GroupStats {
|
||||
fmt.Printf(" [%s]: 总计:%d (热:%d 温:%d 冷:%d)\n",
|
||||
group, groupStats.TotalRecords, groupStats.HotRecords, groupStats.WarmRecords, groupStats.ColdRecords)
|
||||
}
|
||||
|
||||
// 演示组查询
|
||||
fmt.Println("\n🔍 步骤4: 演示组查询")
|
||||
|
||||
for group := range groups {
|
||||
fmt.Printf("\n📋 查询组: %s\n", group)
|
||||
|
||||
// 分页查询
|
||||
pageReq := &pipelinedb.PageRequest{
|
||||
Page: 1,
|
||||
PageSize: 3, // 每页3条记录
|
||||
}
|
||||
|
||||
response, err := pdb.GetRecordsByGroup(group, pageReq)
|
||||
if err != nil {
|
||||
fmt.Printf(" ❌ 查询失败: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf(" 📊 总记录: %d, 当前页: %d/%d\n",
|
||||
response.TotalCount, response.Page, response.TotalPages)
|
||||
|
||||
for _, record := range response.Records {
|
||||
fmt.Printf(" 📄 ID:%d [%s] %s\n",
|
||||
record.ID, record.Status, string(record.Data))
|
||||
}
|
||||
|
||||
// 如果有多页,查询第二页
|
||||
if response.TotalPages > 1 {
|
||||
fmt.Printf(" 📄 查询第2页:\n")
|
||||
pageReq.Page = 2
|
||||
|
||||
response2, err := pdb.GetRecordsByGroup(group, pageReq)
|
||||
if err != nil {
|
||||
fmt.Printf(" ❌ 查询第2页失败: %v\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, record := range response2.Records {
|
||||
fmt.Printf(" 📄 ID:%d [%s] %s\n",
|
||||
record.ID, record.Status, string(record.Data))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 演示数据状态流转
|
||||
fmt.Println("\n🔄 步骤5: 观察数据状态流转")
|
||||
|
||||
// 等待一段时间让数据处理
|
||||
fmt.Println("⏳ 等待数据处理...")
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
// 再次查看统计
|
||||
fmt.Println("\n📊 处理后的统计:")
|
||||
finalStats, err := pdb.GetStats()
|
||||
if err != nil {
|
||||
log.Fatalf("获取最终统计失败: %v", err)
|
||||
}
|
||||
|
||||
for group, groupStats := range finalStats.GroupStats {
|
||||
fmt.Printf(" [%s]: 总计:%d (热:%d 温:%d 冷:%d)\n",
|
||||
group, groupStats.TotalRecords, groupStats.HotRecords, groupStats.WarmRecords, groupStats.ColdRecords)
|
||||
}
|
||||
|
||||
// 演示跨组查询
|
||||
fmt.Println("\n🔍 步骤6: 跨组数据分析")
|
||||
|
||||
totalRecords := 0
|
||||
totalHot := 0
|
||||
totalWarm := 0
|
||||
totalCold := 0
|
||||
|
||||
for _, groupStats := range finalStats.GroupStats {
|
||||
totalRecords += groupStats.TotalRecords
|
||||
totalHot += groupStats.HotRecords
|
||||
totalWarm += groupStats.WarmRecords
|
||||
totalCold += groupStats.ColdRecords
|
||||
}
|
||||
|
||||
fmt.Printf("📊 跨组统计汇总:\n")
|
||||
fmt.Printf(" 总记录数: %d\n", totalRecords)
|
||||
fmt.Printf(" 状态分布:\n")
|
||||
fmt.Printf(" 热数据: %d (%.1f%%)\n", totalHot, float64(totalHot)/float64(totalRecords)*100)
|
||||
fmt.Printf(" 温数据: %d (%.1f%%)\n", totalWarm, float64(totalWarm)/float64(totalRecords)*100)
|
||||
fmt.Printf(" 冷数据: %d (%.1f%%)\n", totalCold, float64(totalCold)/float64(totalRecords)*100)
|
||||
|
||||
// 找出最活跃的组
|
||||
maxRecords := 0
|
||||
mostActiveGroup := ""
|
||||
for group, groupStats := range finalStats.GroupStats {
|
||||
if groupStats.TotalRecords > maxRecords {
|
||||
maxRecords = groupStats.TotalRecords
|
||||
mostActiveGroup = group
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n🏆 最活跃的组: %s (%d 条记录)\n", mostActiveGroup, maxRecords)
|
||||
|
||||
// 演示组级别的操作
|
||||
fmt.Println("\n🛠️ 步骤7: 组级别操作演示")
|
||||
|
||||
fmt.Printf("📋 所有组列表:\n")
|
||||
for group := range finalStats.GroupStats {
|
||||
fmt.Printf(" 📁 %s\n", group)
|
||||
}
|
||||
|
||||
// 模拟组的生命周期管理
|
||||
fmt.Printf("\n🔄 组生命周期管理:\n")
|
||||
fmt.Printf(" 1. 创建组 -> 接收数据 -> 处理数据\n")
|
||||
fmt.Printf(" 2. 监控组状态 -> 统计分析\n")
|
||||
fmt.Printf(" 3. 暂停/恢复组 -> 组级别控制\n")
|
||||
fmt.Printf(" 4. 查询组数据 -> 分页浏览\n")
|
||||
fmt.Printf(" 5. 跨组分析 -> 全局统计\n")
|
||||
|
||||
fmt.Println("\n🎉 组管理示例完成!")
|
||||
fmt.Println("💡 提示: 组功能让你可以按业务逻辑组织和管理数据")
|
||||
fmt.Println("💡 每个组都有独立的统计信息和处理流程")
|
||||
fmt.Println("💡 支持组级别的暂停、恢复和查询操作")
|
||||
}
|
||||
9
examples/high-concurrency/go.mod
Normal file
9
examples/high-concurrency/go.mod
Normal file
@@ -0,0 +1,9 @@
|
||||
module high-concurrency
|
||||
|
||||
go 1.24.0
|
||||
|
||||
replace code.tczkiot.com/wlw/pipelinedb => ../../
|
||||
|
||||
require code.tczkiot.com/wlw/pipelinedb v0.0.0-00010101000000-000000000000
|
||||
|
||||
require github.com/google/btree v1.1.3 // indirect
|
||||
2
examples/high-concurrency/go.sum
Normal file
2
examples/high-concurrency/go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
315
examples/high-concurrency/main.go
Normal file
315
examples/high-concurrency/main.go
Normal file
@@ -0,0 +1,315 @@
|
||||
// 高并发压力测试示例
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"code.tczkiot.com/wlw/pipelinedb"
|
||||
"code.tczkiot.com/wlw/pipelinedb/examples/common"
|
||||
)
|
||||
|
||||
// 性能指标
|
||||
type PerformanceMetrics struct {
|
||||
TotalOperations int64
|
||||
SuccessfulWrites int64
|
||||
FailedWrites int64
|
||||
SuccessfulReads int64
|
||||
FailedReads int64
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
}
|
||||
|
||||
// 工作负载配置
|
||||
type WorkloadConfig struct {
|
||||
NumWriters int // 写入goroutine数量
|
||||
NumReaders int // 读取goroutine数量
|
||||
WritesPerSecond int // 每秒写入次数
|
||||
ReadsPerSecond int // 每秒读取次数
|
||||
TestDuration time.Duration // 测试持续时间
|
||||
DataSize int // 每条记录的数据大小
|
||||
NumGroups int // 数据组数量
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 设置随机种子
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
// 创建临时数据库文件
|
||||
dbFile := "high_concurrency_test.db"
|
||||
defer os.Remove(dbFile)
|
||||
|
||||
// 确保文件可以创建
|
||||
if _, err := os.Create(dbFile); err != nil {
|
||||
log.Fatalf("创建数据库文件失败: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println("🚀 高并发压力测试")
|
||||
fmt.Println("==================")
|
||||
fmt.Printf("🖥️ 系统信息: %d CPU核心, %s\n", runtime.NumCPU(), runtime.Version())
|
||||
|
||||
// 配置工作负载
|
||||
config := WorkloadConfig{
|
||||
NumWriters: 50, // 50个写入goroutine
|
||||
NumReaders: 20, // 20个读取goroutine
|
||||
WritesPerSecond: 1000, // 每秒1000次写入
|
||||
ReadsPerSecond: 500, // 每秒500次读取
|
||||
TestDuration: 30 * time.Second, // 运行30秒
|
||||
DataSize: 256, // 256字节数据
|
||||
NumGroups: 10, // 10个数据组
|
||||
}
|
||||
|
||||
fmt.Printf("📊 测试配置:\n")
|
||||
fmt.Printf(" 写入goroutine: %d\n", config.NumWriters)
|
||||
fmt.Printf(" 读取goroutine: %d\n", config.NumReaders)
|
||||
fmt.Printf(" 目标写入QPS: %d\n", config.WritesPerSecond)
|
||||
fmt.Printf(" 目标读取QPS: %d\n", config.ReadsPerSecond)
|
||||
fmt.Printf(" 测试时长: %v\n", config.TestDuration)
|
||||
fmt.Printf(" 数据大小: %d bytes\n", config.DataSize)
|
||||
fmt.Printf(" 数据组数: %d\n", config.NumGroups)
|
||||
|
||||
// 配置数据库
|
||||
fmt.Println("\n📂 步骤1: 初始化数据库")
|
||||
dbConfig := &pipelinedb.Config{
|
||||
CacheSize: 1000, // 大缓存支持高并发
|
||||
WarmInterval: 5 * time.Second, // 较长的预热间隔
|
||||
ProcessInterval: 10 * time.Second, // 较长的处理间隔
|
||||
BatchSize: 100, // 大批次处理
|
||||
}
|
||||
|
||||
// 创建处理器
|
||||
handler := common.NewLoggingHandler()
|
||||
|
||||
pdb, err := pipelinedb.Open(pipelinedb.Options{
|
||||
Filename: dbFile,
|
||||
Config: dbConfig,
|
||||
Handler: handler,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("打开数据库失败: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
fmt.Println("✅ 数据库已初始化")
|
||||
|
||||
// 性能指标
|
||||
metrics := &PerformanceMetrics{
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
|
||||
// 控制通道
|
||||
stopChan := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 启动写入goroutine
|
||||
fmt.Println("\n🏭 步骤2: 启动写入压力测试")
|
||||
writeInterval := time.Duration(int64(time.Second) / int64(config.WritesPerSecond/config.NumWriters))
|
||||
|
||||
for i := 0; i < config.NumWriters; i++ {
|
||||
wg.Add(1)
|
||||
go func(writerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(writeInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
localWrites := 0
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
fmt.Printf("📝 写入器 %d 停止,完成 %d 次写入\n", writerID, localWrites)
|
||||
return
|
||||
case <-ticker.C:
|
||||
// 生成随机数据
|
||||
groupID := rand.Intn(config.NumGroups)
|
||||
groupName := fmt.Sprintf("group_%d", groupID)
|
||||
|
||||
data := make([]byte, config.DataSize)
|
||||
for j := range data {
|
||||
data[j] = byte(rand.Intn(256))
|
||||
}
|
||||
|
||||
metadata := fmt.Sprintf(`{"writer_id": %d, "timestamp": %d, "sequence": %d}`,
|
||||
writerID, time.Now().UnixNano(), localWrites)
|
||||
|
||||
// 执行写入
|
||||
_, err := pdb.AcceptData(groupName, data, metadata)
|
||||
atomic.AddInt64(&metrics.TotalOperations, 1)
|
||||
|
||||
if err != nil {
|
||||
atomic.AddInt64(&metrics.FailedWrites, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&metrics.SuccessfulWrites, 1)
|
||||
localWrites++
|
||||
}
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// 启动读取goroutine
|
||||
fmt.Println("🔍 启动读取压力测试")
|
||||
readInterval := time.Duration(int64(time.Second) / int64(config.ReadsPerSecond/config.NumReaders))
|
||||
|
||||
for i := 0; i < config.NumReaders; i++ {
|
||||
wg.Add(1)
|
||||
go func(readerID int) {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(readInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
localReads := 0
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
fmt.Printf("📖 读取器 %d 停止,完成 %d 次读取\n", readerID, localReads)
|
||||
return
|
||||
case <-ticker.C:
|
||||
// 随机选择组进行查询
|
||||
groupID := rand.Intn(config.NumGroups)
|
||||
groupName := fmt.Sprintf("group_%d", groupID)
|
||||
|
||||
pageReq := &pipelinedb.PageRequest{
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
}
|
||||
|
||||
// 执行读取
|
||||
_, err := pdb.GetRecordsByGroup(groupName, pageReq)
|
||||
atomic.AddInt64(&metrics.TotalOperations, 1)
|
||||
|
||||
if err != nil {
|
||||
atomic.AddInt64(&metrics.FailedReads, 1)
|
||||
} else {
|
||||
atomic.AddInt64(&metrics.SuccessfulReads, 1)
|
||||
localReads++
|
||||
}
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// 启动实时监控
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
lastWrites := int64(0)
|
||||
lastReads := int64(0)
|
||||
lastTime := time.Now()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stopChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
currentWrites := atomic.LoadInt64(&metrics.SuccessfulWrites)
|
||||
currentReads := atomic.LoadInt64(&metrics.SuccessfulReads)
|
||||
currentTime := time.Now()
|
||||
|
||||
elapsed := currentTime.Sub(lastTime).Seconds()
|
||||
writeQPS := float64(currentWrites-lastWrites) / elapsed
|
||||
readQPS := float64(currentReads-lastReads) / elapsed
|
||||
|
||||
fmt.Printf("\n📊 实时性能 (时间: %s)\n", currentTime.Format("15:04:05"))
|
||||
fmt.Printf(" 写入QPS: %.1f (目标: %d)\n", writeQPS, config.WritesPerSecond)
|
||||
fmt.Printf(" 读取QPS: %.1f (目标: %d)\n", readQPS, config.ReadsPerSecond)
|
||||
fmt.Printf(" 总写入: %d (失败: %d)\n", currentWrites, atomic.LoadInt64(&metrics.FailedWrites))
|
||||
fmt.Printf(" 总读取: %d (失败: %d)\n", currentReads, atomic.LoadInt64(&metrics.FailedReads))
|
||||
|
||||
// 获取数据库统计
|
||||
stats, err := pdb.GetStats()
|
||||
if err == nil {
|
||||
fmt.Printf(" 数据库记录: %d\n", stats.TotalRecords)
|
||||
fmt.Printf(" 活跃组数: %d\n", len(stats.GroupStats))
|
||||
}
|
||||
|
||||
lastWrites = currentWrites
|
||||
lastReads = currentReads
|
||||
lastTime = currentTime
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 运行测试
|
||||
fmt.Printf("\n⏳ 步骤3: 运行压力测试 (%v)\n", config.TestDuration)
|
||||
time.Sleep(config.TestDuration)
|
||||
|
||||
// 停止所有goroutine
|
||||
fmt.Println("\n🛑 步骤4: 停止压力测试")
|
||||
close(stopChan)
|
||||
wg.Wait()
|
||||
|
||||
metrics.EndTime = time.Now()
|
||||
|
||||
// 最终性能报告
|
||||
fmt.Println("\n📈 步骤5: 性能报告")
|
||||
totalDuration := metrics.EndTime.Sub(metrics.StartTime)
|
||||
|
||||
fmt.Printf("🎯 测试结果:\n")
|
||||
fmt.Printf(" 测试时长: %v\n", totalDuration)
|
||||
fmt.Printf(" 总操作数: %d\n", metrics.TotalOperations)
|
||||
fmt.Printf(" 平均QPS: %.1f\n", float64(metrics.TotalOperations)/totalDuration.Seconds())
|
||||
|
||||
fmt.Printf("\n📝 写入性能:\n")
|
||||
fmt.Printf(" 成功写入: %d\n", metrics.SuccessfulWrites)
|
||||
fmt.Printf(" 失败写入: %d\n", metrics.FailedWrites)
|
||||
fmt.Printf(" 写入成功率: %.2f%%\n", float64(metrics.SuccessfulWrites)/float64(metrics.SuccessfulWrites+metrics.FailedWrites)*100)
|
||||
fmt.Printf(" 平均写入QPS: %.1f\n", float64(metrics.SuccessfulWrites)/totalDuration.Seconds())
|
||||
|
||||
fmt.Printf("\n📖 读取性能:\n")
|
||||
fmt.Printf(" 成功读取: %d\n", metrics.SuccessfulReads)
|
||||
fmt.Printf(" 失败读取: %d\n", metrics.FailedReads)
|
||||
fmt.Printf(" 读取成功率: %.2f%%\n", float64(metrics.SuccessfulReads)/float64(metrics.SuccessfulReads+metrics.FailedReads)*100)
|
||||
fmt.Printf(" 平均读取QPS: %.1f\n", float64(metrics.SuccessfulReads)/totalDuration.Seconds())
|
||||
|
||||
// 数据库最终状态
|
||||
finalStats, err := pdb.GetStats()
|
||||
if err == nil {
|
||||
fmt.Printf("\n💾 数据库状态:\n")
|
||||
fmt.Printf(" 总记录数: %d\n", finalStats.TotalRecords)
|
||||
fmt.Printf(" 总组数: %d\n", len(finalStats.GroupStats))
|
||||
fmt.Printf(" 热数据: %d\n", finalStats.HotRecords)
|
||||
fmt.Printf(" 温数据: %d\n", finalStats.WarmRecords)
|
||||
fmt.Printf(" 冷数据: %d\n", finalStats.ColdRecords)
|
||||
|
||||
fmt.Printf("\n📊 各组分布:\n")
|
||||
for group, groupStats := range finalStats.GroupStats {
|
||||
fmt.Printf(" %s: %d 条记录\n", group, groupStats.TotalRecords)
|
||||
}
|
||||
}
|
||||
|
||||
// 性能评估
|
||||
fmt.Printf("\n🏆 性能评估:\n")
|
||||
expectedWrites := float64(config.WritesPerSecond) * totalDuration.Seconds()
|
||||
expectedReads := float64(config.ReadsPerSecond) * totalDuration.Seconds()
|
||||
|
||||
writeEfficiency := float64(metrics.SuccessfulWrites) / expectedWrites * 100
|
||||
readEfficiency := float64(metrics.SuccessfulReads) / expectedReads * 100
|
||||
|
||||
fmt.Printf(" 写入效率: %.1f%% (期望: %.0f, 实际: %d)\n",
|
||||
writeEfficiency, expectedWrites, metrics.SuccessfulWrites)
|
||||
fmt.Printf(" 读取效率: %.1f%% (期望: %.0f, 实际: %d)\n",
|
||||
readEfficiency, expectedReads, metrics.SuccessfulReads)
|
||||
|
||||
if writeEfficiency > 90 && readEfficiency > 90 {
|
||||
fmt.Println(" 🎉 性能优秀!数据库在高并发下表现稳定")
|
||||
} else if writeEfficiency > 70 && readEfficiency > 70 {
|
||||
fmt.Println(" 👍 性能良好,可以承受高并发负载")
|
||||
} else {
|
||||
fmt.Println(" ⚠️ 性能需要优化,建议调整配置参数")
|
||||
}
|
||||
|
||||
fmt.Println("\n🎉 高并发压力测试完成!")
|
||||
fmt.Println("💡 这个测试验证了Pipeline Database在极限并发下的稳定性和性能")
|
||||
}
|
||||
122
examples/run_all.sh
Executable file
122
examples/run_all.sh
Executable file
@@ -0,0 +1,122 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Pipeline Database V4 - 运行所有示例脚本
|
||||
|
||||
echo "🚀 Pipeline Database V4 - 运行所有示例"
|
||||
echo "========================================"
|
||||
|
||||
# 示例列表
|
||||
examples=(
|
||||
"basic-usage:基础使用示例:演示数据库的基本操作"
|
||||
"group-management:组管理示例:演示多组数据管理功能"
|
||||
"external-handler:外部处理器示例:演示自定义数据处理流程"
|
||||
"data-analytics:数据分析示例:演示复杂数据分析功能"
|
||||
"concurrent-processing:并发处理示例:演示高并发数据处理"
|
||||
)
|
||||
|
||||
echo "📋 将要运行 ${#examples[@]} 个示例"
|
||||
echo ""
|
||||
|
||||
# 询问用户是否继续
|
||||
read -p "是否继续运行所有示例? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
echo "❌ 已取消运行"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
success_count=0
|
||||
fail_count=0
|
||||
total_start=$(date +%s)
|
||||
|
||||
for i in "${!examples[@]}"; do
|
||||
IFS=':' read -r dir name desc <<< "${examples[$i]}"
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "📍 示例 $((i+1))/${#examples[@]}: $name"
|
||||
echo "📁 目录: $dir"
|
||||
echo "📝 描述: $desc"
|
||||
echo "============================================================"
|
||||
|
||||
if [ ! -d "$dir" ]; then
|
||||
echo "❌ 目录不存在: $dir"
|
||||
((fail_count++))
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ ! -f "$dir/main.go" ]; then
|
||||
echo "❌ 文件不存在: $dir/main.go"
|
||||
((fail_count++))
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "🚀 开始运行..."
|
||||
echo ""
|
||||
|
||||
start_time=$(date +%s)
|
||||
|
||||
# 运行示例
|
||||
if (cd "$dir" && go run main.go); then
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
echo ""
|
||||
echo "✅ 示例运行成功!"
|
||||
echo "⏱️ 运行时间: ${duration}秒"
|
||||
((success_count++))
|
||||
else
|
||||
end_time=$(date +%s)
|
||||
duration=$((end_time - start_time))
|
||||
echo ""
|
||||
echo "❌ 示例运行失败!"
|
||||
echo "⏱️ 运行时间: ${duration}秒"
|
||||
((fail_count++))
|
||||
fi
|
||||
|
||||
# 如果不是最后一个示例,等待一下
|
||||
if [ $((i+1)) -lt ${#examples[@]} ]; then
|
||||
echo ""
|
||||
echo "⏳ 等待 2 秒后运行下一个示例..."
|
||||
sleep 2
|
||||
fi
|
||||
done
|
||||
|
||||
# 总结
|
||||
total_end=$(date +%s)
|
||||
total_duration=$((total_end - total_start))
|
||||
|
||||
echo ""
|
||||
echo "============================================================"
|
||||
echo "🎯 运行总结"
|
||||
echo "============================================================"
|
||||
echo "✅ 成功: $success_count 个示例"
|
||||
echo "❌ 失败: $fail_count 个示例"
|
||||
echo "📊 成功率: $(echo "scale=1; $success_count * 100 / ${#examples[@]}" | bc -l)%"
|
||||
echo "⏱️ 总运行时间: ${total_duration}秒"
|
||||
|
||||
if [ $fail_count -eq 0 ]; then
|
||||
echo ""
|
||||
echo "🎉 所有示例都运行成功!"
|
||||
echo "💡 你现在已经了解了 Pipeline Database V4 的主要功能"
|
||||
echo "📚 建议接下来:"
|
||||
echo " 1. 查看示例源代码,理解实现细节"
|
||||
echo " 2. 根据你的需求修改示例"
|
||||
echo " 3. 在你的项目中集成 Pipeline Database"
|
||||
else
|
||||
echo ""
|
||||
echo "⚠️ 有 $fail_count 个示例运行失败"
|
||||
echo "🔧 建议检查:"
|
||||
echo " 1. Go 版本是否兼容 (需要 1.19+)"
|
||||
echo " 2. 依赖包是否正确安装"
|
||||
echo " 3. 文件权限是否正确"
|
||||
echo " 4. 系统资源是否充足"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "📞 需要帮助?"
|
||||
echo " - 查看 README.md 了解详细信息"
|
||||
echo " - 检查项目文档和 API 参考"
|
||||
echo " - 在项目仓库提交 Issue"
|
||||
|
||||
echo ""
|
||||
echo "🙏 感谢使用 Pipeline Database V4!"
|
||||
344
freepage.go
Normal file
344
freepage.go
Normal file
@@ -0,0 +1,344 @@
|
||||
// Package pipelinedb provides an integrated pipeline database system
|
||||
// 集成了数据库存储和业务管道处理的一体化解决方案
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// FreePageManager 空闲页面管理器
|
||||
//
|
||||
// 核心职责:
|
||||
// 1. 管理数据库文件中的空闲页面
|
||||
// 2. 提供页面分配和回收功能
|
||||
// 3. 维护空闲页面的持久化存储
|
||||
// 4. 确保页面分配的线程安全性
|
||||
//
|
||||
// 设计思想:
|
||||
// - 使用切片维护空闲页面列表,提供O(1)的分配性能
|
||||
// - 支持空闲页面链表的持久化存储和恢复
|
||||
// - 使用互斥锁保证并发安全
|
||||
// - 防重复释放机制,避免空闲页面重复添加
|
||||
//
|
||||
// 适用场景:
|
||||
// - 数据库页面空间管理
|
||||
// - 文件系统空闲块管理
|
||||
// - 任何需要页面级别资源管理的系统
|
||||
//
|
||||
// 性能特征:
|
||||
// - 分配:O(1) - 从切片末尾取出
|
||||
// - 释放:O(n) - 需要检查重复(n为空闲页数量)
|
||||
// - 空间:O(n) - n为空闲页面数量
|
||||
type FreePageManager struct {
|
||||
freePages []uint16 // 空闲页面编号列表(使用切片实现栈结构)
|
||||
mu sync.Mutex // 互斥锁,保护空闲页面列表的并发访问
|
||||
}
|
||||
|
||||
// NewFreePageManager 创建一个新的空闲页面管理器实例
|
||||
//
|
||||
// 初始化内容:
|
||||
// 1. 创建空的空闲页面切片
|
||||
// 2. 初始化互斥锁(自动完成)
|
||||
//
|
||||
// 返回值:
|
||||
// - *FreePageManager: 初始化完成的空闲页面管理器
|
||||
//
|
||||
// 使用示例:
|
||||
//
|
||||
// fpm := NewFreePageManager()
|
||||
// pageNo, ok := fpm.AllocPage() // 分配页面
|
||||
// fpm.FreePage(pageNo) // 释放页面
|
||||
func NewFreePageManager() *FreePageManager {
|
||||
return &FreePageManager{
|
||||
freePages: make([]uint16, 0), // 初始化空的切片,容量为0
|
||||
// mu 会自动初始化为零值(未锁定状态)
|
||||
}
|
||||
}
|
||||
|
||||
// AllocPage 从空闲页面池中分配一个页面
|
||||
//
|
||||
// 核心算法:栈式分配(LIFO - Last In First Out)
|
||||
// 时间复杂度:O(1) - 直接从切片末尾取出
|
||||
// 空间复杂度:O(1) - 只修改切片长度
|
||||
//
|
||||
// 返回值:
|
||||
// - uint16: 分配的页面编号(如果成功)
|
||||
// - bool: 是否分配成功(true=成功,false=无空闲页面)
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取互斥锁(保证线程安全)
|
||||
// 2. 检查是否有空闲页面可用
|
||||
// 3. 如果有:从切片末尾取出页面编号,缩短切片
|
||||
// 4. 如果没有:返回失败标志
|
||||
//
|
||||
// 设计考虑:
|
||||
// - 使用LIFO策略提高缓存局部性
|
||||
// - 最近释放的页面可能仍在缓存中
|
||||
// - O(1)时间复杂度确保高性能分配
|
||||
//
|
||||
// 并发安全:使用互斥锁保护,支持多线程并发调用
|
||||
func (fpm *FreePageManager) AllocPage() (uint16, bool) {
|
||||
fpm.mu.Lock() // 获取互斥锁,保证线程安全
|
||||
defer fpm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 检查是否有空闲页面可用
|
||||
if len(fpm.freePages) == 0 {
|
||||
return 0, false // 无空闲页面,分配失败
|
||||
}
|
||||
|
||||
// 从切片末尾取出最后一个空闲页面(LIFO策略)
|
||||
// 这样可以提高缓存局部性,最近释放的页面可能仍在内存中
|
||||
pageNo := fpm.freePages[len(fpm.freePages)-1]
|
||||
|
||||
// 缩短切片,移除已分配的页面
|
||||
// 注意:这里使用切片操作,不会重新分配内存
|
||||
fpm.freePages = fpm.freePages[:len(fpm.freePages)-1]
|
||||
|
||||
return pageNo, true // 返回分配的页面编号和成功标志
|
||||
}
|
||||
|
||||
// FreePage 将一个页面释放回空闲页面池
|
||||
//
|
||||
// 核心算法:防重复释放的页面回收
|
||||
// 时间复杂度:O(n) - n为当前空闲页面数量(需要检查重复)
|
||||
// 空间复杂度:O(1) - 只添加一个页面编号
|
||||
//
|
||||
// 参数说明:
|
||||
// - pageNo: 要释放的页面编号
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取互斥锁(保证线程安全)
|
||||
// 2. 遍历现有空闲页面列表,检查是否重复
|
||||
// 3. 如果不重复:将页面编号添加到列表末尾
|
||||
// 4. 如果重复:静默忽略(幂等操作)
|
||||
//
|
||||
// 设计考虑:
|
||||
// - 防重复机制避免空闲页面列表污染
|
||||
// - 幂等操作确保多次释放同一页面不会出错
|
||||
// - 添加到末尾保持LIFO分配顺序
|
||||
//
|
||||
// 性能权衡:
|
||||
// - 检查重复需要O(n)时间,但保证了数据一致性
|
||||
// - 对于大量空闲页面的场景,可考虑使用Set数据结构优化
|
||||
//
|
||||
// 并发安全:使用互斥锁保护,支持多线程并发调用
|
||||
func (fpm *FreePageManager) FreePage(pageNo uint16) {
|
||||
fpm.mu.Lock() // 获取互斥锁,保证线程安全
|
||||
defer fpm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 检查页面是否已经在空闲列表中(防重复释放)
|
||||
for _, p := range fpm.freePages {
|
||||
if p == pageNo {
|
||||
return // 页面已存在,静默忽略(幂等操作)
|
||||
}
|
||||
}
|
||||
|
||||
// 将页面添加到空闲列表末尾
|
||||
// 这样下次分配时会优先使用(LIFO策略)
|
||||
fpm.freePages = append(fpm.freePages, pageNo)
|
||||
}
|
||||
|
||||
// FreeCount 获取当前空闲页面的数量
|
||||
//
|
||||
// 用途:
|
||||
// - 监控数据库空间使用情况
|
||||
// - 评估是否需要扩展数据库文件
|
||||
// - 性能调优和容量规划
|
||||
//
|
||||
// 核心算法:直接返回切片长度
|
||||
// 时间复杂度:O(1) - 直接读取切片长度
|
||||
// 空间复杂度:O(1) - 不创建额外数据
|
||||
//
|
||||
// 返回值:
|
||||
// - int: 当前空闲页面数量(>=0)
|
||||
//
|
||||
// 使用示例:
|
||||
//
|
||||
// count := fpm.FreeCount()
|
||||
// if count < 100 {
|
||||
// // 空闲页面不足,考虑扩展数据库
|
||||
// }
|
||||
//
|
||||
// 并发安全:使用互斥锁保护,支持多线程并发调用
|
||||
func (fpm *FreePageManager) FreeCount() int {
|
||||
fpm.mu.Lock() // 获取互斥锁,保证读取一致性
|
||||
defer fpm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 返回空闲页面列表的长度
|
||||
return len(fpm.freePages)
|
||||
}
|
||||
|
||||
// LoadFromHeader 从数据库文件头加载空闲页面信息
|
||||
//
|
||||
// 核心功能:将持久化存储的空闲页面链表加载到内存中
|
||||
// 时间复杂度:O(n) - n为空闲页面数量(需要遍历链表)
|
||||
// 空间复杂度:O(n) - 需要存储所有空闲页面编号
|
||||
//
|
||||
// 参数说明:
|
||||
// - freeHead: 空闲页面链表的头页面编号(0表示无空闲页面)
|
||||
// - readPage: 页面读取函数,用于从磁盘读取页面数据
|
||||
// 函数签名:func(pageNo uint16) ([]byte, error)
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 加载过程中的错误(nil表示成功)
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取互斥锁(保证线程安全)
|
||||
// 2. 清空当前内存中的空闲页面列表
|
||||
// 3. 从链表头开始遍历空闲页面链表:
|
||||
// a. 读取当前页面数据
|
||||
// b. 解析下一页面指针(存储在页面偏移4-6字节)
|
||||
// c. 将当前页面添加到空闲列表
|
||||
// d. 移动到下一页面
|
||||
// 4. 遍历完成后返回结果
|
||||
//
|
||||
// 链表结构:
|
||||
// - 每个空闲页面的4-6字节存储下一个空闲页面的编号
|
||||
// - 链表末尾页面的下一页面指针为0
|
||||
// - 支持最多65535个空闲页面
|
||||
//
|
||||
// 错误处理:
|
||||
// - 页面读取失败时立即返回错误
|
||||
// - 防止无限循环(限制最大页面数)
|
||||
//
|
||||
// 并发安全:使用互斥锁保护,确保加载过程的原子性
|
||||
func (fpm *FreePageManager) LoadFromHeader(freeHead uint16, readPage func(uint16) ([]byte, error)) error {
|
||||
fpm.mu.Lock() // 获取互斥锁,保证线程安全
|
||||
defer fpm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 清空现有的空闲页面列表,准备重新加载
|
||||
fpm.freePages = fpm.freePages[:0]
|
||||
|
||||
// 从链表头开始遍历空闲页面链表
|
||||
current := freeHead
|
||||
for current != 0 {
|
||||
// 读取当前页面的数据
|
||||
p, err := readPage(current)
|
||||
if err != nil {
|
||||
return err // 页面读取失败,返回错误
|
||||
}
|
||||
|
||||
// 从页面数据中解析下一页面指针
|
||||
// 空闲页面的4-6字节存储下一个空闲页面的编号
|
||||
nextPage := binary.LittleEndian.Uint16(p[4:6])
|
||||
|
||||
// 将当前页面添加到空闲列表中
|
||||
fpm.freePages = append(fpm.freePages, current)
|
||||
|
||||
// 移动到链表中的下一个页面
|
||||
current = nextPage
|
||||
|
||||
// 防止无限循环的安全检查
|
||||
// 理论上最多支持65535个页面(uint16的最大值)
|
||||
if len(fpm.freePages) > 65535 {
|
||||
break // 超出合理范围,停止遍历
|
||||
}
|
||||
}
|
||||
|
||||
return nil // 加载成功
|
||||
}
|
||||
|
||||
// SaveToHeader 将空闲页面信息保存到数据库文件
|
||||
//
|
||||
// 核心功能:将内存中的空闲页面列表持久化为磁盘上的链表结构
|
||||
// 时间复杂度:O(n) - n为空闲页面数量(需要写入所有页面)
|
||||
// 空间复杂度:O(1) - 只创建单个页面的缓冲区
|
||||
//
|
||||
// 参数说明:
|
||||
// - writePage: 页面写入函数,用于将页面数据写入磁盘
|
||||
// 函数签名:func(pageNo uint16, data []byte) error
|
||||
//
|
||||
// 返回值:
|
||||
// - uint16: 空闲页面链表的头页面编号(0表示无空闲页面)
|
||||
// - error: 保存过程中的错误(nil表示成功)
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取互斥锁(保证线程安全)
|
||||
// 2. 检查是否有空闲页面需要保存
|
||||
// 3. 遍历所有空闲页面,构建链表结构:
|
||||
// a. 创建页面数据缓冲区
|
||||
// b. 设置下一页面指针(存储在4-6字节)
|
||||
// c. 写入页面数据到磁盘
|
||||
// 4. 返回链表头页面编号
|
||||
//
|
||||
// 链表结构:
|
||||
// - 每个页面的4-6字节存储下一页面编号
|
||||
// - 最后一个页面的下一页面指针设为0
|
||||
// - 链表头是第一个空闲页面的编号
|
||||
//
|
||||
// 错误处理:
|
||||
// - 页面写入失败时立即返回错误
|
||||
// - 部分写入成功的情况下,链表可能处于不一致状态
|
||||
//
|
||||
// 并发安全:使用互斥锁保护,确保保存过程的原子性
|
||||
func (fpm *FreePageManager) SaveToHeader(writePage func(uint16, []byte) error) (uint16, error) {
|
||||
fpm.mu.Lock() // 获取互斥锁,保证线程安全
|
||||
defer fpm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 检查是否有空闲页面需要保存
|
||||
if len(fpm.freePages) == 0 {
|
||||
return 0, nil // 无空闲页面,返回0作为链表头
|
||||
}
|
||||
|
||||
// 遍历所有空闲页面,构建链表结构
|
||||
for i, pageNo := range fpm.freePages {
|
||||
// 创建页面大小的数据缓冲区
|
||||
p := make([]byte, PageSize)
|
||||
|
||||
// 设置下一页面指针(存储在页面的4-6字节位置)
|
||||
if i < len(fpm.freePages)-1 {
|
||||
// 不是最后一个页面,设置指向下一个空闲页面
|
||||
nextPage := fpm.freePages[i+1]
|
||||
binary.LittleEndian.PutUint16(p[4:6], nextPage)
|
||||
} else {
|
||||
// 最后一个页面,设置下一页面指针为0(链表结束标志)
|
||||
binary.LittleEndian.PutUint16(p[4:6], 0)
|
||||
}
|
||||
|
||||
// 将页面数据写入磁盘
|
||||
if err := writePage(pageNo, p); err != nil {
|
||||
return 0, err // 写入失败,返回错误
|
||||
}
|
||||
}
|
||||
|
||||
// 返回链表头页面编号(第一个空闲页面)
|
||||
return fpm.freePages[0], nil
|
||||
}
|
||||
|
||||
// GetFreePages 获取所有空闲页面编号的副本(主要用于调试和监控)
|
||||
//
|
||||
// 用途:
|
||||
// - 调试空闲页面管理逻辑
|
||||
// - 监控空闲页面分布情况
|
||||
// - 数据库诊断和分析
|
||||
//
|
||||
// 核心算法:创建空闲页面列表的副本
|
||||
// 时间复杂度:O(n) - n为空闲页面数量(需要复制所有元素)
|
||||
// 空间复杂度:O(n) - 创建完整的副本
|
||||
//
|
||||
// 返回值:
|
||||
// - []uint16: 所有空闲页面编号的副本切片
|
||||
//
|
||||
// 设计考虑:
|
||||
// - 返回副本而不是原始切片,防止外部修改
|
||||
// - 提供只读访问,不影响内部状态
|
||||
// - 适用于调试、监控和诊断场景
|
||||
//
|
||||
// 使用示例:
|
||||
//
|
||||
// freePages := fpm.GetFreePages()
|
||||
// fmt.Printf("空闲页面: %v\n", freePages)
|
||||
// fmt.Printf("空闲页面数量: %d\n", len(freePages))
|
||||
//
|
||||
// 并发安全:使用互斥锁保护,确保读取一致性
|
||||
func (fpm *FreePageManager) GetFreePages() []uint16 {
|
||||
fpm.mu.Lock() // 获取互斥锁,保证读取一致性
|
||||
defer fpm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 创建空闲页面列表的完整副本
|
||||
result := make([]uint16, len(fpm.freePages))
|
||||
copy(result, fpm.freePages)
|
||||
|
||||
return result // 返回副本,防止外部修改原始数据
|
||||
}
|
||||
579
freepage_test.go
Normal file
579
freepage_test.go
Normal file
@@ -0,0 +1,579 @@
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestNewFreePageManager 测试空闲页面管理器的创建
|
||||
func TestNewFreePageManager(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
if fpm == nil {
|
||||
t.Fatal("NewFreePageManager returned nil")
|
||||
}
|
||||
|
||||
if fpm.FreeCount() != 0 {
|
||||
t.Errorf("initial free count = %d, want 0", fpm.FreeCount())
|
||||
}
|
||||
|
||||
// 验证初始状态下没有空闲页面
|
||||
freePages := fpm.GetFreePages()
|
||||
if len(freePages) != 0 {
|
||||
t.Errorf("initial free pages length = %d, want 0", len(freePages))
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerAllocPage 测试页面分配
|
||||
func TestFreePageManagerAllocPage(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 测试空管理器分配页面
|
||||
pageNo, ok := fpm.AllocPage()
|
||||
if ok {
|
||||
t.Error("AllocPage should return false when no free pages available")
|
||||
}
|
||||
if pageNo != 0 {
|
||||
t.Errorf("AllocPage returned pageNo = %d, want 0", pageNo)
|
||||
}
|
||||
|
||||
// 添加一些空闲页面
|
||||
testPages := []uint16{10, 20, 30}
|
||||
for _, page := range testPages {
|
||||
fpm.FreePage(page)
|
||||
}
|
||||
|
||||
// 验证页面数量
|
||||
if fpm.FreeCount() != len(testPages) {
|
||||
t.Errorf("free count = %d, want %d", fpm.FreeCount(), len(testPages))
|
||||
}
|
||||
|
||||
// 测试LIFO分配(后进先出)
|
||||
expectedOrder := []uint16{30, 20, 10} // 反向顺序
|
||||
for i, expected := range expectedOrder {
|
||||
pageNo, ok := fpm.AllocPage()
|
||||
if !ok {
|
||||
t.Errorf("AllocPage[%d] should return true", i)
|
||||
}
|
||||
if pageNo != expected {
|
||||
t.Errorf("AllocPage[%d] = %d, want %d", i, pageNo, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证所有页面都被分配完
|
||||
if fpm.FreeCount() != 0 {
|
||||
t.Errorf("free count after allocation = %d, want 0", fpm.FreeCount())
|
||||
}
|
||||
|
||||
// 再次尝试分配应该失败
|
||||
_, ok = fpm.AllocPage()
|
||||
if ok {
|
||||
t.Error("AllocPage should return false when all pages allocated")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerFreePage 测试页面释放
|
||||
func TestFreePageManagerFreePage(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 释放一个页面
|
||||
fpm.FreePage(100)
|
||||
|
||||
if fpm.FreeCount() != 1 {
|
||||
t.Errorf("free count after FreePage = %d, want 1", fpm.FreeCount())
|
||||
}
|
||||
|
||||
// 验证页面可以被分配
|
||||
pageNo, ok := fpm.AllocPage()
|
||||
if !ok || pageNo != 100 {
|
||||
t.Errorf("AllocPage = (%d, %t), want (100, true)", pageNo, ok)
|
||||
}
|
||||
|
||||
// 测试重复释放同一页面(应该是幂等操作)
|
||||
fpm.FreePage(200)
|
||||
fpm.FreePage(200) // 重复释放
|
||||
|
||||
if fpm.FreeCount() != 1 {
|
||||
t.Errorf("free count after duplicate FreePage = %d, want 1", fpm.FreeCount())
|
||||
}
|
||||
|
||||
// 验证只有一个页面200
|
||||
pageNo, ok = fpm.AllocPage()
|
||||
if !ok || pageNo != 200 {
|
||||
t.Errorf("AllocPage after duplicate free = (%d, %t), want (200, true)", pageNo, ok)
|
||||
}
|
||||
|
||||
// 验证没有更多页面
|
||||
_, ok = fpm.AllocPage()
|
||||
if ok {
|
||||
t.Error("should have no more pages after allocating the only one")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerGetFreePages 测试获取空闲页面列表
|
||||
func TestFreePageManagerGetFreePages(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 添加一些页面
|
||||
testPages := []uint16{5, 15, 25, 35}
|
||||
for _, page := range testPages {
|
||||
fpm.FreePage(page)
|
||||
}
|
||||
|
||||
// 获取空闲页面列表
|
||||
freePages := fpm.GetFreePages()
|
||||
|
||||
if len(freePages) != len(testPages) {
|
||||
t.Errorf("free pages length = %d, want %d", len(freePages), len(testPages))
|
||||
}
|
||||
|
||||
// 验证返回的是副本(修改不影响原始数据)
|
||||
originalCount := fpm.FreeCount()
|
||||
freePages[0] = 999 // 修改副本
|
||||
|
||||
if fpm.FreeCount() != originalCount {
|
||||
t.Error("modifying returned slice affected original data")
|
||||
}
|
||||
|
||||
// 验证原始数据未被修改
|
||||
newFreePages := fpm.GetFreePages()
|
||||
if newFreePages[0] == 999 {
|
||||
t.Error("original data was modified when returned slice was changed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerLoadFromHeader 测试从文件头加载空闲页面
|
||||
func TestFreePageManagerLoadFromHeader(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 模拟页面数据
|
||||
const testPageSize = 4096
|
||||
pages := make(map[uint16][]byte)
|
||||
|
||||
// 创建空闲页面链表:1 -> 2 -> 3 -> 0
|
||||
// 页面1
|
||||
page1 := make([]byte, testPageSize)
|
||||
binary.LittleEndian.PutUint16(page1[4:6], 2) // 下一页是2
|
||||
pages[1] = page1
|
||||
|
||||
// 页面2
|
||||
page2 := make([]byte, testPageSize)
|
||||
binary.LittleEndian.PutUint16(page2[4:6], 3) // 下一页是3
|
||||
pages[2] = page2
|
||||
|
||||
// 页面3
|
||||
page3 := make([]byte, testPageSize)
|
||||
binary.LittleEndian.PutUint16(page3[4:6], 0) // 链表结束
|
||||
pages[3] = page3
|
||||
|
||||
// 模拟读取函数
|
||||
readPageFunc := func(pageNo uint16) ([]byte, error) {
|
||||
if page, exists := pages[pageNo]; exists {
|
||||
return page, nil
|
||||
}
|
||||
return nil, errors.New("page not found")
|
||||
}
|
||||
|
||||
// 从头页面1开始加载
|
||||
err := fpm.LoadFromHeader(1, readPageFunc)
|
||||
if err != nil {
|
||||
t.Errorf("LoadFromHeader returned error: %v", err)
|
||||
}
|
||||
|
||||
// 验证加载的页面数量
|
||||
if fpm.FreeCount() != 3 {
|
||||
t.Errorf("free count after load = %d, want 3", fpm.FreeCount())
|
||||
}
|
||||
|
||||
// 验证页面顺序(应该按链表顺序加载)
|
||||
expectedPages := []uint16{1, 2, 3}
|
||||
freePages := fpm.GetFreePages()
|
||||
|
||||
for i, expected := range expectedPages {
|
||||
if freePages[i] != expected {
|
||||
t.Errorf("loaded page[%d] = %d, want %d", i, freePages[i], expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerLoadFromHeaderEmpty 测试加载空链表
|
||||
func TestFreePageManagerLoadFromHeaderEmpty(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 先添加一些页面
|
||||
fpm.FreePage(100)
|
||||
fpm.FreePage(200)
|
||||
|
||||
// 模拟读取函数(不会被调用)
|
||||
readPageFunc := func(pageNo uint16) ([]byte, error) {
|
||||
t.Error("readPageFunc should not be called for empty list")
|
||||
return nil, errors.New("unexpected call")
|
||||
}
|
||||
|
||||
// 从空链表加载(头页面为0)
|
||||
err := fpm.LoadFromHeader(0, readPageFunc)
|
||||
if err != nil {
|
||||
t.Errorf("LoadFromHeader with empty list returned error: %v", err)
|
||||
}
|
||||
|
||||
// 验证原有页面被清空
|
||||
if fpm.FreeCount() != 0 {
|
||||
t.Errorf("free count after loading empty list = %d, want 0", fpm.FreeCount())
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerLoadFromHeaderError 测试加载过程中的错误处理
|
||||
func TestFreePageManagerLoadFromHeaderError(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 模拟读取函数,第二次调用时返回错误
|
||||
callCount := 0
|
||||
readPageFunc := func(pageNo uint16) ([]byte, error) {
|
||||
callCount++
|
||||
if callCount == 2 {
|
||||
return nil, errors.New("simulated read error")
|
||||
}
|
||||
|
||||
// 第一次调用返回指向页面2的数据
|
||||
page := make([]byte, 4096)
|
||||
binary.LittleEndian.PutUint16(page[4:6], 2)
|
||||
return page, nil
|
||||
}
|
||||
|
||||
// 尝试加载,应该在第二次读取时失败
|
||||
err := fpm.LoadFromHeader(1, readPageFunc)
|
||||
if err == nil {
|
||||
t.Error("LoadFromHeader should return error when read fails")
|
||||
}
|
||||
|
||||
if err.Error() != "simulated read error" {
|
||||
t.Errorf("error message = %s, want 'simulated read error'", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerSaveToHeader 测试保存空闲页面到文件头
|
||||
func TestFreePageManagerSaveToHeader(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 添加一些空闲页面
|
||||
testPages := []uint16{10, 20, 30}
|
||||
for _, page := range testPages {
|
||||
fpm.FreePage(page)
|
||||
}
|
||||
|
||||
// 用于存储写入的页面数据
|
||||
writtenPages := make(map[uint16][]byte)
|
||||
|
||||
// 模拟写入函数
|
||||
writePageFunc := func(pageNo uint16, data []byte) error {
|
||||
writtenPages[pageNo] = make([]byte, len(data))
|
||||
copy(writtenPages[pageNo], data)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 保存到文件头
|
||||
headPage, err := fpm.SaveToHeader(writePageFunc)
|
||||
if err != nil {
|
||||
t.Errorf("SaveToHeader returned error: %v", err)
|
||||
}
|
||||
|
||||
// 验证返回的头页面
|
||||
if headPage != testPages[0] {
|
||||
t.Errorf("head page = %d, want %d", headPage, testPages[0])
|
||||
}
|
||||
|
||||
// 验证写入的页面数量
|
||||
if len(writtenPages) != len(testPages) {
|
||||
t.Errorf("written pages count = %d, want %d", len(writtenPages), len(testPages))
|
||||
}
|
||||
|
||||
// 验证链表结构
|
||||
for i, pageNo := range testPages {
|
||||
data, exists := writtenPages[pageNo]
|
||||
if !exists {
|
||||
t.Errorf("page %d was not written", pageNo)
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查下一页指针
|
||||
nextPage := binary.LittleEndian.Uint16(data[4:6])
|
||||
if i < len(testPages)-1 {
|
||||
// 不是最后一页,应该指向下一页
|
||||
expectedNext := testPages[i+1]
|
||||
if nextPage != expectedNext {
|
||||
t.Errorf("page %d next pointer = %d, want %d", pageNo, nextPage, expectedNext)
|
||||
}
|
||||
} else {
|
||||
// 最后一页,应该指向0
|
||||
if nextPage != 0 {
|
||||
t.Errorf("last page %d next pointer = %d, want 0", pageNo, nextPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerSaveToHeaderEmpty 测试保存空列表
|
||||
func TestFreePageManagerSaveToHeaderEmpty(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 模拟写入函数(不应该被调用)
|
||||
writePageFunc := func(pageNo uint16, data []byte) error {
|
||||
t.Error("writePageFunc should not be called for empty list")
|
||||
return errors.New("unexpected call")
|
||||
}
|
||||
|
||||
// 保存空列表
|
||||
headPage, err := fpm.SaveToHeader(writePageFunc)
|
||||
if err != nil {
|
||||
t.Errorf("SaveToHeader with empty list returned error: %v", err)
|
||||
}
|
||||
|
||||
// 验证返回的头页面为0
|
||||
if headPage != 0 {
|
||||
t.Errorf("head page for empty list = %d, want 0", headPage)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerSaveToHeaderError 测试保存过程中的错误处理
|
||||
func TestFreePageManagerSaveToHeaderError(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 添加一些页面
|
||||
fpm.FreePage(10)
|
||||
fpm.FreePage(20)
|
||||
|
||||
// 模拟写入函数,第二次调用时返回错误
|
||||
callCount := 0
|
||||
writePageFunc := func(pageNo uint16, data []byte) error {
|
||||
callCount++
|
||||
if callCount == 2 {
|
||||
return errors.New("simulated write error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 尝试保存,应该在第二次写入时失败
|
||||
headPage, err := fpm.SaveToHeader(writePageFunc)
|
||||
if err == nil {
|
||||
t.Error("SaveToHeader should return error when write fails")
|
||||
}
|
||||
|
||||
if err.Error() != "simulated write error" {
|
||||
t.Errorf("error message = %s, want 'simulated write error'", err.Error())
|
||||
}
|
||||
|
||||
if headPage != 0 {
|
||||
t.Errorf("head page on error = %d, want 0", headPage)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerRoundTrip 测试保存和加载的往返操作
|
||||
func TestFreePageManagerRoundTrip(t *testing.T) {
|
||||
// 创建第一个管理器并添加页面
|
||||
fpm1 := NewFreePageManager()
|
||||
originalPages := []uint16{100, 200, 300, 400}
|
||||
for _, page := range originalPages {
|
||||
fpm1.FreePage(page)
|
||||
}
|
||||
|
||||
// 用于存储页面数据的映射
|
||||
pageStorage := make(map[uint16][]byte)
|
||||
|
||||
// 保存到存储
|
||||
writePageFunc := func(pageNo uint16, data []byte) error {
|
||||
pageStorage[pageNo] = make([]byte, len(data))
|
||||
copy(pageStorage[pageNo], data)
|
||||
return nil
|
||||
}
|
||||
|
||||
headPage, err := fpm1.SaveToHeader(writePageFunc)
|
||||
if err != nil {
|
||||
t.Errorf("SaveToHeader failed: %v", err)
|
||||
}
|
||||
|
||||
// 创建第二个管理器并从存储加载
|
||||
fpm2 := NewFreePageManager()
|
||||
|
||||
readPageFunc := func(pageNo uint16) ([]byte, error) {
|
||||
if data, exists := pageStorage[pageNo]; exists {
|
||||
return data, nil
|
||||
}
|
||||
return nil, errors.New("page not found")
|
||||
}
|
||||
|
||||
err = fpm2.LoadFromHeader(headPage, readPageFunc)
|
||||
if err != nil {
|
||||
t.Errorf("LoadFromHeader failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证加载的页面与原始页面相同
|
||||
if fpm2.FreeCount() != len(originalPages) {
|
||||
t.Errorf("loaded free count = %d, want %d", fpm2.FreeCount(), len(originalPages))
|
||||
}
|
||||
|
||||
loadedPages := fpm2.GetFreePages()
|
||||
for i, expected := range originalPages {
|
||||
if loadedPages[i] != expected {
|
||||
t.Errorf("loaded page[%d] = %d, want %d", i, loadedPages[i], expected)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证分配顺序相同(LIFO)
|
||||
for i := len(originalPages) - 1; i >= 0; i-- {
|
||||
expected := originalPages[i]
|
||||
pageNo, ok := fpm2.AllocPage()
|
||||
if !ok || pageNo != expected {
|
||||
t.Errorf("AllocPage[%d] = (%d, %t), want (%d, true)", i, pageNo, ok, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerConcurrency 测试并发安全性
|
||||
func TestFreePageManagerConcurrency(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
const numGoroutines = 10
|
||||
const numOperations = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 启动多个goroutine进行并发操作
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
for j := 0; j < numOperations; j++ {
|
||||
pageNo := uint16(id*numOperations + j)
|
||||
|
||||
// 释放页面
|
||||
fpm.FreePage(pageNo)
|
||||
|
||||
// 尝试分配页面
|
||||
if allocPage, ok := fpm.AllocPage(); ok {
|
||||
// 再次释放分配到的页面
|
||||
fpm.FreePage(allocPage)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// 同时进行统计查询
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 50; i++ {
|
||||
fpm.FreeCount()
|
||||
fpm.GetFreePages()
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待所有goroutine完成
|
||||
wg.Wait()
|
||||
|
||||
// 验证管理器仍然可用
|
||||
fpm.FreePage(9999)
|
||||
pageNo, ok := fpm.AllocPage()
|
||||
if !ok || pageNo != 9999 {
|
||||
t.Error("manager corrupted after concurrent operations")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFreePageManagerLargeList 测试大量页面的处理
|
||||
func TestFreePageManagerLargeList(t *testing.T) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 添加大量页面
|
||||
const numPages = 1000
|
||||
for i := uint16(1); i <= numPages; i++ {
|
||||
fpm.FreePage(i)
|
||||
}
|
||||
|
||||
// 验证数量
|
||||
if fpm.FreeCount() != numPages {
|
||||
t.Errorf("free count = %d, want %d", fpm.FreeCount(), numPages)
|
||||
}
|
||||
|
||||
// 分配所有页面
|
||||
allocatedPages := make([]uint16, 0, numPages)
|
||||
for i := 0; i < numPages; i++ {
|
||||
pageNo, ok := fpm.AllocPage()
|
||||
if !ok {
|
||||
t.Errorf("AllocPage[%d] failed", i)
|
||||
break
|
||||
}
|
||||
allocatedPages = append(allocatedPages, pageNo)
|
||||
}
|
||||
|
||||
// 验证分配完毕
|
||||
if fpm.FreeCount() != 0 {
|
||||
t.Errorf("free count after allocation = %d, want 0", fpm.FreeCount())
|
||||
}
|
||||
|
||||
// 验证LIFO顺序(最后添加的最先分配)
|
||||
for i, pageNo := range allocatedPages {
|
||||
expected := uint16(numPages - i)
|
||||
if pageNo != expected {
|
||||
t.Errorf("allocated page[%d] = %d, want %d", i, pageNo, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFreePageManagerAllocPage 性能测试:页面分配
|
||||
func BenchmarkFreePageManagerAllocPage(b *testing.B) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 预填充大量空闲页面
|
||||
for i := uint16(0); i < 10000; i++ {
|
||||
fpm.FreePage(i)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
fpm.AllocPage()
|
||||
if fpm.FreeCount() == 0 {
|
||||
// 重新填充
|
||||
for j := uint16(0); j < 10000; j++ {
|
||||
fpm.FreePage(j)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFreePageManagerFreePage 性能测试:页面释放
|
||||
func BenchmarkFreePageManagerFreePage(b *testing.B) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
fpm.FreePage(uint16(i % 10000))
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFreePageManagerConcurrentAccess 性能测试:并发访问
|
||||
func BenchmarkFreePageManagerConcurrentAccess(b *testing.B) {
|
||||
fpm := NewFreePageManager()
|
||||
|
||||
// 预填充一些页面
|
||||
for i := uint16(0); i < 1000; i++ {
|
||||
fpm.FreePage(i)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
if i%2 == 0 {
|
||||
fpm.AllocPage()
|
||||
} else {
|
||||
fpm.FreePage(uint16(i % 1000))
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
5
go.mod
Normal file
5
go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module code.tczkiot.com/wlw/pipelinedb
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require github.com/google/btree v1.1.3
|
||||
2
go.sum
Normal file
2
go.sum
Normal file
@@ -0,0 +1,2 @@
|
||||
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||
626
group_manager.go
Normal file
626
group_manager.go
Normal file
@@ -0,0 +1,626 @@
|
||||
// Package pipelinedb provides an integrated pipeline database system
|
||||
// 集成了数据库存储和业务管道处理的一体化解决方案
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// GroupStatsCache 组统计信息缓存结构
|
||||
//
|
||||
// 核心功能:
|
||||
// - 缓存每个组的统计信息,提供O(1)的统计查询性能
|
||||
// - 支持按数据状态分类统计(Hot/Warm/Cold)
|
||||
// - 实时更新,确保统计数据的准确性
|
||||
//
|
||||
// 设计考虑:
|
||||
// - 使用整数类型提供高效的计数操作
|
||||
// - 支持JSON序列化,便于数据交换和持久化
|
||||
// - 结构简单,内存占用小
|
||||
//
|
||||
// 适用场景:
|
||||
// - 数据库性能监控
|
||||
// - 组级别的数据分析
|
||||
// - 缓存命中率统计
|
||||
type GroupStatsCache struct {
|
||||
TotalRecords int `json:"total_records"` // 总记录数(所有状态的记录总和)
|
||||
HotRecords int `json:"hot_records"` // 热数据记录数(最新、最活跃的数据)
|
||||
WarmRecords int `json:"warm_records"` // 温数据记录数(中等活跃度的数据)
|
||||
ColdRecords int `json:"cold_records"` // 冷数据记录数(较少访问的数据)
|
||||
}
|
||||
|
||||
// GroupManager 数据组管理器
|
||||
//
|
||||
// 核心职责:
|
||||
// 1. 管理数据组的生命周期(暂停/恢复)
|
||||
// 2. 维护组级别的ID自增计数器
|
||||
// 3. 缓存组统计信息,提供高性能统计查询
|
||||
// 4. 管理组的OnComplete回调执行状态
|
||||
// 5. 提供组状态的持久化存储
|
||||
//
|
||||
// 设计思想:
|
||||
// - 使用读写锁支持高并发访问
|
||||
// - 统计信息缓存在内存中,避免频繁的磁盘I/O
|
||||
// - ID计数器持久化到专用页面,确保重启后的连续性
|
||||
// - 状态管理支持复杂的业务流程控制
|
||||
//
|
||||
// 性能特征:
|
||||
// - 统计查询:O(1) - 直接从内存缓存读取
|
||||
// - ID生成:O(1) - 内存计数器自增
|
||||
// - 状态检查:O(1) - HashMap查找
|
||||
// - 并发友好:读多写少的场景下性能优异
|
||||
//
|
||||
// 适用场景:
|
||||
// - 多租户数据库系统
|
||||
// - 分组数据处理管道
|
||||
// - 需要组级别控制的业务系统
|
||||
type GroupManager struct {
|
||||
pausedGroups map[string]bool // 暂停状态的组集合(key=组名,value=true表示暂停)
|
||||
onCompleteExecuting map[string]bool // 正在执行OnComplete回调的组集合
|
||||
groupCounters map[string]*int64 // 每个组的ID自增计数器(持久化到磁盘)
|
||||
groupStats map[string]*GroupStatsCache // 组统计信息缓存(内存中维护)
|
||||
mu sync.RWMutex // 读写锁,保护所有内部状态的并发访问
|
||||
pdb *PipelineDB // 数据库实例引用,用于计数器持久化
|
||||
}
|
||||
|
||||
// NewGroupManager 创建一个新的组管理器实例
|
||||
//
|
||||
// 核心功能:初始化组管理器的所有内部数据结构
|
||||
// 时间复杂度:O(1) - 只创建空的数据结构
|
||||
// 空间复杂度:O(1) - 初始状态下不占用额外空间
|
||||
//
|
||||
// 参数说明:
|
||||
// - pdb: PipelineDB实例引用,用于ID计数器的持久化存储
|
||||
// 如果为nil,计数器功能将不可用,但其他功能正常
|
||||
//
|
||||
// 返回值:
|
||||
// - *GroupManager: 完全初始化的组管理器实例
|
||||
//
|
||||
// 初始化内容:
|
||||
// 1. 创建空的暂停组映射
|
||||
// 2. 创建空的OnComplete执行状态映射
|
||||
// 3. 创建空的组计数器映射
|
||||
// 4. 创建空的组统计缓存映射
|
||||
// 5. 设置数据库引用
|
||||
//
|
||||
// 使用示例:
|
||||
//
|
||||
// gm := NewGroupManager(pipelineDB)
|
||||
// gm.PauseGroup("batch1") // 暂停组
|
||||
// id := gm.GetNextID("user") // 获取ID
|
||||
func NewGroupManager(pdb *PipelineDB) *GroupManager {
|
||||
return &GroupManager{
|
||||
pausedGroups: make(map[string]bool), // 初始化暂停组映射
|
||||
onCompleteExecuting: make(map[string]bool), // 初始化OnComplete执行状态映射
|
||||
groupCounters: make(map[string]*int64), // 初始化组计数器映射
|
||||
groupStats: make(map[string]*GroupStatsCache), // 初始化组统计缓存映射
|
||||
pdb: pdb, // 设置数据库引用
|
||||
// mu 会自动初始化为零值(未锁定状态)
|
||||
}
|
||||
}
|
||||
|
||||
// PauseGroup 暂停指定组的数据接收功能
|
||||
//
|
||||
// 核心功能:将组标记为暂停状态,阻止新数据的接收
|
||||
// 时间复杂度:O(1) - HashMap插入操作
|
||||
// 空间复杂度:O(1) - 只添加一个映射条目
|
||||
//
|
||||
// 参数说明:
|
||||
// - group: 要暂停的组名
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取写锁(独占访问)
|
||||
// 2. 将组添加到暂停组映射中
|
||||
// 3. 输出暂停确认信息
|
||||
//
|
||||
// 使用场景:
|
||||
// - 维护期间暂停特定组的数据处理
|
||||
// - 数据质量问题时临时停止数据接收
|
||||
// - 系统负载过高时选择性暂停部分组
|
||||
// - 业务流程控制需要
|
||||
//
|
||||
// 注意事项:
|
||||
// - 暂停后该组无法接收新数据,但已有数据继续处理
|
||||
// - 可以通过ResumeGroup恢复
|
||||
// - 暂停状态不会持久化,重启后会丢失
|
||||
//
|
||||
// 并发安全:使用写锁保护,确保状态修改的原子性
|
||||
func (gm *GroupManager) PauseGroup(group string) {
|
||||
gm.mu.Lock() // 获取写锁,独占访问
|
||||
defer gm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 将组标记为暂停状态
|
||||
gm.pausedGroups[group] = true
|
||||
|
||||
// 输出暂停确认信息(便于监控和调试)
|
||||
gm.pdb.logger.Info("⏸️ 组已暂停接收数据", "group", group)
|
||||
}
|
||||
|
||||
// ResumeGroup 恢复指定组的数据接收功能
|
||||
//
|
||||
// 核心功能:将组从暂停状态中移除,恢复正常的数据接收
|
||||
// 时间复杂度:O(1) - HashMap删除操作
|
||||
// 空间复杂度:O(1) - 释放一个映射条目的空间
|
||||
//
|
||||
// 参数说明:
|
||||
// - group: 要恢复的组名
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取写锁(独占访问)
|
||||
// 2. 从暂停组映射中删除该组
|
||||
// 3. 输出恢复确认信息
|
||||
//
|
||||
// 使用场景:
|
||||
// - 维护完成后恢复组的正常运行
|
||||
// - 数据质量问题解决后重新启用组
|
||||
// - 系统负载降低后恢复暂停的组
|
||||
// - 业务流程需要重新激活组
|
||||
//
|
||||
// 注意事项:
|
||||
// - 恢复后该组立即可以接收新数据
|
||||
// - 如果组本来就没有暂停,操作是幂等的(无副作用)
|
||||
// - 恢复状态不会持久化,重启后默认为活跃状态
|
||||
//
|
||||
// 并发安全:使用写锁保护,确保状态修改的原子性
|
||||
func (gm *GroupManager) ResumeGroup(group string) {
|
||||
gm.mu.Lock() // 获取写锁,独占访问
|
||||
defer gm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 从暂停组映射中删除该组(恢复活跃状态)
|
||||
delete(gm.pausedGroups, group)
|
||||
|
||||
// 输出恢复确认信息(便于监控和调试)
|
||||
gm.pdb.logger.Info("▶️ 组已恢复接收数据", "group", group)
|
||||
}
|
||||
|
||||
// IsPaused 检查指定组是否处于暂停状态
|
||||
//
|
||||
// 核心功能:查询组的暂停状态,用于数据接收前的状态检查
|
||||
// 时间复杂度:O(1) - HashMap查找操作
|
||||
// 空间复杂度:O(1) - 不创建额外数据
|
||||
//
|
||||
// 参数说明:
|
||||
// - group: 要检查的组名
|
||||
//
|
||||
// 返回值:
|
||||
// - bool: true表示组已暂停,false表示组处于活跃状态
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取读锁(允许并发读取)
|
||||
// 2. 在暂停组映射中查找该组
|
||||
// 3. 返回查找结果
|
||||
//
|
||||
// 使用场景:
|
||||
// - 数据接收前的状态验证
|
||||
// - 业务逻辑中的条件判断
|
||||
// - 监控和状态报告
|
||||
// - 自动化运维脚本
|
||||
//
|
||||
// 设计考虑:
|
||||
// - 使用读锁支持高并发查询
|
||||
// - HashMap查找提供O(1)性能
|
||||
// - 不存在的组默认返回false(活跃状态)
|
||||
//
|
||||
// 并发安全:使用读锁保护,支持多线程并发调用
|
||||
func (gm *GroupManager) IsPaused(group string) bool {
|
||||
gm.mu.RLock() // 获取读锁,允许并发读取
|
||||
defer gm.mu.RUnlock() // 确保函数退出时释放锁
|
||||
|
||||
// 在暂停组映射中查找该组
|
||||
// 如果组不存在,返回false(默认为活跃状态)
|
||||
return gm.pausedGroups[group]
|
||||
}
|
||||
|
||||
// GetPausedGroups 获取所有暂停的组
|
||||
func (gm *GroupManager) GetPausedGroups() []string {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
|
||||
var groups []string
|
||||
for group := range gm.pausedGroups {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// GetGroupStatus 获取组状态
|
||||
func (gm *GroupManager) GetGroupStatus(group string) string {
|
||||
if gm.IsPaused(group) {
|
||||
return "paused"
|
||||
}
|
||||
return "active"
|
||||
}
|
||||
|
||||
// IsOnCompleteExecuting 检查组是否正在执行 OnComplete
|
||||
func (gm *GroupManager) IsOnCompleteExecuting(group string) bool {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
return gm.onCompleteExecuting[group]
|
||||
}
|
||||
|
||||
// SetOnCompleteExecuting 设置组的 OnComplete 执行状态
|
||||
func (gm *GroupManager) SetOnCompleteExecuting(group string, executing bool) {
|
||||
gm.mu.Lock()
|
||||
defer gm.mu.Unlock()
|
||||
if executing {
|
||||
gm.onCompleteExecuting[group] = true
|
||||
gm.pdb.logger.Info("🔒 组 OnComplete 开始执行", "group", group)
|
||||
} else {
|
||||
delete(gm.onCompleteExecuting, group)
|
||||
gm.pdb.logger.Info("🔓 组 OnComplete 执行完成", "group", group)
|
||||
}
|
||||
}
|
||||
|
||||
// GetExecutingGroups 获取所有正在执行 OnComplete 的组
|
||||
func (gm *GroupManager) GetExecutingGroups() []string {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
|
||||
var groups []string
|
||||
for group := range gm.onCompleteExecuting {
|
||||
groups = append(groups, group)
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
// GetNextID 为指定组生成下一个唯一的自增ID
|
||||
//
|
||||
// 核心功能:为每个组维护独立的ID序列,确保ID的唯一性和连续性
|
||||
// 时间复杂度:O(1) - 内存计数器自增 + 持久化写入
|
||||
// 空间复杂度:O(1) - 只维护单个计数器
|
||||
//
|
||||
// 参数说明:
|
||||
// - group: 组名,每个组有独立的ID序列
|
||||
//
|
||||
// 返回值:
|
||||
// - int64: 新生成的唯一ID(从1开始递增)
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取写锁(保证ID生成的原子性)
|
||||
// 2. 检查组计数器是否已加载:
|
||||
// a. 如果未加载:从持久化存储中加载计数器
|
||||
// b. 如果已加载:直接使用内存中的计数器
|
||||
// 3. 计数器自增1
|
||||
// 4. 将新的计数器值持久化到磁盘
|
||||
// 5. 返回新生成的ID
|
||||
//
|
||||
// 持久化机制:
|
||||
// - 计数器存储在专用的计数器页面中
|
||||
// - 每次ID生成都会立即持久化,确保重启后的连续性
|
||||
// - 页面格式:[组名长度][组名][计数器值]...
|
||||
//
|
||||
// 设计考虑:
|
||||
// - 每个组独立的ID序列,避免冲突
|
||||
// - 立即持久化确保数据安全性
|
||||
// - 内存缓存提高性能
|
||||
// - 写锁保证并发安全
|
||||
//
|
||||
// 使用场景:
|
||||
// - 数据记录的主键生成
|
||||
// - 业务对象的唯一标识
|
||||
// - 序列号生成
|
||||
//
|
||||
// 并发安全:使用写锁保护,确保ID生成的唯一性
|
||||
func (gm *GroupManager) GetNextID(group string) int64 {
|
||||
gm.mu.Lock() // 获取写锁,保证ID生成的原子性
|
||||
defer gm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 检查组的计数器是否已经加载到内存中
|
||||
if gm.groupCounters[group] == nil {
|
||||
// 计数器未加载,从持久化存储中加载
|
||||
counter := gm.loadCounterFromPage(group)
|
||||
gm.groupCounters[group] = &counter
|
||||
}
|
||||
|
||||
// 计数器自增,生成新的ID
|
||||
*gm.groupCounters[group]++
|
||||
newID := *gm.groupCounters[group]
|
||||
|
||||
// 立即将新的计数器值持久化到磁盘
|
||||
// 这确保了即使系统崩溃,ID序列也不会重复
|
||||
gm.saveCounterToPage(group, newID)
|
||||
|
||||
return newID // 返回新生成的唯一ID
|
||||
}
|
||||
|
||||
// GetGroupCounter 获取组的当前计数器值
|
||||
func (gm *GroupManager) GetGroupCounter(group string) int64 {
|
||||
gm.mu.RLock()
|
||||
defer gm.mu.RUnlock()
|
||||
|
||||
if gm.groupCounters[group] == nil {
|
||||
return 0
|
||||
}
|
||||
return *gm.groupCounters[group]
|
||||
}
|
||||
|
||||
// loadCounterFromPage 从页面加载组计数器
|
||||
func (gm *GroupManager) loadCounterFromPage(group string) int64 {
|
||||
if gm.pdb == nil || gm.pdb.header.CounterPage == 0 {
|
||||
return 0 // 如果没有计数器页面,返回0
|
||||
}
|
||||
|
||||
// 读取计数器页面
|
||||
page, err := gm.pdb.readPage(gm.pdb.header.CounterPage)
|
||||
if err != nil {
|
||||
gm.pdb.logger.Warn("⚠️ 读取计数器页面失败", "error", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
// 在页面中查找组的计数器
|
||||
// 页面格式:[组名长度(2字节)][组名][计数器(8字节)]...
|
||||
offset := uint16(0)
|
||||
for offset < uint16(len(page)-10) { // 至少需要2+1+8=11字节
|
||||
// 读取组名长度
|
||||
nameLen := binary.LittleEndian.Uint16(page[offset : offset+2])
|
||||
if nameLen == 0 || offset+2+nameLen+8 > uint16(len(page)) {
|
||||
break
|
||||
}
|
||||
|
||||
// 读取组名
|
||||
name := string(page[offset+2 : offset+2+nameLen])
|
||||
if name == group {
|
||||
// 找到了,读取计数器
|
||||
counter := binary.LittleEndian.Uint64(page[offset+2+nameLen : offset+2+nameLen+8])
|
||||
return int64(counter)
|
||||
}
|
||||
|
||||
// 移动到下一个条目
|
||||
offset += 2 + nameLen + 8
|
||||
}
|
||||
|
||||
return 0 // 没找到,返回0
|
||||
}
|
||||
|
||||
// saveCounterToPage 保存组计数器到页面
|
||||
func (gm *GroupManager) saveCounterToPage(group string, counter int64) {
|
||||
if gm.pdb == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果没有计数器页面,创建一个
|
||||
if gm.pdb.header.CounterPage == 0 {
|
||||
gm.initCounterPage()
|
||||
}
|
||||
|
||||
// 读取计数器页面
|
||||
page, err := gm.pdb.readPage(gm.pdb.header.CounterPage)
|
||||
if err != nil {
|
||||
gm.pdb.logger.Warn("⚠️ 读取计数器页面失败", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 查找并更新组的计数器
|
||||
offset := uint16(0)
|
||||
found := false
|
||||
|
||||
for offset < uint16(len(page)-10) {
|
||||
// 读取组名长度
|
||||
nameLen := binary.LittleEndian.Uint16(page[offset : offset+2])
|
||||
if nameLen == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
if offset+2+nameLen+8 > uint16(len(page)) {
|
||||
break
|
||||
}
|
||||
|
||||
// 读取组名
|
||||
name := string(page[offset+2 : offset+2+nameLen])
|
||||
if name == group {
|
||||
// 找到了,更新计数器
|
||||
binary.LittleEndian.PutUint64(page[offset+2+nameLen:offset+2+nameLen+8], uint64(counter))
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
// 移动到下一个条目
|
||||
offset += 2 + nameLen + 8
|
||||
}
|
||||
|
||||
// 如果没找到,添加新条目
|
||||
if !found {
|
||||
nameLen := uint16(len(group))
|
||||
if offset+2+nameLen+8 <= uint16(len(page)) {
|
||||
// 写入组名长度
|
||||
binary.LittleEndian.PutUint16(page[offset:offset+2], nameLen)
|
||||
// 写入组名
|
||||
copy(page[offset+2:offset+2+nameLen], []byte(group))
|
||||
// 写入计数器
|
||||
binary.LittleEndian.PutUint64(page[offset+2+nameLen:offset+2+nameLen+8], uint64(counter))
|
||||
}
|
||||
}
|
||||
|
||||
// 写回页面
|
||||
gm.pdb.cache.Put(gm.pdb.header.CounterPage, page, true)
|
||||
}
|
||||
|
||||
// initCounterPage 初始化计数器页面
|
||||
func (gm *GroupManager) initCounterPage() {
|
||||
if gm.pdb == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// 分配新页面
|
||||
pageNo := gm.pdb.header.TotalPages
|
||||
gm.pdb.header.TotalPages++
|
||||
gm.pdb.header.CounterPage = pageNo
|
||||
|
||||
// 创建空页面
|
||||
page := make([]byte, PageSize)
|
||||
|
||||
// 写入页面
|
||||
gm.pdb.cache.Put(pageNo, page, true)
|
||||
|
||||
// 保存头部
|
||||
gm.pdb.saveHeader()
|
||||
|
||||
gm.pdb.logger.Info("📄 初始化计数器页面", "page", pageNo)
|
||||
}
|
||||
|
||||
// IncrementStats 增加组的统计计数(新增记录时调用)
|
||||
//
|
||||
// 核心功能:当新记录添加到组时,更新该组的统计信息
|
||||
// 时间复杂度:O(1) - 直接的计数器操作
|
||||
// 空间复杂度:O(1) - 可能创建一个新的统计缓存结构
|
||||
//
|
||||
// 参数说明:
|
||||
// - group: 组名
|
||||
// - status: 新记录的数据状态(Hot/Warm/Cold)
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取写锁(保证统计更新的原子性)
|
||||
// 2. 确保组的统计缓存已初始化
|
||||
// 3. 增加总记录数计数
|
||||
// 4. 根据状态增加对应的状态计数
|
||||
//
|
||||
// 统计维护:
|
||||
// - TotalRecords: 总记录数自增1
|
||||
// - 对应状态计数: Hot/Warm/Cold计数自增1
|
||||
// - 统计信息实时更新,无延迟
|
||||
//
|
||||
// 使用场景:
|
||||
// - 新数据记录插入时
|
||||
// - 数据导入过程中
|
||||
// - 批量数据处理时
|
||||
//
|
||||
// 并发安全:使用写锁保护,确保统计数据的一致性
|
||||
func (gm *GroupManager) IncrementStats(group string, status DataStatus) {
|
||||
gm.mu.Lock() // 获取写锁,保证统计更新的原子性
|
||||
defer gm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 确保组的统计缓存已经初始化
|
||||
if gm.groupStats[group] == nil {
|
||||
gm.groupStats[group] = &GroupStatsCache{}
|
||||
}
|
||||
|
||||
// 增加总记录数
|
||||
gm.groupStats[group].TotalRecords++
|
||||
|
||||
// 根据数据状态增加对应的计数
|
||||
switch status {
|
||||
case StatusHot:
|
||||
gm.groupStats[group].HotRecords++ // 增加热数据计数
|
||||
case StatusWarm:
|
||||
gm.groupStats[group].WarmRecords++ // 增加温数据计数
|
||||
case StatusCold:
|
||||
gm.groupStats[group].ColdRecords++ // 增加冷数据计数
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateStats 更新组统计信息(数据状态转换时调用)
|
||||
//
|
||||
// 核心功能:当记录的状态发生转换时,更新统计信息以保持准确性
|
||||
// 时间复杂度:O(1) - 直接的计数器操作
|
||||
// 空间复杂度:O(1) - 可能创建一个新的统计缓存结构
|
||||
//
|
||||
// 参数说明:
|
||||
// - group: 组名
|
||||
// - oldStatus: 记录的原始状态
|
||||
// - newStatus: 记录的新状态
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取写锁(保证统计更新的原子性)
|
||||
// 2. 确保组的统计缓存已初始化
|
||||
// 3. 减少原状态的计数
|
||||
// 4. 增加新状态的计数
|
||||
// 5. 总记录数保持不变
|
||||
//
|
||||
// 状态转换处理:
|
||||
// - Hot → Warm: HotRecords-1, WarmRecords+1
|
||||
// - Warm → Cold: WarmRecords-1, ColdRecords+1
|
||||
// - 支持任意状态间的转换
|
||||
// - TotalRecords保持不变(只是状态分布改变)
|
||||
//
|
||||
// 使用场景:
|
||||
// - 数据生命周期管理(Hot→Warm→Cold)
|
||||
// - 缓存策略调整
|
||||
// - 数据归档过程
|
||||
//
|
||||
// 并发安全:使用写锁保护,确保统计数据的一致性
|
||||
func (gm *GroupManager) UpdateStats(group string, oldStatus, newStatus DataStatus) {
|
||||
gm.mu.Lock() // 获取写锁,保证统计更新的原子性
|
||||
defer gm.mu.Unlock() // 确保函数退出时释放锁
|
||||
|
||||
// 确保组的统计缓存已经初始化
|
||||
if gm.groupStats[group] == nil {
|
||||
gm.groupStats[group] = &GroupStatsCache{}
|
||||
}
|
||||
|
||||
// 减少原状态的计数
|
||||
switch oldStatus {
|
||||
case StatusHot:
|
||||
gm.groupStats[group].HotRecords-- // 减少热数据计数
|
||||
case StatusWarm:
|
||||
gm.groupStats[group].WarmRecords-- // 减少温数据计数
|
||||
case StatusCold:
|
||||
gm.groupStats[group].ColdRecords-- // 减少冷数据计数
|
||||
}
|
||||
|
||||
// 增加新状态的计数
|
||||
switch newStatus {
|
||||
case StatusHot:
|
||||
gm.groupStats[group].HotRecords++ // 增加热数据计数
|
||||
case StatusWarm:
|
||||
gm.groupStats[group].WarmRecords++ // 增加温数据计数
|
||||
case StatusCold:
|
||||
gm.groupStats[group].ColdRecords++ // 增加冷数据计数
|
||||
}
|
||||
|
||||
// 注意:TotalRecords保持不变,因为只是状态转换,记录总数没有变化
|
||||
}
|
||||
|
||||
// GetFastStats 快速获取所有组的统计信息(O(1)复杂度)
|
||||
//
|
||||
// 核心功能:从内存缓存中快速获取所有组的统计信息
|
||||
// 时间复杂度:O(n) - n为组的数量(需要复制所有统计数据)
|
||||
// 空间复杂度:O(n) - 创建所有统计数据的副本
|
||||
//
|
||||
// 返回值:
|
||||
// - map[string]*GroupStatsCache: 所有组的统计信息副本
|
||||
// key为组名,value为该组的统计信息
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 获取读锁(允许并发读取)
|
||||
// 2. 遍历所有组的统计缓存
|
||||
// 3. 为每个组创建统计信息的副本
|
||||
// 4. 返回完整的统计信息映射
|
||||
//
|
||||
// 设计考虑:
|
||||
// - 返回副本而不是原始数据,防止外部修改
|
||||
// - 使用读锁支持高并发查询
|
||||
// - 内存缓存提供极快的查询性能
|
||||
// - 避免磁盘I/O,适合频繁查询
|
||||
//
|
||||
// 性能优势:
|
||||
// - 相比传统的磁盘扫描统计,性能提升数百倍
|
||||
// - 支持高频率的监控查询
|
||||
// - 实时性强,统计数据始终是最新的
|
||||
//
|
||||
// 使用场景:
|
||||
// - 系统监控和仪表板
|
||||
// - 性能分析和报告
|
||||
// - 容量规划和预警
|
||||
// - API接口的统计查询
|
||||
//
|
||||
// 并发安全:使用读锁保护,支持多线程并发调用
|
||||
func (gm *GroupManager) GetFastStats() map[string]*GroupStatsCache {
|
||||
gm.mu.RLock() // 获取读锁,允许并发读取
|
||||
defer gm.mu.RUnlock() // 确保函数退出时释放锁
|
||||
|
||||
// 创建统计数据的完整副本
|
||||
result := make(map[string]*GroupStatsCache)
|
||||
|
||||
// 遍历所有组的统计缓存,创建副本
|
||||
for group, stats := range gm.groupStats {
|
||||
result[group] = &GroupStatsCache{
|
||||
TotalRecords: stats.TotalRecords, // 复制总记录数
|
||||
HotRecords: stats.HotRecords, // 复制热数据记录数
|
||||
WarmRecords: stats.WarmRecords, // 复制温数据记录数
|
||||
ColdRecords: stats.ColdRecords, // 复制冷数据记录数
|
||||
}
|
||||
}
|
||||
|
||||
return result // 返回统计信息副本
|
||||
}
|
||||
700
group_manager_test.go
Normal file
700
group_manager_test.go
Normal file
@@ -0,0 +1,700 @@
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MockPipelineDBForGroupManager 用于测试GroupManager的模拟数据库
|
||||
type MockPipelineDBForGroupManager struct {
|
||||
PipelineDB // 嵌入PipelineDB以支持类型转换
|
||||
pages map[uint16][]byte
|
||||
}
|
||||
|
||||
func NewMockPipelineDBForGroupManager() *MockPipelineDBForGroupManager {
|
||||
mock := &MockPipelineDBForGroupManager{
|
||||
pages: make(map[uint16][]byte),
|
||||
}
|
||||
// 初始化logger字段以避免nil指针错误
|
||||
mock.PipelineDB.logger = slog.Default()
|
||||
// 初始化header以避免nil指针错误
|
||||
mock.PipelineDB.header = &Header{
|
||||
Magic: 0x50444200, // PDB magic number
|
||||
PageSize: PageSize,
|
||||
TotalPages: 0,
|
||||
FreeHead: 0,
|
||||
RootPage: 0,
|
||||
CounterPage: 0, // 没有计数器页面
|
||||
}
|
||||
// 初始化缓存以避免nil指针错误
|
||||
mock.PipelineDB.cache = NewPageCache(10)
|
||||
// 初始化空闲页面管理器
|
||||
mock.PipelineDB.freePageMgr = NewFreePageManager()
|
||||
return mock
|
||||
}
|
||||
|
||||
func (db *MockPipelineDBForGroupManager) readPage(pageNo uint16) ([]byte, error) {
|
||||
if page, exists := db.pages[pageNo]; exists {
|
||||
return page, nil
|
||||
}
|
||||
return make([]byte, PageSize), nil
|
||||
}
|
||||
|
||||
func (db *MockPipelineDBForGroupManager) saveHeader() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestNewGroupManager 测试组管理器的创建
|
||||
func TestNewGroupManager(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
if gm == nil {
|
||||
t.Fatal("NewGroupManager returned nil")
|
||||
}
|
||||
|
||||
if gm.pdb == nil {
|
||||
t.Error("database reference not set correctly")
|
||||
}
|
||||
|
||||
// 验证初始状态
|
||||
if len(gm.GetPausedGroups()) != 0 {
|
||||
t.Error("should have no paused groups initially")
|
||||
}
|
||||
|
||||
if len(gm.GetExecutingGroups()) != 0 {
|
||||
t.Error("should have no executing groups initially")
|
||||
}
|
||||
|
||||
stats := gm.GetFastStats()
|
||||
if len(stats) != 0 {
|
||||
t.Error("should have no stats initially")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerPauseResume 测试组的暂停和恢复
|
||||
func TestGroupManagerPauseResume(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 初始状态应该是活跃的
|
||||
if gm.IsPaused(groupName) {
|
||||
t.Error("group should be active initially")
|
||||
}
|
||||
|
||||
if gm.GetGroupStatus(groupName) != "active" {
|
||||
t.Errorf("group status = %s, want 'active'", gm.GetGroupStatus(groupName))
|
||||
}
|
||||
|
||||
// 暂停组
|
||||
gm.PauseGroup(groupName)
|
||||
|
||||
if !gm.IsPaused(groupName) {
|
||||
t.Error("group should be paused after PauseGroup")
|
||||
}
|
||||
|
||||
if gm.GetGroupStatus(groupName) != "paused" {
|
||||
t.Errorf("group status = %s, want 'paused'", gm.GetGroupStatus(groupName))
|
||||
}
|
||||
|
||||
// 验证暂停组列表
|
||||
pausedGroups := gm.GetPausedGroups()
|
||||
if len(pausedGroups) != 1 || pausedGroups[0] != groupName {
|
||||
t.Errorf("paused groups = %v, want [%s]", pausedGroups, groupName)
|
||||
}
|
||||
|
||||
// 恢复组
|
||||
gm.ResumeGroup(groupName)
|
||||
|
||||
if gm.IsPaused(groupName) {
|
||||
t.Error("group should be active after ResumeGroup")
|
||||
}
|
||||
|
||||
if gm.GetGroupStatus(groupName) != "active" {
|
||||
t.Errorf("group status = %s, want 'active'", gm.GetGroupStatus(groupName))
|
||||
}
|
||||
|
||||
// 验证暂停组列表为空
|
||||
pausedGroups = gm.GetPausedGroups()
|
||||
if len(pausedGroups) != 0 {
|
||||
t.Errorf("paused groups = %v, want []", pausedGroups)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerMultipleGroups 测试多个组的管理
|
||||
func TestGroupManagerMultipleGroups(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groups := []string{"group1", "group2", "group3"}
|
||||
|
||||
// 暂停部分组
|
||||
gm.PauseGroup(groups[0])
|
||||
gm.PauseGroup(groups[2])
|
||||
|
||||
// 验证状态
|
||||
if !gm.IsPaused(groups[0]) {
|
||||
t.Errorf("group %s should be paused", groups[0])
|
||||
}
|
||||
|
||||
if gm.IsPaused(groups[1]) {
|
||||
t.Errorf("group %s should be active", groups[1])
|
||||
}
|
||||
|
||||
if !gm.IsPaused(groups[2]) {
|
||||
t.Errorf("group %s should be paused", groups[2])
|
||||
}
|
||||
|
||||
// 验证暂停组列表
|
||||
pausedGroups := gm.GetPausedGroups()
|
||||
if len(pausedGroups) != 2 {
|
||||
t.Errorf("paused groups count = %d, want 2", len(pausedGroups))
|
||||
}
|
||||
|
||||
// 验证包含正确的组(顺序可能不同)
|
||||
pausedMap := make(map[string]bool)
|
||||
for _, group := range pausedGroups {
|
||||
pausedMap[group] = true
|
||||
}
|
||||
|
||||
if !pausedMap[groups[0]] || !pausedMap[groups[2]] {
|
||||
t.Errorf("paused groups = %v, should contain %s and %s", pausedGroups, groups[0], groups[2])
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerOnCompleteExecution 测试OnComplete执行状态管理
|
||||
func TestGroupManagerOnCompleteExecution(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 初始状态不应该在执行
|
||||
if gm.IsOnCompleteExecuting(groupName) {
|
||||
t.Error("group should not be executing initially")
|
||||
}
|
||||
|
||||
// 设置为执行状态
|
||||
gm.SetOnCompleteExecuting(groupName, true)
|
||||
|
||||
if !gm.IsOnCompleteExecuting(groupName) {
|
||||
t.Error("group should be executing after setting to true")
|
||||
}
|
||||
|
||||
// 验证执行组列表
|
||||
executingGroups := gm.GetExecutingGroups()
|
||||
if len(executingGroups) != 1 || executingGroups[0] != groupName {
|
||||
t.Errorf("executing groups = %v, want [%s]", executingGroups, groupName)
|
||||
}
|
||||
|
||||
// 设置为非执行状态
|
||||
gm.SetOnCompleteExecuting(groupName, false)
|
||||
|
||||
if gm.IsOnCompleteExecuting(groupName) {
|
||||
t.Error("group should not be executing after setting to false")
|
||||
}
|
||||
|
||||
// 验证执行组列表为空
|
||||
executingGroups = gm.GetExecutingGroups()
|
||||
if len(executingGroups) != 0 {
|
||||
t.Errorf("executing groups = %v, want []", executingGroups)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerGetNextID 测试ID生成功能
|
||||
func TestGroupManagerGetNextID(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 第一次获取ID应该是1
|
||||
id1 := gm.GetNextID(groupName)
|
||||
if id1 != 1 {
|
||||
t.Errorf("first ID = %d, want 1", id1)
|
||||
}
|
||||
|
||||
// 后续ID应该递增
|
||||
id2 := gm.GetNextID(groupName)
|
||||
if id2 != 2 {
|
||||
t.Errorf("second ID = %d, want 2", id2)
|
||||
}
|
||||
|
||||
id3 := gm.GetNextID(groupName)
|
||||
if id3 != 3 {
|
||||
t.Errorf("third ID = %d, want 3", id3)
|
||||
}
|
||||
|
||||
// 验证计数器值
|
||||
counter := gm.GetGroupCounter(groupName)
|
||||
if counter != 3 {
|
||||
t.Errorf("group counter = %d, want 3", counter)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerMultipleGroupCounters 测试多个组的ID计数器
|
||||
func TestGroupManagerMultipleGroupCounters(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
group1 := "group1"
|
||||
group2 := "group2"
|
||||
|
||||
// 为不同组生成ID
|
||||
id1_1 := gm.GetNextID(group1)
|
||||
id2_1 := gm.GetNextID(group2)
|
||||
id1_2 := gm.GetNextID(group1)
|
||||
id2_2 := gm.GetNextID(group2)
|
||||
|
||||
// 验证每个组的ID独立递增
|
||||
if id1_1 != 1 || id1_2 != 2 {
|
||||
t.Errorf("group1 IDs = [%d, %d], want [1, 2]", id1_1, id1_2)
|
||||
}
|
||||
|
||||
if id2_1 != 1 || id2_2 != 2 {
|
||||
t.Errorf("group2 IDs = [%d, %d], want [1, 2]", id2_1, id2_2)
|
||||
}
|
||||
|
||||
// 验证计数器值
|
||||
if gm.GetGroupCounter(group1) != 2 {
|
||||
t.Errorf("group1 counter = %d, want 2", gm.GetGroupCounter(group1))
|
||||
}
|
||||
|
||||
if gm.GetGroupCounter(group2) != 2 {
|
||||
t.Errorf("group2 counter = %d, want 2", gm.GetGroupCounter(group2))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerIncrementStats 测试统计信息增量更新
|
||||
func TestGroupManagerIncrementStats(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 增加不同状态的记录
|
||||
gm.IncrementStats(groupName, StatusHot)
|
||||
gm.IncrementStats(groupName, StatusHot)
|
||||
gm.IncrementStats(groupName, StatusWarm)
|
||||
gm.IncrementStats(groupName, StatusCold)
|
||||
|
||||
// 获取统计信息
|
||||
stats := gm.GetFastStats()
|
||||
|
||||
if len(stats) != 1 {
|
||||
t.Errorf("stats count = %d, want 1", len(stats))
|
||||
}
|
||||
|
||||
groupStats := stats[groupName]
|
||||
if groupStats == nil {
|
||||
t.Fatal("group stats not found")
|
||||
}
|
||||
|
||||
// 验证统计数据
|
||||
if groupStats.TotalRecords != 4 {
|
||||
t.Errorf("total records = %d, want 4", groupStats.TotalRecords)
|
||||
}
|
||||
|
||||
if groupStats.HotRecords != 2 {
|
||||
t.Errorf("hot records = %d, want 2", groupStats.HotRecords)
|
||||
}
|
||||
|
||||
if groupStats.WarmRecords != 1 {
|
||||
t.Errorf("warm records = %d, want 1", groupStats.WarmRecords)
|
||||
}
|
||||
|
||||
if groupStats.ColdRecords != 1 {
|
||||
t.Errorf("cold records = %d, want 1", groupStats.ColdRecords)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerUpdateStats 测试统计信息状态转换更新
|
||||
func TestGroupManagerUpdateStats(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 先增加一些记录
|
||||
gm.IncrementStats(groupName, StatusHot)
|
||||
gm.IncrementStats(groupName, StatusHot)
|
||||
gm.IncrementStats(groupName, StatusWarm)
|
||||
|
||||
// 验证初始状态
|
||||
stats := gm.GetFastStats()[groupName]
|
||||
if stats.HotRecords != 2 || stats.WarmRecords != 1 || stats.ColdRecords != 0 {
|
||||
t.Errorf("initial stats: hot=%d, warm=%d, cold=%d, want hot=2, warm=1, cold=0",
|
||||
stats.HotRecords, stats.WarmRecords, stats.ColdRecords)
|
||||
}
|
||||
|
||||
// 执行状态转换:Hot -> Warm
|
||||
gm.UpdateStats(groupName, StatusHot, StatusWarm)
|
||||
|
||||
// 验证转换后的状态
|
||||
stats = gm.GetFastStats()[groupName]
|
||||
if stats.TotalRecords != 3 {
|
||||
t.Errorf("total records after update = %d, want 3", stats.TotalRecords)
|
||||
}
|
||||
|
||||
if stats.HotRecords != 1 {
|
||||
t.Errorf("hot records after update = %d, want 1", stats.HotRecords)
|
||||
}
|
||||
|
||||
if stats.WarmRecords != 2 {
|
||||
t.Errorf("warm records after update = %d, want 2", stats.WarmRecords)
|
||||
}
|
||||
|
||||
if stats.ColdRecords != 0 {
|
||||
t.Errorf("cold records after update = %d, want 0", stats.ColdRecords)
|
||||
}
|
||||
|
||||
// 执行另一个转换:Warm -> Cold
|
||||
gm.UpdateStats(groupName, StatusWarm, StatusCold)
|
||||
|
||||
// 验证第二次转换后的状态
|
||||
stats = gm.GetFastStats()[groupName]
|
||||
if stats.HotRecords != 1 || stats.WarmRecords != 1 || stats.ColdRecords != 1 {
|
||||
t.Errorf("final stats: hot=%d, warm=%d, cold=%d, want hot=1, warm=1, cold=1",
|
||||
stats.HotRecords, stats.WarmRecords, stats.ColdRecords)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerMultipleGroupStats 测试多个组的统计信息
|
||||
func TestGroupManagerMultipleGroupStats(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
// 为不同组添加统计数据
|
||||
groups := map[string]struct {
|
||||
hot, warm, cold int
|
||||
}{
|
||||
"group1": {3, 2, 1},
|
||||
"group2": {1, 4, 2},
|
||||
"group3": {2, 1, 3},
|
||||
}
|
||||
|
||||
for groupName, counts := range groups {
|
||||
for i := 0; i < counts.hot; i++ {
|
||||
gm.IncrementStats(groupName, StatusHot)
|
||||
}
|
||||
for i := 0; i < counts.warm; i++ {
|
||||
gm.IncrementStats(groupName, StatusWarm)
|
||||
}
|
||||
for i := 0; i < counts.cold; i++ {
|
||||
gm.IncrementStats(groupName, StatusCold)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有统计信息
|
||||
allStats := gm.GetFastStats()
|
||||
|
||||
if len(allStats) != len(groups) {
|
||||
t.Errorf("stats count = %d, want %d", len(allStats), len(groups))
|
||||
}
|
||||
|
||||
// 验证每个组的统计信息
|
||||
for groupName, expected := range groups {
|
||||
stats := allStats[groupName]
|
||||
if stats == nil {
|
||||
t.Errorf("stats for group %s not found", groupName)
|
||||
continue
|
||||
}
|
||||
|
||||
expectedTotal := expected.hot + expected.warm + expected.cold
|
||||
if stats.TotalRecords != expectedTotal {
|
||||
t.Errorf("group %s total = %d, want %d", groupName, stats.TotalRecords, expectedTotal)
|
||||
}
|
||||
|
||||
if stats.HotRecords != expected.hot {
|
||||
t.Errorf("group %s hot = %d, want %d", groupName, stats.HotRecords, expected.hot)
|
||||
}
|
||||
|
||||
if stats.WarmRecords != expected.warm {
|
||||
t.Errorf("group %s warm = %d, want %d", groupName, stats.WarmRecords, expected.warm)
|
||||
}
|
||||
|
||||
if stats.ColdRecords != expected.cold {
|
||||
t.Errorf("group %s cold = %d, want %d", groupName, stats.ColdRecords, expected.cold)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerStatsIsolation 测试统计信息的数据隔离
|
||||
func TestGroupManagerStatsIsolation(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "test_group"
|
||||
gm.IncrementStats(groupName, StatusHot)
|
||||
|
||||
// 获取统计信息
|
||||
stats1 := gm.GetFastStats()
|
||||
stats2 := gm.GetFastStats()
|
||||
|
||||
// 修改第一个返回的统计信息
|
||||
stats1[groupName].HotRecords = 999
|
||||
|
||||
// 验证第二个统计信息未被影响
|
||||
if stats2[groupName].HotRecords != 1 {
|
||||
t.Error("stats isolation failed: modifying returned stats affected original data")
|
||||
}
|
||||
|
||||
// 验证原始数据未被影响
|
||||
stats3 := gm.GetFastStats()
|
||||
if stats3[groupName].HotRecords != 1 {
|
||||
t.Error("original stats data was modified")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerConcurrency 测试并发安全性
|
||||
func TestGroupManagerConcurrency(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
const numGoroutines = 10
|
||||
const numOperations = 100
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// 启动多个goroutine进行并发操作
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
|
||||
groupName := "group_" + string(rune('A'+id%3))
|
||||
|
||||
for j := 0; j < numOperations; j++ {
|
||||
// 并发的各种操作
|
||||
switch j % 6 {
|
||||
case 0:
|
||||
gm.PauseGroup(groupName)
|
||||
case 1:
|
||||
gm.ResumeGroup(groupName)
|
||||
case 2:
|
||||
gm.SetOnCompleteExecuting(groupName, true)
|
||||
case 3:
|
||||
gm.SetOnCompleteExecuting(groupName, false)
|
||||
case 4:
|
||||
gm.GetNextID(groupName)
|
||||
case 5:
|
||||
gm.IncrementStats(groupName, StatusHot)
|
||||
}
|
||||
|
||||
// 并发读取操作
|
||||
gm.IsPaused(groupName)
|
||||
gm.IsOnCompleteExecuting(groupName)
|
||||
gm.GetGroupCounter(groupName)
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
// 同时进行统计查询
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < 50; i++ {
|
||||
gm.GetFastStats()
|
||||
gm.GetPausedGroups()
|
||||
gm.GetExecutingGroups()
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待所有goroutine完成
|
||||
wg.Wait()
|
||||
|
||||
// 验证管理器仍然可用
|
||||
testGroup := "final_test"
|
||||
gm.PauseGroup(testGroup)
|
||||
if !gm.IsPaused(testGroup) {
|
||||
t.Error("manager corrupted after concurrent operations")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerIdempotentOperations 测试幂等操作
|
||||
func TestGroupManagerIdempotentOperations(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 多次暂停同一组
|
||||
gm.PauseGroup(groupName)
|
||||
gm.PauseGroup(groupName)
|
||||
gm.PauseGroup(groupName)
|
||||
|
||||
// 应该只有一个暂停的组
|
||||
pausedGroups := gm.GetPausedGroups()
|
||||
if len(pausedGroups) != 1 {
|
||||
t.Errorf("paused groups count = %d, want 1", len(pausedGroups))
|
||||
}
|
||||
|
||||
// 多次恢复同一组
|
||||
gm.ResumeGroup(groupName)
|
||||
gm.ResumeGroup(groupName)
|
||||
gm.ResumeGroup(groupName)
|
||||
|
||||
// 应该没有暂停的组
|
||||
pausedGroups = gm.GetPausedGroups()
|
||||
if len(pausedGroups) != 0 {
|
||||
t.Errorf("paused groups count = %d, want 0", len(pausedGroups))
|
||||
}
|
||||
|
||||
// 多次设置OnComplete执行状态
|
||||
gm.SetOnCompleteExecuting(groupName, true)
|
||||
gm.SetOnCompleteExecuting(groupName, true)
|
||||
|
||||
executingGroups := gm.GetExecutingGroups()
|
||||
if len(executingGroups) != 1 {
|
||||
t.Errorf("executing groups count = %d, want 1", len(executingGroups))
|
||||
}
|
||||
|
||||
gm.SetOnCompleteExecuting(groupName, false)
|
||||
gm.SetOnCompleteExecuting(groupName, false)
|
||||
|
||||
executingGroups = gm.GetExecutingGroups()
|
||||
if len(executingGroups) != 0 {
|
||||
t.Errorf("executing groups count = %d, want 0", len(executingGroups))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerGetGroupCounterNonExistent 测试获取不存在组的计数器
|
||||
func TestGroupManagerGetGroupCounterNonExistent(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
// 获取不存在组的计数器应该返回0
|
||||
counter := gm.GetGroupCounter("non_existent_group")
|
||||
if counter != 0 {
|
||||
t.Errorf("counter for non-existent group = %d, want 0", counter)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerStatsInitialization 测试统计信息的初始化
|
||||
func TestGroupManagerStatsInitialization(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 第一次增加统计应该初始化结构
|
||||
gm.IncrementStats(groupName, StatusHot)
|
||||
|
||||
stats := gm.GetFastStats()[groupName]
|
||||
if stats == nil {
|
||||
t.Fatal("stats should be initialized after first increment")
|
||||
}
|
||||
|
||||
if stats.TotalRecords != 1 || stats.HotRecords != 1 {
|
||||
t.Errorf("initial stats: total=%d, hot=%d, want total=1, hot=1",
|
||||
stats.TotalRecords, stats.HotRecords)
|
||||
}
|
||||
|
||||
if stats.WarmRecords != 0 || stats.ColdRecords != 0 {
|
||||
t.Errorf("initial stats: warm=%d, cold=%d, want warm=0, cold=0",
|
||||
stats.WarmRecords, stats.ColdRecords)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGroupManagerUpdateStatsInitialization 测试更新统计时的初始化
|
||||
func TestGroupManagerUpdateStatsInitialization(t *testing.T) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 直接调用UpdateStats应该初始化统计结构
|
||||
gm.UpdateStats(groupName, StatusHot, StatusWarm)
|
||||
|
||||
stats := gm.GetFastStats()[groupName]
|
||||
if stats == nil {
|
||||
t.Fatal("stats should be initialized after UpdateStats")
|
||||
}
|
||||
|
||||
// 由于没有Hot记录,减少操作不会产生负数,但会初始化结构
|
||||
if stats.TotalRecords != 0 {
|
||||
t.Errorf("total records = %d, want 0", stats.TotalRecords)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGroupManagerGetNextID 性能测试:ID生成
|
||||
func BenchmarkGroupManagerGetNextID(b *testing.B) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "bench_group"
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
gm.GetNextID(groupName)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGroupManagerIncrementStats 性能测试:统计信息增量
|
||||
func BenchmarkGroupManagerIncrementStats(b *testing.B) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
groupName := "bench_group"
|
||||
statuses := []DataStatus{StatusHot, StatusWarm, StatusCold}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
status := statuses[i%len(statuses)]
|
||||
gm.IncrementStats(groupName, status)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGroupManagerGetFastStats 性能测试:快速统计查询
|
||||
func BenchmarkGroupManagerGetFastStats(b *testing.B) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
// 预填充一些统计数据
|
||||
for i := 0; i < 10; i++ {
|
||||
groupName := "group_" + string(rune('A'+i))
|
||||
for j := 0; j < 100; j++ {
|
||||
gm.IncrementStats(groupName, StatusHot)
|
||||
}
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
gm.GetFastStats()
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGroupManagerConcurrentOperations 性能测试:并发操作
|
||||
func BenchmarkGroupManagerConcurrentOperations(b *testing.B) {
|
||||
mockDB := NewMockPipelineDBForGroupManager()
|
||||
gm := NewGroupManager(&mockDB.PipelineDB)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
i := 0
|
||||
for pb.Next() {
|
||||
groupName := "group_" + string(rune('A'+i%5))
|
||||
|
||||
switch i % 4 {
|
||||
case 0:
|
||||
gm.GetNextID(groupName)
|
||||
case 1:
|
||||
gm.IncrementStats(groupName, StatusHot)
|
||||
case 2:
|
||||
gm.IsPaused(groupName)
|
||||
case 3:
|
||||
gm.GetFastStats()
|
||||
}
|
||||
i++
|
||||
}
|
||||
})
|
||||
}
|
||||
651
index.go
Normal file
651
index.go
Normal file
@@ -0,0 +1,651 @@
|
||||
// Package pipelinedb provides an integrated pipeline database system
|
||||
// 集成了数据库存储和业务管道处理的一体化解决方案
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
|
||||
"github.com/google/btree"
|
||||
)
|
||||
|
||||
// IndexEntry B+Tree索引条目结构
|
||||
//
|
||||
// ...
|
||||
// 核心功能:
|
||||
// - 存储记录ID到物理位置的映射关系
|
||||
// - 实现btree.Item接口,支持B+Tree操作
|
||||
// - 提供O(log n)的查找性能
|
||||
//
|
||||
// 设计思想:
|
||||
// - ID作为主键,确保唯一性和有序性
|
||||
// - PageNo和SlotNo组合定位记录的物理位置
|
||||
// - 结构紧凑,减少内存占用
|
||||
//
|
||||
// 物理位置编码:
|
||||
// - PageNo: 页面编号(16位,支持65536个页面)
|
||||
// - SlotNo: 页内槽位编号(16位,支持65536个槽位)
|
||||
// - 组合可定位约42亿个记录位置
|
||||
//
|
||||
// 适用场景:
|
||||
// - 数据库主键索引
|
||||
// - 记录快速定位
|
||||
// - 范围查询支持
|
||||
type IndexEntry struct {
|
||||
ID int64 // 记录的唯一标识符(主键)
|
||||
PageNo uint16 // 记录所在的页面编号
|
||||
SlotNo uint16 // 记录在页面内的槽位编号
|
||||
}
|
||||
|
||||
// Less 实现btree.Item接口的比较方法
|
||||
//
|
||||
// 核心功能:定义索引条目的排序规则,支持B+Tree的有序存储
|
||||
// 时间复杂度:O(1) - 直接的整数比较
|
||||
// 空间复杂度:O(1) - 不创建额外数据
|
||||
//
|
||||
// 参数说明:
|
||||
// - b: 要比较的另一个btree.Item(必须是IndexEntry类型)
|
||||
//
|
||||
// 返回值:
|
||||
// - bool: true表示当前条目小于参数条目,false表示大于等于
|
||||
//
|
||||
// 排序规则:
|
||||
// - 按记录ID升序排列
|
||||
// - ID相同时认为相等(不小于)
|
||||
// - 支持B+Tree的二分查找和范围查询
|
||||
//
|
||||
// 使用场景:
|
||||
// - B+Tree内部排序和查找
|
||||
// - 范围查询时的边界确定
|
||||
// - 索引条目的插入位置确定
|
||||
//
|
||||
// 注意事项:
|
||||
// - 必须确保传入的参数是IndexEntry类型,否则会panic
|
||||
// - 该方法被B+Tree库频繁调用,性能要求高
|
||||
func (a IndexEntry) Less(b btree.Item) bool {
|
||||
// 按记录ID进行升序比较
|
||||
// 类型断言:将btree.Item转换为IndexEntry
|
||||
return a.ID < b.(IndexEntry).ID
|
||||
}
|
||||
|
||||
// TableIndex 组级索引管理器(注意:虽然名称是TableIndex,但实际管理的是组索引)
|
||||
//
|
||||
// 核心职责:
|
||||
// 1. 维护单个组内所有记录的ID到物理位置的映射
|
||||
// 2. 提供高效的记录查找、插入、删除操作
|
||||
// 3. 支持范围查询和统计功能
|
||||
// 4. 管理索引的生命周期
|
||||
//
|
||||
// 设计思想:
|
||||
// - 使用B+Tree提供O(log n)的查找性能
|
||||
// - 支持范围查询,适合数据库场景
|
||||
// - 内存索引,提供极快的访问速度
|
||||
// - 度数为32的B+Tree,平衡内存使用和性能
|
||||
//
|
||||
// 性能特征:
|
||||
// - 查找:O(log n) - B+Tree查找
|
||||
// - 插入:O(log n) - B+Tree插入
|
||||
// - 删除:O(log n) - B+Tree删除
|
||||
// - 范围查询:O(log n + k) - k为结果数量
|
||||
// - 空间:O(n) - n为索引条目数量
|
||||
//
|
||||
// 适用场景:
|
||||
// - 数据库表的主键索引
|
||||
// - 记录快速定位系统
|
||||
// - 支持范围查询的索引系统
|
||||
type TableIndex struct {
|
||||
mu sync.RWMutex // 保护并发访问的读写锁
|
||||
tree *btree.BTree // B+Tree实例,存储IndexEntry
|
||||
name string // 索引名称(通常是组名)
|
||||
}
|
||||
|
||||
// NewGroupIndex 创建一个新的组索引实例
|
||||
//
|
||||
// 核心功能:初始化一个空的B+Tree索引,用于管理单个组的记录映射
|
||||
// 时间复杂度:O(1) - 只创建数据结构
|
||||
// 空间复杂度:O(1) - 初始状态下只占用基础结构空间
|
||||
//
|
||||
// 参数说明:
|
||||
// - groupName: 组名,用于标识和调试
|
||||
//
|
||||
// 返回值:
|
||||
// - *TableIndex: 初始化完成的索引实例
|
||||
//
|
||||
// B+Tree配置:
|
||||
// - 度数32:平衡内存使用和查找性能
|
||||
// - 每个节点最多63个键(2*32-1)
|
||||
// - 树高度较低,查找效率高
|
||||
// - 适合数据库索引场景
|
||||
//
|
||||
// 使用示例:
|
||||
//
|
||||
// idx := NewGroupIndex("user_data")
|
||||
// idx.Insert(1001, 5, 10) // 插入记录映射
|
||||
// pageNo, slotNo, found := idx.Get(1001) // 查找记录
|
||||
func NewGroupIndex(groupName string) *TableIndex {
|
||||
return &TableIndex{
|
||||
tree: btree.New(32), // 创建度数为32的B+Tree
|
||||
name: groupName, // 设置索引名称
|
||||
}
|
||||
}
|
||||
|
||||
// Insert 向索引中插入或更新记录映射
|
||||
//
|
||||
// 核心功能:建立记录ID到物理位置的映射关系
|
||||
// 时间复杂度:O(log n) - B+Tree插入操作
|
||||
// 空间复杂度:O(1) - 只添加一个索引条目
|
||||
//
|
||||
// 参数说明:
|
||||
// - id: 记录的唯一标识符
|
||||
// - pageNo: 记录所在的页面编号
|
||||
// - slotNo: 记录在页面内的槽位编号
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 创建IndexEntry结构
|
||||
// 2. 调用B+Tree的ReplaceOrInsert方法
|
||||
// 3. 如果ID已存在,更新位置信息
|
||||
// 4. 如果ID不存在,插入新的映射
|
||||
//
|
||||
// 设计考虑:
|
||||
// - 使用ReplaceOrInsert支持位置更新
|
||||
// - 自动维护B+Tree的平衡性
|
||||
// - 保持索引的有序性
|
||||
// 使用场景:
|
||||
// - 新记录插入时建立映射
|
||||
// - 记录位置变更时更新映射
|
||||
// - 索引重建过程
|
||||
func (idx *TableIndex) Insert(id int64, pageNo, slotNo uint16) {
|
||||
idx.mu.Lock()
|
||||
defer idx.mu.Unlock()
|
||||
|
||||
// 创建索引条目
|
||||
entry := IndexEntry{
|
||||
ID: id, // 记录ID
|
||||
PageNo: pageNo, // 页面编号
|
||||
SlotNo: slotNo, // 槽位编号
|
||||
}
|
||||
|
||||
// 插入到B+Tree
|
||||
// 如果ID已存在,会自动替换旧条目
|
||||
idx.tree.ReplaceOrInsert(entry)
|
||||
}
|
||||
|
||||
// Delete 从索引中删除指定记录的映射
|
||||
//
|
||||
// 核心功能:移除记录ID到物理位置的映射关系
|
||||
// 时间复杂度:O(log n) - B+Tree删除操作
|
||||
// 空间复杂度:O(1) - 释放一个索引条目的空间
|
||||
//
|
||||
// 参数说明:
|
||||
// - id: 要删除的记录ID
|
||||
//
|
||||
// 返回值:
|
||||
// - bool: true表示删除成功,false表示记录不存在
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 创建只包含ID的IndexEntry(用于查找)
|
||||
// 2. 调用B+Tree的Delete方法
|
||||
// 3. 检查删除结果并返回状态
|
||||
//
|
||||
// 设计考虑:
|
||||
// - 只需要ID即可定位要删除的条目
|
||||
// - B+Tree自动维护删除后的平衡性
|
||||
// - 返回删除状态便于错误处理
|
||||
//
|
||||
// 使用场景:
|
||||
// - 记录物理删除时清理映射
|
||||
// - 索引维护和清理
|
||||
// - 数据迁移过程
|
||||
func (idx *TableIndex) Delete(id int64) bool {
|
||||
idx.mu.Lock()
|
||||
defer idx.mu.Unlock()
|
||||
|
||||
// 创建用于查找的索引条目(只需要ID)
|
||||
entry := IndexEntry{ID: id}
|
||||
|
||||
// 从B+Tree中删除条目
|
||||
// Delete返回被删除的项,如果为nil表示不存在
|
||||
return idx.tree.Delete(entry) != nil
|
||||
}
|
||||
|
||||
// Get 根据记录ID查找其物理位置
|
||||
//
|
||||
// 核心功能:通过记录ID快速定位记录的物理存储位置
|
||||
// 时间复杂度:O(log n) - B+Tree查找操作
|
||||
// 空间复杂度:O(1) - 不创建额外数据
|
||||
//
|
||||
// 参数说明:
|
||||
// - id: 要查找的记录ID
|
||||
//
|
||||
// 返回值:
|
||||
// - uint16: 页面编号(如果找到)
|
||||
// - uint16: 槽位编号(如果找到)
|
||||
// - bool: 是否找到记录(true=找到,false=不存在)
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 创建只包含ID的IndexEntry(用于查找)
|
||||
// 2. 调用B+Tree的Get方法查找
|
||||
// 3. 如果找到,提取位置信息并返回
|
||||
// 4. 如果未找到,返回零值和false
|
||||
//
|
||||
// 设计考虑:
|
||||
// - 返回三元组提供完整的查找结果
|
||||
// - 零值返回便于调用者处理未找到的情况
|
||||
// - 类型断言确保数据类型正确性
|
||||
//
|
||||
// 使用场景:
|
||||
// - 记录读取前的位置查找
|
||||
// - 记录更新前的位置确认
|
||||
// - 数据完整性检查
|
||||
//
|
||||
// 性能优势:
|
||||
// - O(log n)查找性能,远优于线性扫描
|
||||
// - 内存索引,无磁盘I/O开销
|
||||
// - B+Tree缓存友好,热点数据访问快
|
||||
func (idx *TableIndex) Get(id int64) (uint16, uint16, bool) {
|
||||
idx.mu.RLock()
|
||||
defer idx.mu.RUnlock()
|
||||
|
||||
// 创建用于查找的索引条目(只需要ID)
|
||||
entry := IndexEntry{ID: id}
|
||||
|
||||
// 在B+Tree中查找对应的条目
|
||||
if item := idx.tree.Get(entry); item != nil {
|
||||
// 找到记录,提取位置信息
|
||||
found := item.(IndexEntry)
|
||||
return found.PageNo, found.SlotNo, true
|
||||
}
|
||||
|
||||
// 未找到记录,返回零值和false
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
// Range 执行范围查询,遍历指定ID范围内的所有记录
|
||||
//
|
||||
// 核心功能:按ID顺序遍历指定范围内的所有索引条目
|
||||
// 时间复杂度:O(log n + k) - n为总条目数,k为结果数量
|
||||
// 空间复杂度:O(1) - 不创建额外的数据结构
|
||||
//
|
||||
// 参数说明:
|
||||
// - startID: 范围查询的起始ID(包含)
|
||||
// - endID: 范围查询的结束ID(包含)
|
||||
// - visitor: 访问者函数,对每个匹配的条目调用
|
||||
// 函数签名:func(id int64, pageNo, slotNo uint16) bool
|
||||
// 返回值:true继续遍历,false停止遍历
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 创建起始和结束的IndexEntry边界
|
||||
// 2. 调用B+Tree的AscendRange方法
|
||||
// 3. 对范围内每个条目调用visitor函数
|
||||
// 4. 根据visitor返回值决定是否继续
|
||||
//
|
||||
// 查询特性:
|
||||
// - 按ID升序遍历结果
|
||||
// - 支持提前终止遍历
|
||||
// - 包含边界值(闭区间查询)
|
||||
// - 利用B+Tree的有序性提供高效遍历
|
||||
//
|
||||
// 使用场景:
|
||||
// - 批量数据处理
|
||||
// - 数据导出和备份
|
||||
// - 范围统计和分析
|
||||
// - 分页查询的底层实现
|
||||
//
|
||||
// 性能优势:
|
||||
// - O(log n)定位起始位置
|
||||
// - 顺序遍历,缓存友好
|
||||
// - 支持早期终止,避免不必要的遍历
|
||||
func (idx *TableIndex) Range(startID, endID int64, visitor func(id int64, pageNo, slotNo uint16) bool) {
|
||||
idx.mu.RLock()
|
||||
defer idx.mu.RUnlock()
|
||||
|
||||
// 创建范围查询的边界条目
|
||||
start := IndexEntry{ID: startID} // 起始边界(包含)
|
||||
|
||||
// 使用 AscendGreaterOrEqual 从起始点开始遍历
|
||||
// 然后在回调中检查结束条件
|
||||
idx.tree.AscendGreaterOrEqual(start, func(item btree.Item) bool {
|
||||
// 提取索引条目信息
|
||||
entry := item.(IndexEntry)
|
||||
|
||||
// 检查是否超出结束边界
|
||||
if entry.ID > endID {
|
||||
return false // 停止遍历
|
||||
}
|
||||
|
||||
// 调用访问者函数,传递记录位置信息
|
||||
// visitor返回false时停止遍历
|
||||
return visitor(entry.ID, entry.PageNo, entry.SlotNo)
|
||||
})
|
||||
}
|
||||
|
||||
// Count 获取索引中的记录总数
|
||||
//
|
||||
// 核心功能:返回当前索引中存储的记录条目数量
|
||||
// 时间复杂度:O(1) - B+Tree维护内部计数器
|
||||
// 空间复杂度:O(1) - 不创建额外数据
|
||||
//
|
||||
// 返回值:
|
||||
// - int: 索引中的记录条目数量(>=0)
|
||||
//
|
||||
// 使用场景:
|
||||
// - 统计分析和报告
|
||||
// - 容量规划和监控
|
||||
// - 性能分析和调优
|
||||
// - 数据完整性检查
|
||||
//
|
||||
// 性能特征:
|
||||
// - 极快的O(1)查询性能
|
||||
// - 不需要遍历索引结构
|
||||
// - 适合频繁的统计查询
|
||||
//
|
||||
// 使用示例:
|
||||
//
|
||||
// count := idx.Count()
|
||||
// fmt.Printf("组 %s 包含 %d 条记录\n", idx.name, count)
|
||||
func (idx *TableIndex) Count() int {
|
||||
idx.mu.RLock()
|
||||
defer idx.mu.RUnlock()
|
||||
|
||||
// 直接返回B+Tree的长度
|
||||
// B+Tree内部维护计数器,提供O(1)性能
|
||||
return idx.tree.Len()
|
||||
}
|
||||
|
||||
// Clear 清空索引中的所有条目
|
||||
//
|
||||
// 核心功能:移除索引中的所有记录映射,重置为空状态
|
||||
// 时间复杂度:O(n) - 需要释放所有节点
|
||||
// 空间复杂度:O(1) - 释放所有索引占用的空间
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 调用B+Tree的Clear方法
|
||||
// 2. 释放所有索引节点的内存
|
||||
// 3. 重置索引为空状态
|
||||
//
|
||||
// 参数说明:
|
||||
// - false参数:表示不添加到空闲列表(B+Tree内部参数)
|
||||
//
|
||||
// 使用场景:
|
||||
// - 组数据完全清理时
|
||||
// - 索引重建前的清空操作
|
||||
// - 内存回收和资源清理
|
||||
// - 测试环境的数据重置
|
||||
//
|
||||
// 注意事项:
|
||||
// - 操作不可逆,清空后无法恢复
|
||||
// - 不影响磁盘上的实际数据
|
||||
// - 只清理内存中的索引映射
|
||||
// - 清空后Count()返回0
|
||||
//
|
||||
// 使用示例:
|
||||
//
|
||||
// idx.Clear() // 清空索引
|
||||
// fmt.Printf("索引已清空,当前记录数:%d\n", idx.Count())
|
||||
func (idx *TableIndex) Clear() {
|
||||
idx.mu.Lock()
|
||||
defer idx.mu.Unlock()
|
||||
|
||||
// 清空B+Tree中的所有条目
|
||||
// false参数表示不将节点添加到空闲列表
|
||||
idx.tree.Clear(false)
|
||||
}
|
||||
|
||||
// RebuildIndex 从页面链重建组索引
|
||||
//
|
||||
// 核心功能:扫描页面链中的所有记录,重新构建内存索引
|
||||
// 时间复杂度:O(n log n) - n为记录数量,每次插入O(log n)
|
||||
// 空间复杂度:O(n) - 需要存储所有记录的索引条目
|
||||
//
|
||||
// 参数说明:
|
||||
// - groupName: 组名,用于创建新索引
|
||||
// - rootPage: 页面链的根页面编号
|
||||
//
|
||||
// 返回值:
|
||||
// - *TableIndex: 重建完成的索引实例
|
||||
// - error: 重建过程中的错误(nil表示成功)
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 创建新的空索引
|
||||
// 2. 从根页面开始遍历页面链
|
||||
// 3. 扫描每个页面的所有槽位
|
||||
// 4. 提取记录ID并建立索引映射
|
||||
// 5. 跳过已删除和损坏的记录
|
||||
// 6. 返回完整的索引
|
||||
//
|
||||
// 错误处理:
|
||||
// - 页面读取失败时返回错误
|
||||
// - 跳过损坏的记录,继续处理
|
||||
// - 确保索引的完整性和一致性
|
||||
//
|
||||
// 使用场景:
|
||||
// - 数据库启动时的索引恢复
|
||||
// - 索引损坏后的重建操作
|
||||
// - 数据迁移和修复过程
|
||||
//
|
||||
// 性能考虑:
|
||||
// - 需要读取所有数据页面
|
||||
// - 内存使用量与记录数成正比
|
||||
// - 适合在维护窗口期执行
|
||||
func (pdb *PipelineDB) RebuildIndex(groupName string, rootPage uint16) (*TableIndex, error) {
|
||||
// 创建新的空索引
|
||||
idx := NewGroupIndex(groupName)
|
||||
|
||||
// 从根页面开始遍历页面链
|
||||
pageNo := rootPage
|
||||
for pageNo != 0 {
|
||||
// 读取当前页面
|
||||
p, err := pdb.readPage(pageNo)
|
||||
if err != nil {
|
||||
return nil, err // 页面读取失败,返回错误
|
||||
}
|
||||
|
||||
// 获取页面的槽位数组
|
||||
slots := p.slotArray()
|
||||
for slotNo, off := range slots {
|
||||
// 跳过已删除的槽位(偏移为0表示已删除)
|
||||
if off == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查记录是否损坏(偏移超出页面范围)
|
||||
if int(off)+8 > PageSize {
|
||||
continue // 跳过损坏的记录
|
||||
}
|
||||
|
||||
// 从记录中提取ID(记录的前8字节)
|
||||
id := int64(binary.LittleEndian.Uint64(p[off:]))
|
||||
|
||||
// 将记录ID和位置信息添加到索引中
|
||||
idx.Insert(id, pageNo, uint16(slotNo))
|
||||
}
|
||||
|
||||
// 移动到链表中的下一个页面
|
||||
pageNo = p.nextPage()
|
||||
}
|
||||
|
||||
return idx, nil // 返回重建完成的索引
|
||||
}
|
||||
|
||||
// IndexManager 全局索引管理器
|
||||
//
|
||||
// 核心职责:
|
||||
// 1. 管理所有组的索引实例
|
||||
// 2. 提供索引的创建、获取、删除功能
|
||||
// 3. 维护索引的生命周期
|
||||
// 4. 提供索引统计信息
|
||||
//
|
||||
// 设计思想:
|
||||
// - 使用HashMap管理多个组索引
|
||||
// - 延迟创建索引,按需分配资源
|
||||
// - 提供统一的索引管理接口
|
||||
// - 支持索引的动态添加和删除
|
||||
//
|
||||
// 性能特征:
|
||||
// - 索引查找:O(1) - HashMap查找
|
||||
// - 内存使用:O(k) - k为组的数量
|
||||
// - 支持大量组的高效管理
|
||||
//
|
||||
// 适用场景:
|
||||
// - 多租户数据库系统
|
||||
// - 分组数据管理
|
||||
// - 索引集中管理
|
||||
type IndexManager struct {
|
||||
mu sync.RWMutex // 保护并发访问的读写锁
|
||||
indexes map[string]*TableIndex // 组名到索引实例的映射
|
||||
}
|
||||
|
||||
// NewIndexManager 创建一个新的索引管理器实例
|
||||
//
|
||||
// 核心功能:初始化空的索引管理器
|
||||
// 时间复杂度:O(1) - 只创建空的HashMap
|
||||
// 空间复杂度:O(1) - 初始状态不占用额外空间
|
||||
//
|
||||
// 返回值:
|
||||
// - *IndexManager: 初始化完成的索引管理器
|
||||
//
|
||||
// 使用示例:
|
||||
//
|
||||
// im := NewIndexManager()
|
||||
// idx := im.GetOrCreateIndex("user_data")
|
||||
func NewIndexManager() *IndexManager {
|
||||
return &IndexManager{
|
||||
indexes: make(map[string]*TableIndex), // 初始化空的索引映射
|
||||
}
|
||||
}
|
||||
|
||||
// GetOrCreateIndex 获取或创建指定组的索引
|
||||
//
|
||||
// 核心功能:延迟创建索引,按需分配资源
|
||||
// 时间复杂度:O(1) - HashMap查找和插入
|
||||
// 空间复杂度:O(1) - 可能创建一个新索引
|
||||
//
|
||||
// 参数说明:
|
||||
// - groupName: 组名
|
||||
//
|
||||
// 返回值:
|
||||
// - *TableIndex: 组对应的索引实例
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 检查索引是否已存在
|
||||
// 2. 如果存在,直接返回
|
||||
// 3. 如果不存在,创建新索引并缓存
|
||||
// 4. 返回索引实例
|
||||
//
|
||||
// 设计优势:
|
||||
// - 延迟创建,节省内存
|
||||
// - 自动管理索引生命周期
|
||||
// - 线程安全的索引获取
|
||||
func (im *IndexManager) GetOrCreateIndex(groupName string) *TableIndex {
|
||||
// 先尝试读锁快速检查
|
||||
im.mu.RLock()
|
||||
if idx, exists := im.indexes[groupName]; exists {
|
||||
im.mu.RUnlock()
|
||||
return idx // 返回已存在的索引
|
||||
}
|
||||
im.mu.RUnlock()
|
||||
|
||||
// 需要创建新索引,获取写锁
|
||||
im.mu.Lock()
|
||||
defer im.mu.Unlock()
|
||||
|
||||
// 双重检查,防止在获取写锁期间其他goroutine已创建
|
||||
if idx, exists := im.indexes[groupName]; exists {
|
||||
return idx // 返回已存在的索引
|
||||
}
|
||||
|
||||
// 创建新索引并缓存
|
||||
idx := NewGroupIndex(groupName)
|
||||
im.indexes[groupName] = idx
|
||||
return idx
|
||||
}
|
||||
|
||||
// GetIndex 获取指定组的索引(如果存在)
|
||||
//
|
||||
// 核心功能:查找已存在的索引,不创建新索引
|
||||
// 时间复杂度:O(1) - HashMap查找
|
||||
// 空间复杂度:O(1) - 不创建额外数据
|
||||
//
|
||||
// 参数说明:
|
||||
// - groupName: 组名
|
||||
//
|
||||
// 返回值:
|
||||
// - *TableIndex: 索引实例(如果存在)
|
||||
// - bool: 是否找到索引
|
||||
//
|
||||
// 使用场景:
|
||||
// - 检查索引是否已创建
|
||||
// - 只读访问现有索引
|
||||
// - 避免意外创建索引
|
||||
func (im *IndexManager) GetIndex(groupName string) (*TableIndex, bool) {
|
||||
im.mu.RLock()
|
||||
defer im.mu.RUnlock()
|
||||
idx, exists := im.indexes[groupName]
|
||||
return idx, exists
|
||||
}
|
||||
|
||||
// DropIndex 删除指定组的索引
|
||||
//
|
||||
// 核心功能:清理并删除组索引,释放相关资源
|
||||
// 时间复杂度:O(n) - 需要清空索引中的所有条目
|
||||
// 空间复杂度:O(1) - 释放索引占用的空间
|
||||
//
|
||||
// 参数说明:
|
||||
// - groupName: 要删除的组名
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 检查索引是否存在
|
||||
// 2. 如果存在,先清空索引内容
|
||||
// 3. 从管理器中删除索引引用
|
||||
// 4. 释放相关资源
|
||||
//
|
||||
// 使用场景:
|
||||
// - 组数据完全删除时
|
||||
// - 内存回收和资源清理
|
||||
// - 系统维护和重组
|
||||
func (im *IndexManager) DropIndex(groupName string) {
|
||||
im.mu.Lock()
|
||||
defer im.mu.Unlock()
|
||||
|
||||
// 检查索引是否存在
|
||||
if idx, exists := im.indexes[groupName]; exists {
|
||||
// 清空索引内容,释放内存
|
||||
idx.Clear()
|
||||
|
||||
// 从管理器中删除索引引用
|
||||
delete(im.indexes, groupName)
|
||||
}
|
||||
// 如果索引不存在,操作是幂等的(无副作用)
|
||||
}
|
||||
|
||||
// GetStats 获取所有组的索引统计信息
|
||||
//
|
||||
// 核心功能:收集所有组索引的记录数量统计
|
||||
// 时间复杂度:O(k) - k为组的数量
|
||||
// 空间复杂度:O(k) - 创建统计信息映射
|
||||
//
|
||||
// 返回值:
|
||||
// - map[string]int: 组名到记录数量的映射
|
||||
//
|
||||
// 使用场景:
|
||||
// - 系统监控和报告
|
||||
// - 容量规划和分析
|
||||
// - 性能调优和诊断
|
||||
//
|
||||
// 使用示例:
|
||||
//
|
||||
// stats := im.GetStats()
|
||||
// for group, count := range stats {
|
||||
// fmt.Printf("组 %s: %d 条记录\n", group, count)
|
||||
// }
|
||||
func (im *IndexManager) GetStats() map[string]int {
|
||||
// 创建统计信息映射
|
||||
stats := make(map[string]int)
|
||||
|
||||
// 遍历所有索引,收集记录数量
|
||||
for name, idx := range im.indexes {
|
||||
stats[name] = idx.Count()
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
711
index_test.go
Normal file
711
index_test.go
Normal file
@@ -0,0 +1,711 @@
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"os"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestIndexEntry 测试索引条目的基本功能
|
||||
func TestIndexEntry(t *testing.T) {
|
||||
entry1 := IndexEntry{ID: 100, PageNo: 5, SlotNo: 10}
|
||||
entry2 := IndexEntry{ID: 200, PageNo: 8, SlotNo: 15}
|
||||
entry3 := IndexEntry{ID: 100, PageNo: 6, SlotNo: 12} // 相同ID,不同位置
|
||||
|
||||
// 测试Less方法
|
||||
if !entry1.Less(entry2) {
|
||||
t.Error("entry1 should be less than entry2")
|
||||
}
|
||||
|
||||
if entry2.Less(entry1) {
|
||||
t.Error("entry2 should not be less than entry1")
|
||||
}
|
||||
|
||||
if entry1.Less(entry3) || entry3.Less(entry1) {
|
||||
t.Error("entries with same ID should be equal")
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewGroupIndex 测试组索引的创建
|
||||
func TestNewGroupIndex(t *testing.T) {
|
||||
groupName := "test_group"
|
||||
idx := NewGroupIndex(groupName)
|
||||
|
||||
if idx == nil {
|
||||
t.Fatal("NewGroupIndex returned nil")
|
||||
}
|
||||
|
||||
if idx.name != groupName {
|
||||
t.Errorf("index name = %s, want %s", idx.name, groupName)
|
||||
}
|
||||
|
||||
if idx.Count() != 0 {
|
||||
t.Errorf("initial count = %d, want 0", idx.Count())
|
||||
}
|
||||
|
||||
if idx.tree == nil {
|
||||
t.Error("B+Tree not initialized")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTableIndexInsert 测试索引插入操作
|
||||
func TestTableIndexInsert(t *testing.T) {
|
||||
idx := NewGroupIndex("test")
|
||||
|
||||
// 插入一些条目
|
||||
testData := []struct {
|
||||
id int64
|
||||
pageNo uint16
|
||||
slotNo uint16
|
||||
}{
|
||||
{100, 1, 5},
|
||||
{200, 2, 10},
|
||||
{150, 1, 8},
|
||||
{300, 3, 2},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
idx.Insert(data.id, data.pageNo, data.slotNo)
|
||||
}
|
||||
|
||||
// 验证索引大小
|
||||
if idx.Count() != len(testData) {
|
||||
t.Errorf("count after inserts = %d, want %d", idx.Count(), len(testData))
|
||||
}
|
||||
|
||||
// 验证每个条目都能找到
|
||||
for _, data := range testData {
|
||||
pageNo, slotNo, found := idx.Get(data.id)
|
||||
if !found {
|
||||
t.Errorf("entry with ID %d not found", data.id)
|
||||
continue
|
||||
}
|
||||
if pageNo != data.pageNo || slotNo != data.slotNo {
|
||||
t.Errorf("entry %d: got (%d, %d), want (%d, %d)",
|
||||
data.id, pageNo, slotNo, data.pageNo, data.slotNo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestTableIndexInsertUpdate 测试插入时的更新操作
|
||||
func TestTableIndexInsertUpdate(t *testing.T) {
|
||||
idx := NewGroupIndex("test")
|
||||
|
||||
id := int64(100)
|
||||
|
||||
// 首次插入
|
||||
idx.Insert(id, 1, 5)
|
||||
|
||||
pageNo, slotNo, found := idx.Get(id)
|
||||
if !found || pageNo != 1 || slotNo != 5 {
|
||||
t.Errorf("first insert: got (%d, %d, %t), want (1, 5, true)", pageNo, slotNo, found)
|
||||
}
|
||||
|
||||
// 更新同一ID的位置
|
||||
idx.Insert(id, 2, 10)
|
||||
|
||||
pageNo, slotNo, found = idx.Get(id)
|
||||
if !found || pageNo != 2 || slotNo != 10 {
|
||||
t.Errorf("after update: got (%d, %d, %t), want (2, 10, true)", pageNo, slotNo, found)
|
||||
}
|
||||
|
||||
// 验证索引大小没有增加
|
||||
if idx.Count() != 1 {
|
||||
t.Errorf("count after update = %d, want 1", idx.Count())
|
||||
}
|
||||
}
|
||||
|
||||
// TestTableIndexGet 测试索引查找操作
|
||||
func TestTableIndexGet(t *testing.T) {
|
||||
idx := NewGroupIndex("test")
|
||||
|
||||
// 插入测试数据
|
||||
idx.Insert(100, 1, 5)
|
||||
idx.Insert(200, 2, 10)
|
||||
idx.Insert(300, 3, 15)
|
||||
|
||||
// 测试存在的条目
|
||||
pageNo, slotNo, found := idx.Get(200)
|
||||
if !found || pageNo != 2 || slotNo != 10 {
|
||||
t.Errorf("Get(200) = (%d, %d, %t), want (2, 10, true)", pageNo, slotNo, found)
|
||||
}
|
||||
|
||||
// 测试不存在的条目
|
||||
pageNo, slotNo, found = idx.Get(999)
|
||||
if found || pageNo != 0 || slotNo != 0 {
|
||||
t.Errorf("Get(999) = (%d, %d, %t), want (0, 0, false)", pageNo, slotNo, found)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTableIndexDelete 测试索引删除操作
|
||||
func TestTableIndexDelete(t *testing.T) {
|
||||
idx := NewGroupIndex("test")
|
||||
|
||||
// 插入测试数据
|
||||
testIDs := []int64{100, 200, 300, 400}
|
||||
for i, id := range testIDs {
|
||||
idx.Insert(id, uint16(i+1), uint16(i*5))
|
||||
}
|
||||
|
||||
initialCount := idx.Count()
|
||||
|
||||
// 删除存在的条目
|
||||
deleted := idx.Delete(200)
|
||||
if !deleted {
|
||||
t.Error("Delete(200) should return true")
|
||||
}
|
||||
|
||||
// 验证条目被删除
|
||||
_, _, found := idx.Get(200)
|
||||
if found {
|
||||
t.Error("deleted entry should not be found")
|
||||
}
|
||||
|
||||
// 验证索引大小减少
|
||||
if idx.Count() != initialCount-1 {
|
||||
t.Errorf("count after delete = %d, want %d", idx.Count(), initialCount-1)
|
||||
}
|
||||
|
||||
// 删除不存在的条目
|
||||
deleted = idx.Delete(999)
|
||||
if deleted {
|
||||
t.Error("Delete(999) should return false")
|
||||
}
|
||||
|
||||
// 验证索引大小没有变化
|
||||
if idx.Count() != initialCount-1 {
|
||||
t.Errorf("count after deleting non-existent = %d, want %d", idx.Count(), initialCount-1)
|
||||
}
|
||||
|
||||
// 验证其他条目仍然存在
|
||||
for _, id := range []int64{100, 300, 400} {
|
||||
_, _, found := idx.Get(id)
|
||||
if !found {
|
||||
t.Errorf("entry %d should still exist", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestTableIndexRange 测试范围查询
|
||||
func TestTableIndexRange(t *testing.T) {
|
||||
idx := NewGroupIndex("test")
|
||||
|
||||
// 插入测试数据(乱序插入)
|
||||
testData := []struct {
|
||||
id int64
|
||||
pageNo uint16
|
||||
slotNo uint16
|
||||
}{
|
||||
{300, 3, 15},
|
||||
{100, 1, 5},
|
||||
{500, 5, 25},
|
||||
{200, 2, 10},
|
||||
{400, 4, 20},
|
||||
}
|
||||
|
||||
for _, data := range testData {
|
||||
idx.Insert(data.id, data.pageNo, data.slotNo)
|
||||
}
|
||||
|
||||
// 测试范围查询 [200, 400]
|
||||
var results []int64
|
||||
idx.Range(200, 400, func(id int64, pageNo, slotNo uint16) bool {
|
||||
results = append(results, id)
|
||||
return true // 继续遍历
|
||||
})
|
||||
|
||||
// 验证结果按ID升序排列
|
||||
expectedIDs := []int64{200, 300, 400}
|
||||
if len(results) != len(expectedIDs) {
|
||||
t.Errorf("range query returned %d results, want %d", len(results), len(expectedIDs))
|
||||
t.Errorf("actual results: %v", results)
|
||||
}
|
||||
|
||||
for i, expected := range expectedIDs {
|
||||
if i >= len(results) || results[i] != expected {
|
||||
t.Errorf("result[%d] = %d, want %d", i, results[i], expected)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试提前终止的范围查询
|
||||
results = nil
|
||||
count := 0
|
||||
idx.Range(100, 500, func(id int64, pageNo, slotNo uint16) bool {
|
||||
results = append(results, id)
|
||||
count++
|
||||
return count < 2 // 只处理前2个
|
||||
})
|
||||
|
||||
if len(results) != 2 {
|
||||
t.Errorf("early termination returned %d results, want 2", len(results))
|
||||
}
|
||||
|
||||
if results[0] != 100 || results[1] != 200 {
|
||||
t.Errorf("early termination results = %v, want [100, 200]", results)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTableIndexRangeEmpty 测试空范围查询
|
||||
func TestTableIndexRangeEmpty(t *testing.T) {
|
||||
idx := NewGroupIndex("test")
|
||||
|
||||
// 插入一些数据
|
||||
idx.Insert(100, 1, 5)
|
||||
idx.Insert(300, 3, 15)
|
||||
|
||||
// 查询不存在的范围
|
||||
var results []int64
|
||||
idx.Range(150, 250, func(id int64, pageNo, slotNo uint16) bool {
|
||||
results = append(results, id)
|
||||
return true
|
||||
})
|
||||
|
||||
if len(results) != 0 {
|
||||
t.Errorf("empty range query returned %d results, want 0", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
// TestTableIndexClear 测试清空索引
|
||||
func TestTableIndexClear(t *testing.T) {
|
||||
idx := NewGroupIndex("test")
|
||||
|
||||
// 插入一些数据
|
||||
for i := int64(1); i <= 10; i++ {
|
||||
idx.Insert(i, uint16(i), uint16(i*2))
|
||||
}
|
||||
|
||||
// 验证数据存在
|
||||
if idx.Count() != 10 {
|
||||
t.Errorf("count before clear = %d, want 10", idx.Count())
|
||||
}
|
||||
|
||||
// 清空索引
|
||||
idx.Clear()
|
||||
|
||||
// 验证索引被清空
|
||||
if idx.Count() != 0 {
|
||||
t.Errorf("count after clear = %d, want 0", idx.Count())
|
||||
}
|
||||
|
||||
// 验证数据不再存在
|
||||
_, _, found := idx.Get(5)
|
||||
if found {
|
||||
t.Error("data should not exist after clear")
|
||||
}
|
||||
|
||||
// 验证可以重新插入数据
|
||||
idx.Insert(100, 1, 5)
|
||||
if idx.Count() != 1 {
|
||||
t.Errorf("count after re-insert = %d, want 1", idx.Count())
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewIndexManager 测试索引管理器的创建
|
||||
func TestNewIndexManager(t *testing.T) {
|
||||
im := NewIndexManager()
|
||||
|
||||
if im == nil {
|
||||
t.Fatal("NewIndexManager returned nil")
|
||||
}
|
||||
|
||||
if im.indexes == nil {
|
||||
t.Error("indexes map not initialized")
|
||||
}
|
||||
|
||||
// 验证初始状态
|
||||
stats := im.GetStats()
|
||||
if len(stats) != 0 {
|
||||
t.Errorf("initial stats length = %d, want 0", len(stats))
|
||||
}
|
||||
}
|
||||
|
||||
// TestIndexManagerGetOrCreateIndex 测试获取或创建索引
|
||||
func TestIndexManagerGetOrCreateIndex(t *testing.T) {
|
||||
im := NewIndexManager()
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 第一次调用应该创建新索引
|
||||
idx1 := im.GetOrCreateIndex(groupName)
|
||||
if idx1 == nil {
|
||||
t.Fatal("GetOrCreateIndex returned nil")
|
||||
}
|
||||
|
||||
if idx1.name != groupName {
|
||||
t.Errorf("index name = %s, want %s", idx1.name, groupName)
|
||||
}
|
||||
|
||||
// 第二次调用应该返回相同的索引
|
||||
idx2 := im.GetOrCreateIndex(groupName)
|
||||
if idx2 != idx1 {
|
||||
t.Error("GetOrCreateIndex should return the same index instance")
|
||||
}
|
||||
|
||||
// 验证统计信息
|
||||
stats := im.GetStats()
|
||||
if len(stats) != 1 {
|
||||
t.Errorf("stats length = %d, want 1", len(stats))
|
||||
}
|
||||
|
||||
if stats[groupName] != 0 {
|
||||
t.Errorf("stats[%s] = %d, want 0", groupName, stats[groupName])
|
||||
}
|
||||
}
|
||||
|
||||
// TestIndexManagerGetIndex 测试获取索引
|
||||
func TestIndexManagerGetIndex(t *testing.T) {
|
||||
im := NewIndexManager()
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 获取不存在的索引
|
||||
idx, exists := im.GetIndex(groupName)
|
||||
if exists || idx != nil {
|
||||
t.Error("GetIndex should return (nil, false) for non-existent index")
|
||||
}
|
||||
|
||||
// 创建索引
|
||||
createdIdx := im.GetOrCreateIndex(groupName)
|
||||
|
||||
// 获取存在的索引
|
||||
idx, exists = im.GetIndex(groupName)
|
||||
if !exists || idx != createdIdx {
|
||||
t.Error("GetIndex should return the created index")
|
||||
}
|
||||
}
|
||||
|
||||
// TestIndexManagerDropIndex 测试删除索引
|
||||
func TestIndexManagerDropIndex(t *testing.T) {
|
||||
im := NewIndexManager()
|
||||
|
||||
groupName := "test_group"
|
||||
|
||||
// 创建索引并添加数据
|
||||
idx := im.GetOrCreateIndex(groupName)
|
||||
idx.Insert(100, 1, 5)
|
||||
idx.Insert(200, 2, 10)
|
||||
|
||||
// 验证索引存在且有数据
|
||||
if idx.Count() != 2 {
|
||||
t.Errorf("index count = %d, want 2", idx.Count())
|
||||
}
|
||||
|
||||
stats := im.GetStats()
|
||||
if stats[groupName] != 2 {
|
||||
t.Errorf("stats[%s] = %d, want 2", groupName, stats[groupName])
|
||||
}
|
||||
|
||||
// 删除索引
|
||||
im.DropIndex(groupName)
|
||||
|
||||
// 验证索引被删除
|
||||
_, exists := im.GetIndex(groupName)
|
||||
if exists {
|
||||
t.Error("index should not exist after drop")
|
||||
}
|
||||
|
||||
// 验证统计信息更新
|
||||
stats = im.GetStats()
|
||||
if len(stats) != 0 {
|
||||
t.Errorf("stats length after drop = %d, want 0", len(stats))
|
||||
}
|
||||
|
||||
// 删除不存在的索引应该是安全的
|
||||
im.DropIndex("non_existent")
|
||||
}
|
||||
|
||||
// TestIndexManagerGetStats 测试获取统计信息
|
||||
func TestIndexManagerGetStats(t *testing.T) {
|
||||
im := NewIndexManager()
|
||||
|
||||
// 创建多个索引并添加不同数量的数据
|
||||
groups := map[string]int{
|
||||
"group1": 5,
|
||||
"group2": 10,
|
||||
"group3": 3,
|
||||
}
|
||||
|
||||
for groupName, count := range groups {
|
||||
idx := im.GetOrCreateIndex(groupName)
|
||||
for i := 0; i < count; i++ {
|
||||
idx.Insert(int64(i), uint16(i), uint16(i*2))
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计信息
|
||||
stats := im.GetStats()
|
||||
|
||||
// 验证统计信息
|
||||
if len(stats) != len(groups) {
|
||||
t.Errorf("stats length = %d, want %d", len(stats), len(groups))
|
||||
}
|
||||
|
||||
for groupName, expectedCount := range groups {
|
||||
if stats[groupName] != expectedCount {
|
||||
t.Errorf("stats[%s] = %d, want %d", groupName, stats[groupName], expectedCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MockPipelineDB 用于测试RebuildIndex的模拟数据库
|
||||
type MockPipelineDB struct {
|
||||
pages map[uint16]*MockPage
|
||||
}
|
||||
|
||||
type MockPage struct {
|
||||
data []byte
|
||||
nextPageNo uint16
|
||||
slots []uint16
|
||||
}
|
||||
|
||||
func (p *MockPage) slotArray() []uint16 {
|
||||
return p.slots
|
||||
}
|
||||
|
||||
func (p *MockPage) nextPage() uint16 {
|
||||
return p.nextPageNo
|
||||
}
|
||||
|
||||
func NewMockPipelineDB() *MockPipelineDB {
|
||||
return &MockPipelineDB{
|
||||
pages: make(map[uint16]*MockPage),
|
||||
}
|
||||
}
|
||||
|
||||
func (db *MockPipelineDB) readPage(pageNo uint16) (*MockPage, error) {
|
||||
if page, exists := db.pages[pageNo]; exists {
|
||||
return page, nil
|
||||
}
|
||||
return nil, errors.New("page not found")
|
||||
}
|
||||
|
||||
func (db *MockPipelineDB) addPage(pageNo uint16, nextPage uint16, records []struct {
|
||||
id int64
|
||||
offset uint16
|
||||
}) {
|
||||
page := &MockPage{
|
||||
data: make([]byte, PageSize),
|
||||
nextPageNo: nextPage,
|
||||
slots: make([]uint16, len(records)),
|
||||
}
|
||||
|
||||
for i, record := range records {
|
||||
// 将ID写入指定偏移位置
|
||||
binary.LittleEndian.PutUint64(page.data[record.offset:], uint64(record.id))
|
||||
page.slots[i] = record.offset
|
||||
}
|
||||
|
||||
db.pages[pageNo] = page
|
||||
}
|
||||
|
||||
// TestRebuildIndex 测试索引重建
|
||||
func TestRebuildIndex(t *testing.T) {
|
||||
// 创建一个临时的PipelineDB实例用于测试
|
||||
tmpFile, err := os.CreateTemp("", "rebuild_test_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 10}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 先添加一些测试数据
|
||||
testData := [][]byte{
|
||||
[]byte("test data 1"),
|
||||
[]byte("test data 2"),
|
||||
[]byte("test data 3"),
|
||||
}
|
||||
|
||||
var recordIDs []int64
|
||||
for _, data := range testData {
|
||||
recordID, err := pdb.AcceptData("test_group", data, `{"test": true}`)
|
||||
if err != nil {
|
||||
t.Fatalf("AcceptData failed: %v", err)
|
||||
}
|
||||
recordIDs = append(recordIDs, recordID)
|
||||
}
|
||||
|
||||
// 重建索引
|
||||
idx, err := pdb.RebuildIndex("test_group", 1)
|
||||
if err != nil {
|
||||
t.Errorf("RebuildIndex returned error: %v", err)
|
||||
}
|
||||
|
||||
if idx == nil {
|
||||
t.Fatal("RebuildIndex returned nil index")
|
||||
}
|
||||
|
||||
// 验证索引包含所有记录
|
||||
if idx.Count() != len(recordIDs) {
|
||||
t.Errorf("rebuilt index count = %d, want %d", idx.Count(), len(recordIDs))
|
||||
}
|
||||
|
||||
// 验证每个记录都能在索引中找到
|
||||
for _, recordID := range recordIDs {
|
||||
_, _, found := idx.Get(recordID)
|
||||
if !found {
|
||||
t.Errorf("record %d not found in rebuilt index", recordID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestIndexLargeDataset 测试大数据集的索引操作
|
||||
func TestIndexLargeDataset(t *testing.T) {
|
||||
idx := NewGroupIndex("large_test")
|
||||
|
||||
const numRecords = 10000
|
||||
|
||||
// 插入大量记录
|
||||
for i := int64(1); i <= numRecords; i++ {
|
||||
idx.Insert(i, uint16(i%1000), uint16(i%100))
|
||||
}
|
||||
|
||||
// 验证记录数量
|
||||
if idx.Count() != numRecords {
|
||||
t.Errorf("count = %d, want %d", idx.Count(), numRecords)
|
||||
}
|
||||
|
||||
// 随机验证一些记录
|
||||
testIDs := []int64{1, 100, 1000, 5000, 9999, 10000}
|
||||
for _, id := range testIDs {
|
||||
_, _, found := idx.Get(id)
|
||||
if !found {
|
||||
t.Errorf("record %d not found", id)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试范围查询
|
||||
var rangeResults []int64
|
||||
idx.Range(5000, 5010, func(id int64, pageNo, slotNo uint16) bool {
|
||||
rangeResults = append(rangeResults, id)
|
||||
return true
|
||||
})
|
||||
|
||||
expectedRange := []int64{5000, 5001, 5002, 5003, 5004, 5005, 5006, 5007, 5008, 5009, 5010}
|
||||
if len(rangeResults) != len(expectedRange) {
|
||||
t.Errorf("range query returned %d results, want %d", len(rangeResults), len(expectedRange))
|
||||
}
|
||||
|
||||
// 删除一些记录
|
||||
for i := int64(1000); i <= 2000; i++ {
|
||||
idx.Delete(i)
|
||||
}
|
||||
|
||||
// 验证删除后的数量
|
||||
expectedCount := numRecords - 1001 // 删除了1001个记录(1000到2000,包含两端)
|
||||
if idx.Count() != expectedCount {
|
||||
t.Errorf("count after deletion = %d, want %d", idx.Count(), expectedCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIndexOrdering 测试索引的有序性
|
||||
func TestIndexOrdering(t *testing.T) {
|
||||
idx := NewGroupIndex("order_test")
|
||||
|
||||
// 乱序插入数据
|
||||
ids := []int64{500, 100, 300, 200, 400, 600, 150, 350}
|
||||
for _, id := range ids {
|
||||
idx.Insert(id, uint16(id/100), uint16(id%100))
|
||||
}
|
||||
|
||||
// 范围查询应该返回有序结果
|
||||
var results []int64
|
||||
idx.Range(0, 1000, func(id int64, pageNo, slotNo uint16) bool {
|
||||
results = append(results, id)
|
||||
return true
|
||||
})
|
||||
|
||||
// 验证结果是有序的
|
||||
if !sort.SliceIsSorted(results, func(i, j int) bool {
|
||||
return results[i] < results[j]
|
||||
}) {
|
||||
t.Errorf("range query results are not sorted: %v", results)
|
||||
}
|
||||
|
||||
// 验证所有ID都存在
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
if len(results) != len(ids) {
|
||||
t.Errorf("result count = %d, want %d", len(results), len(ids))
|
||||
}
|
||||
|
||||
for i, expected := range ids {
|
||||
if i >= len(results) || results[i] != expected {
|
||||
t.Errorf("result[%d] = %d, want %d", i, results[i], expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkTableIndexInsert 性能测试:索引插入
|
||||
func BenchmarkTableIndexInsert(b *testing.B) {
|
||||
idx := NewGroupIndex("bench_test")
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
idx.Insert(int64(i), uint16(i%1000), uint16(i%100))
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkTableIndexGet 性能测试:索引查找
|
||||
func BenchmarkTableIndexGet(b *testing.B) {
|
||||
idx := NewGroupIndex("bench_test")
|
||||
|
||||
// 预填充数据
|
||||
for i := 0; i < 100000; i++ {
|
||||
idx.Insert(int64(i), uint16(i%1000), uint16(i%100))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
idx.Get(int64(i % 100000))
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkTableIndexRange 性能测试:范围查询
|
||||
func BenchmarkTableIndexRange(b *testing.B) {
|
||||
idx := NewGroupIndex("bench_test")
|
||||
|
||||
// 预填充数据
|
||||
for i := 0; i < 100000; i++ {
|
||||
idx.Insert(int64(i), uint16(i%1000), uint16(i%100))
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
start := int64(i % 90000)
|
||||
end := start + 100
|
||||
idx.Range(start, end, func(id int64, pageNo, slotNo uint16) bool {
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkIndexManagerOperations 性能测试:索引管理器操作
|
||||
func BenchmarkIndexManagerOperations(b *testing.B) {
|
||||
im := NewIndexManager()
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
groupName := "group_" + string(rune('A'+i%26))
|
||||
idx := im.GetOrCreateIndex(groupName)
|
||||
idx.Insert(int64(i), uint16(i%1000), uint16(i%100))
|
||||
|
||||
if i%1000 == 0 {
|
||||
im.GetStats()
|
||||
}
|
||||
}
|
||||
}
|
||||
1478
pipeline_db.go
Normal file
1478
pipeline_db.go
Normal file
File diff suppressed because it is too large
Load Diff
1451
pipeline_db_test.go
Normal file
1451
pipeline_db_test.go
Normal file
File diff suppressed because it is too large
Load Diff
600
storage.go
Normal file
600
storage.go
Normal file
@@ -0,0 +1,600 @@
|
||||
// Package pipelinedb - storage.go
|
||||
//
|
||||
// 存储引擎模块:实现了基于页面的存储系统
|
||||
//
|
||||
// 核心功能:
|
||||
// - 页面管理:分配、释放、读写页面
|
||||
// - 记录存储:插入、读取、更新、删除记录
|
||||
// - 索引支持:与索引系统集成,提供快速数据访问
|
||||
// - 缓存机制:页面缓存提高访问性能
|
||||
// - 链式存储:支持页面链接,处理大量数据
|
||||
//
|
||||
// 页面布局:
|
||||
// [0-2] numSlots - 槽位数量
|
||||
// [2-4] freeOff - 空闲空间偏移
|
||||
// [4-6] nextPage - 下一页编号
|
||||
// [6...] slotArray - 槽位数组,每个槽位2字节
|
||||
// [...] records - 记录数据区域(从页面末尾向前增长)
|
||||
//
|
||||
// 记录格式:
|
||||
// [0-8] recordID - 记录ID(8字节)
|
||||
// [8-9+] dataLength - 数据长度(变长编码)
|
||||
// [9+...] data - 实际数据内容
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// Page 页面类型
|
||||
//
|
||||
// 页面是存储系统的基本单位,大小固定为4096字节
|
||||
// 每个页面包含页面头、槽位数组和记录数据区域
|
||||
// 页面采用槽位目录结构,支持变长记录存储
|
||||
type Page []byte
|
||||
|
||||
// ==================== 页面头部访问方法 ====================
|
||||
|
||||
// numSlots 获取页面中的槽位数量
|
||||
//
|
||||
// 返回值:
|
||||
// - uint16: 当前页面的槽位数量
|
||||
//
|
||||
// 槽位数量表示页面中存储的记录数量,每个记录占用一个槽位
|
||||
func (p Page) numSlots() uint16 { return binary.LittleEndian.Uint16(p[0:2]) }
|
||||
|
||||
// setNumSlots 设置页面中的槽位数量
|
||||
//
|
||||
// 参数:
|
||||
// - n: 要设置的槽位数量
|
||||
//
|
||||
// 用于在插入或删除记录时更新槽位计数
|
||||
func (p Page) setNumSlots(n uint16) { binary.LittleEndian.PutUint16(p[0:2], n) }
|
||||
|
||||
// freeOff 获取空闲空间的起始偏移
|
||||
//
|
||||
// 返回值:
|
||||
// - uint16: 空闲空间的起始位置
|
||||
//
|
||||
// 空闲空间从页面末尾向前增长,freeOff指向下一个可用的位置
|
||||
func (p Page) freeOff() uint16 { return binary.LittleEndian.Uint16(p[2:4]) }
|
||||
|
||||
// setFreeOff 设置空闲空间的起始偏移
|
||||
//
|
||||
// 参数:
|
||||
// - off: 新的空闲空间偏移位置
|
||||
//
|
||||
// 在插入记录后需要更新空闲空间指针
|
||||
func (p Page) setFreeOff(off uint16) { binary.LittleEndian.PutUint16(p[2:4], off) }
|
||||
|
||||
// nextPage 获取链接的下一个页面编号
|
||||
//
|
||||
// 返回值:
|
||||
// - uint16: 下一个页面的编号,0表示没有下一页
|
||||
//
|
||||
// 支持页面链接,用于存储超出单页容量的数据
|
||||
func (p Page) nextPage() uint16 { return binary.LittleEndian.Uint16(p[4:6]) }
|
||||
|
||||
// setNextPage 设置链接的下一个页面编号
|
||||
//
|
||||
// 参数:
|
||||
// - n: 下一个页面的编号
|
||||
//
|
||||
// 用于建立页面链接关系
|
||||
func (p Page) setNextPage(n uint16) { binary.LittleEndian.PutUint16(p[4:6], n) }
|
||||
|
||||
// slotArray 获取页面的槽位数组
|
||||
//
|
||||
// 返回值:
|
||||
// - []uint16: 槽位数组,每个元素是记录在页面中的偏移量
|
||||
//
|
||||
// 槽位数组存储每个记录在页面中的位置,支持快速定位记录
|
||||
// 槽位值为0表示该槽位对应的记录已被删除
|
||||
func (p Page) slotArray() []uint16 {
|
||||
n := p.numSlots()
|
||||
arr := make([]uint16, n)
|
||||
for i := uint16(0); i < n; i++ {
|
||||
arr[i] = binary.LittleEndian.Uint16(p[6+i*2:])
|
||||
}
|
||||
return arr
|
||||
}
|
||||
|
||||
// setSlot 设置指定槽位的记录偏移量
|
||||
//
|
||||
// 参数:
|
||||
// - idx: 槽位索引
|
||||
// - off: 记录在页面中的偏移量,0表示删除
|
||||
//
|
||||
// 用于在插入记录时设置槽位指向记录位置,或在删除时标记槽位为空
|
||||
func (p Page) setSlot(idx uint16, off uint16) {
|
||||
binary.LittleEndian.PutUint16(p[6+idx*2:], off)
|
||||
}
|
||||
|
||||
// ==================== 页面I/O操作方法 ====================
|
||||
|
||||
// readPageDirect 直接从磁盘读取页面,绕过缓存
|
||||
//
|
||||
// 参数:
|
||||
// - pageNo: 页面编号
|
||||
//
|
||||
// 返回值:
|
||||
// - []byte: 页面数据
|
||||
// - error: 读取错误
|
||||
//
|
||||
// 用于需要绕过缓存直接访问磁盘的场景,如初始化或特殊操作
|
||||
func (pdb *PipelineDB) readPageDirect(pageNo uint16) ([]byte, error) {
|
||||
buf := make([]byte, PageSize)
|
||||
_, err := pdb.file.ReadAt(buf, int64(pageNo)*PageSize)
|
||||
return buf, err
|
||||
}
|
||||
|
||||
// readPage 读取页面,优先从缓存获取
|
||||
//
|
||||
// 参数:
|
||||
// - pageNo: 页面编号
|
||||
//
|
||||
// 返回值:
|
||||
// - Page: 页面对象
|
||||
// - error: 读取错误
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 首先尝试从页面缓存中获取
|
||||
// 2. 缓存未命中时从磁盘读取
|
||||
// 3. 将读取的页面放入缓存以提高后续访问性能
|
||||
func (pdb *PipelineDB) readPage(pageNo uint16) (Page, error) {
|
||||
// 尝试从缓存获取
|
||||
if p, found := pdb.cache.Get(pageNo); found {
|
||||
return Page(p), nil
|
||||
}
|
||||
|
||||
// 从磁盘读取
|
||||
p, err := pdb.readPageDirect(pageNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 放入缓存
|
||||
pdb.cache.Put(pageNo, p, false)
|
||||
return Page(p), nil
|
||||
}
|
||||
|
||||
// writePage 写入页面到缓存和磁盘
|
||||
//
|
||||
// 参数:
|
||||
// - pageNo: 页面编号
|
||||
// - p: 页面数据
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 写入错误
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 将页面写入缓存,标记为脏页
|
||||
// 2. 立即同步写入磁盘,确保数据持久性
|
||||
func (pdb *PipelineDB) writePage(pageNo uint16, p Page) error {
|
||||
// 写入缓存
|
||||
pdb.cache.Put(pageNo, []byte(p), true)
|
||||
|
||||
// 立即写入磁盘
|
||||
_, err := pdb.file.WriteAt(p, int64(pageNo)*PageSize)
|
||||
return err
|
||||
}
|
||||
|
||||
// writePageDirect 直接写入页面到磁盘,绕过缓存
|
||||
//
|
||||
// 参数:
|
||||
// - pageNo: 页面编号
|
||||
// - p: 页面数据
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 写入错误
|
||||
//
|
||||
// 用于需要绕过缓存直接写入磁盘的场景
|
||||
func (pdb *PipelineDB) writePageDirect(pageNo uint16, p []byte) error {
|
||||
_, err := pdb.file.WriteAt(p, int64(pageNo)*PageSize)
|
||||
return err
|
||||
}
|
||||
|
||||
// ==================== 页面管理方法 ====================
|
||||
|
||||
// allocPage 分配一个新的页面
|
||||
//
|
||||
// 返回值:
|
||||
// - uint16: 分配的页面编号
|
||||
// - error: 分配错误
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 首先尝试从空闲页面管理器中分配已释放的页面
|
||||
// 2. 如果没有空闲页面,则分配新的页面
|
||||
// 3. 初始化页面头部信息
|
||||
// 4. 更新数据库头部的总页面数
|
||||
//
|
||||
// 线程安全:使用互斥锁保护并发访问
|
||||
func (pdb *PipelineDB) allocPage() (uint16, error) {
|
||||
pdb.mu.Lock()
|
||||
defer pdb.mu.Unlock()
|
||||
|
||||
// 尝试从空闲页分配
|
||||
if pageNo, found := pdb.freePageMgr.AllocPage(); found {
|
||||
// 初始化页面
|
||||
p := make(Page, PageSize)
|
||||
p.setNumSlots(0)
|
||||
p.setFreeOff(PageSize)
|
||||
p.setNextPage(0)
|
||||
if err := pdb.writePage(pageNo, p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return pageNo, nil
|
||||
}
|
||||
|
||||
// 分配新页面
|
||||
pageNo := pdb.header.TotalPages
|
||||
p := make(Page, PageSize)
|
||||
p.setNumSlots(0)
|
||||
p.setFreeOff(PageSize)
|
||||
p.setNextPage(0)
|
||||
if err := pdb.writePage(pageNo, p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 更新头部
|
||||
pdb.header.TotalPages++
|
||||
if err := pdb.saveHeader(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return pageNo, nil
|
||||
}
|
||||
|
||||
// freePage 释放页面,将其标记为可重用
|
||||
//
|
||||
// 参数:
|
||||
// - pageNo: 要释放的页面编号
|
||||
//
|
||||
// 返回值:
|
||||
// - error: 释放错误
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 将页面添加到空闲页面管理器
|
||||
// 2. 从缓存中移除该页面
|
||||
//
|
||||
// 线程安全:使用互斥锁保护并发访问
|
||||
func (pdb *PipelineDB) freePage(pageNo uint16) error {
|
||||
pdb.mu.Lock()
|
||||
defer pdb.mu.Unlock()
|
||||
|
||||
pdb.freePageMgr.FreePage(pageNo)
|
||||
pdb.cache.Invalidate(pageNo)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== 记录操作方法 ====================
|
||||
|
||||
// insertToChain 在页面链中插入记录
|
||||
//
|
||||
// 参数:
|
||||
// - rootPage: 页面链的根页面编号
|
||||
// - id: 记录ID
|
||||
// - data: 记录数据
|
||||
//
|
||||
// 返回值:
|
||||
// - uint16: 插入的页面编号
|
||||
// - uint16: 插入的槽位编号
|
||||
// - error: 插入错误
|
||||
//
|
||||
// 执行流程:
|
||||
// 1. 从根页面开始尝试插入记录
|
||||
// 2. 如果当前页面空间不足,沿着页面链查找下一个页面
|
||||
// 3. 如果到达链尾仍无空间,分配新页面并链接到链尾
|
||||
// 4. 在找到的页面中插入记录
|
||||
//
|
||||
// 支持动态扩展:当数据量超过单页容量时自动扩展页面链
|
||||
func (pdb *PipelineDB) insertToChain(rootPage uint16, id int64, data []byte) (uint16, uint16, error) {
|
||||
pageNo := rootPage
|
||||
for {
|
||||
// 尝试插入当前页
|
||||
slotNo, err := pdb.insertToPage(pageNo, id, data)
|
||||
if err == nil {
|
||||
return pageNo, slotNo, nil
|
||||
}
|
||||
if err.Error() != "page full" {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
// 页满:检查是否有后继页
|
||||
p, err := pdb.readPage(pageNo)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
nextPage := p.nextPage()
|
||||
if nextPage != 0 {
|
||||
pageNo = nextPage
|
||||
continue
|
||||
}
|
||||
|
||||
// 分配新页并链接
|
||||
newPage, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
p.setNextPage(newPage)
|
||||
if err := pdb.writePage(pageNo, p); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
pageNo = newPage
|
||||
}
|
||||
}
|
||||
|
||||
func (pdb *PipelineDB) insertToPage(pageNo uint16, id int64, data []byte) (uint16, error) {
|
||||
pdb.mu.Lock()
|
||||
defer pdb.mu.Unlock()
|
||||
|
||||
p, err := pdb.readPage(pageNo)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 计算所需空间
|
||||
recSize := 8 + 1 + len(data)
|
||||
if len(data) >= 128 {
|
||||
recSize++
|
||||
}
|
||||
|
||||
freeOff := p.freeOff()
|
||||
requiredSpace := uint16(recSize)
|
||||
slotSpace := 6 + 2*(p.numSlots()+1)
|
||||
|
||||
if freeOff < requiredSpace || freeOff-requiredSpace < slotSpace {
|
||||
return 0, errors.New("page full")
|
||||
}
|
||||
|
||||
newFree := freeOff - requiredSpace
|
||||
|
||||
// 写入记录
|
||||
off := newFree
|
||||
binary.LittleEndian.PutUint64(p[off:], uint64(id))
|
||||
off += 8
|
||||
|
||||
n := binary.PutUvarint(p[off:], uint64(len(data)))
|
||||
off += uint16(n)
|
||||
|
||||
copy(p[off:], data)
|
||||
|
||||
// 更新槽数组
|
||||
numSlots := p.numSlots()
|
||||
p.setSlot(numSlots, newFree)
|
||||
p.setNumSlots(numSlots + 1)
|
||||
p.setFreeOff(newFree)
|
||||
|
||||
if err := pdb.writePage(pageNo, p); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return numSlots, nil
|
||||
}
|
||||
|
||||
func (pdb *PipelineDB) readRecord(pageNo uint16, slotNo uint16, expectedID int64) ([]byte, error) {
|
||||
p, err := pdb.readPage(pageNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
slots := p.slotArray()
|
||||
if int(slotNo) >= len(slots) {
|
||||
return nil, errors.New("invalid slot index")
|
||||
}
|
||||
|
||||
off := slots[slotNo]
|
||||
if off == 0 {
|
||||
return nil, errors.New("record deleted")
|
||||
}
|
||||
|
||||
// 验证记录ID
|
||||
recID := int64(binary.LittleEndian.Uint64(p[off:]))
|
||||
if recID != expectedID {
|
||||
return nil, errors.New("index inconsistent")
|
||||
}
|
||||
|
||||
// 读取payload
|
||||
pldOff := int(off) + 8
|
||||
length, n := binary.Uvarint(p[pldOff:])
|
||||
if n <= 0 {
|
||||
return nil, errors.New("bad varint")
|
||||
}
|
||||
|
||||
pldOff += n
|
||||
if pldOff+int(length) > PageSize {
|
||||
return nil, errors.New("payload out of bounds")
|
||||
}
|
||||
|
||||
return p[pldOff : pldOff+int(length)], nil
|
||||
}
|
||||
|
||||
func (pdb *PipelineDB) updateInPlace(pageNo uint16, slotNo uint16, id int64, data []byte) error {
|
||||
p, err := pdb.readPage(pageNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
slots := p.slotArray()
|
||||
if int(slotNo) >= len(slots) {
|
||||
return errors.New("invalid slot index")
|
||||
}
|
||||
|
||||
off := slots[slotNo]
|
||||
if off == 0 {
|
||||
return errors.New("record deleted")
|
||||
}
|
||||
|
||||
// 验证记录ID
|
||||
recID := int64(binary.LittleEndian.Uint64(p[off:]))
|
||||
if recID != id {
|
||||
return errors.New("index inconsistent")
|
||||
}
|
||||
|
||||
// 检查原payload长度
|
||||
pldOff := int(off) + 8
|
||||
oldLength, n := binary.Uvarint(p[pldOff:])
|
||||
if n <= 0 {
|
||||
return errors.New("bad varint in existing record")
|
||||
}
|
||||
|
||||
// 如果长度相同,原地更新
|
||||
if int(oldLength) == len(data) {
|
||||
pldStart := pldOff + n
|
||||
copy(p[pldStart:pldStart+len(data)], data)
|
||||
return pdb.writePage(pageNo, p)
|
||||
}
|
||||
|
||||
return errors.New("cannot update in place")
|
||||
}
|
||||
|
||||
func (pdb *PipelineDB) deleteRecord(pageNo uint16, slotNo uint16, id int64, idx *TableIndex) error {
|
||||
pdb.mu.Lock()
|
||||
defer pdb.mu.Unlock()
|
||||
|
||||
p, err := pdb.readPage(pageNo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
numSlots := p.numSlots()
|
||||
if slotNo >= numSlots {
|
||||
return errors.New("slot index out of bounds")
|
||||
}
|
||||
|
||||
// 标记槽位为删除
|
||||
p.setSlot(slotNo, 0)
|
||||
|
||||
// 如果是最后一个槽位,压缩槽数组
|
||||
if slotNo == numSlots-1 {
|
||||
newSlots := numSlots
|
||||
slots := p.slotArray()
|
||||
for newSlots > 0 && slots[newSlots-1] == 0 {
|
||||
newSlots--
|
||||
}
|
||||
p.setNumSlots(newSlots)
|
||||
}
|
||||
|
||||
// 写回页面
|
||||
if err := pdb.writePage(pageNo, p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 从索引中删除(如果提供了索引)
|
||||
if idx != nil {
|
||||
idx.Delete(id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 高级数据库操作
|
||||
|
||||
func (pdb *PipelineDB) insert(group string, id int64, data []byte) error {
|
||||
if len(data) > MaxRecSize {
|
||||
return errors.New("record too large")
|
||||
}
|
||||
|
||||
// 获取组索引
|
||||
idx := pdb.indexMgr.GetOrCreateIndex(group)
|
||||
|
||||
// 检查是否已存在
|
||||
if _, _, exists := idx.Get(id); exists {
|
||||
return errors.New("record already exists")
|
||||
}
|
||||
|
||||
// 行锁
|
||||
mutex := pdb.getRowMutex(id)
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
// 插入到页链
|
||||
pageNo, slotNo, err := pdb.insertToChain(pdb.header.RootPage, id, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新索引
|
||||
idx.Insert(id, pageNo, slotNo)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pdb *PipelineDB) get(group string, id int64) ([]byte, error) {
|
||||
idx, exists := pdb.indexMgr.GetIndex(group)
|
||||
if !exists {
|
||||
return nil, errors.New("group not found")
|
||||
}
|
||||
|
||||
// 使用索引定位
|
||||
pageNo, slotNo, found := idx.Get(id)
|
||||
if !found {
|
||||
return nil, errors.New("record not found")
|
||||
}
|
||||
|
||||
// 读取记录
|
||||
return pdb.readRecord(pageNo, slotNo, id)
|
||||
}
|
||||
|
||||
func (pdb *PipelineDB) update(group string, id int64, data []byte) error {
|
||||
if len(data) > MaxRecSize {
|
||||
return errors.New("record too large")
|
||||
}
|
||||
|
||||
idx, exists := pdb.indexMgr.GetIndex(group)
|
||||
if !exists {
|
||||
return errors.New("group not found")
|
||||
}
|
||||
|
||||
pageNo, slotNo, found := idx.Get(id)
|
||||
if !found {
|
||||
return errors.New("record not found")
|
||||
}
|
||||
|
||||
// 行锁
|
||||
mutex := pdb.getRowMutex(id)
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
// 尝试原地更新
|
||||
if err := pdb.updateInPlace(pageNo, slotNo, id, data); err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 删除后重新插入
|
||||
if err := pdb.deleteRecord(pageNo, slotNo, id, idx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newPageNo, newSlotNo, err := pdb.insertToChain(pdb.header.RootPage, id, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx.Insert(id, newPageNo, newSlotNo)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pdb *PipelineDB) rangeQuery(group string, startID, endID int64, visitor func(id int64, data []byte) error) error {
|
||||
idx, exists := pdb.indexMgr.GetIndex(group)
|
||||
if !exists {
|
||||
return errors.New("group not found")
|
||||
}
|
||||
|
||||
idx.Range(startID, endID, func(id int64, pageNo, slotNo uint16) bool {
|
||||
data, err := pdb.readRecord(pageNo, slotNo, id)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if err := visitor(id, data); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
709
storage_test.go
Normal file
709
storage_test.go
Normal file
@@ -0,0 +1,709 @@
|
||||
package pipelinedb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPageBasicOperations 测试页面基本操作
|
||||
func TestPageBasicOperations(t *testing.T) {
|
||||
p := make(Page, PageSize)
|
||||
|
||||
// 测试槽数量操作
|
||||
p.setNumSlots(5)
|
||||
if p.numSlots() != 5 {
|
||||
t.Errorf("numSlots() = %d, want 5", p.numSlots())
|
||||
}
|
||||
|
||||
// 测试空闲偏移操作
|
||||
p.setFreeOff(1000)
|
||||
if p.freeOff() != 1000 {
|
||||
t.Errorf("freeOff() = %d, want 1000", p.freeOff())
|
||||
}
|
||||
|
||||
// 测试下一页操作
|
||||
p.setNextPage(42)
|
||||
if p.nextPage() != 42 {
|
||||
t.Errorf("nextPage() = %d, want 42", p.nextPage())
|
||||
}
|
||||
|
||||
// 测试槽位操作
|
||||
p.setSlot(0, 100)
|
||||
p.setSlot(1, 200)
|
||||
p.setSlot(2, 300)
|
||||
|
||||
slots := p.slotArray()
|
||||
expected := []uint16{100, 200, 300, 0, 0}
|
||||
|
||||
for i, exp := range expected {
|
||||
if slots[i] != exp {
|
||||
t.Errorf("slot[%d] = %d, want %d", i, slots[i], exp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MockFileForStorage 用于测试存储的模拟文件
|
||||
type MockFileForStorage struct {
|
||||
data []byte
|
||||
offset int64
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewMockFileForStorage(size int) *MockFileForStorage {
|
||||
return &MockFileForStorage{
|
||||
data: make([]byte, size),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *MockFileForStorage) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if off >= int64(len(f.data)) {
|
||||
return 0, errors.New("EOF")
|
||||
}
|
||||
|
||||
n = copy(p, f.data[off:])
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (f *MockFileForStorage) WriteAt(p []byte, off int64) (n int, err error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
// 扩展数据如果需要
|
||||
needed := int(off) + len(p)
|
||||
if needed > len(f.data) {
|
||||
newData := make([]byte, needed)
|
||||
copy(newData, f.data)
|
||||
f.data = newData
|
||||
}
|
||||
|
||||
n = copy(f.data[off:], p)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (f *MockFileForStorage) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *MockFileForStorage) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestPipelineDBReadWritePage 测试页面读写操作
|
||||
func TestPipelineDBReadWritePage(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test_page_rw_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 10}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 分配一个新页面
|
||||
pageNo, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPage failed: %v", err)
|
||||
}
|
||||
|
||||
// 创建测试页面数据
|
||||
testPage := make(Page, PageSize)
|
||||
testPage.setNumSlots(2)
|
||||
testPage.setFreeOff(PageSize - 100)
|
||||
testPage.setNextPage(123)
|
||||
|
||||
// 写入页面
|
||||
err = pdb.writePage(pageNo, testPage)
|
||||
if err != nil {
|
||||
t.Fatalf("writePage failed: %v", err)
|
||||
}
|
||||
|
||||
// 读取页面
|
||||
readPage, err := pdb.readPage(pageNo)
|
||||
if err != nil {
|
||||
t.Fatalf("readPage failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证页面内容
|
||||
if readPage.numSlots() != 2 {
|
||||
t.Errorf("numSlots = %d, want 2", readPage.numSlots())
|
||||
}
|
||||
if readPage.freeOff() != PageSize-100 {
|
||||
t.Errorf("freeOff = %d, want %d", readPage.freeOff(), PageSize-100)
|
||||
}
|
||||
if readPage.nextPage() != 123 {
|
||||
t.Errorf("nextPage = %d, want 123", readPage.nextPage())
|
||||
}
|
||||
}
|
||||
|
||||
// TestPipelineDBAllocFreePage 测试页面分配和释放
|
||||
func TestPipelineDBAllocFreePage(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test_alloc_free_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 10}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 分配多个页面
|
||||
var allocatedPages []uint16
|
||||
for i := 0; i < 5; i++ {
|
||||
pageNo, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPage[%d] failed: %v", i, err)
|
||||
}
|
||||
allocatedPages = append(allocatedPages, pageNo)
|
||||
}
|
||||
|
||||
// 验证页面号是连续的(新数据库)
|
||||
for i, pageNo := range allocatedPages {
|
||||
expected := uint16(i + 2) // 页面0是头部,页面1是计数器
|
||||
if pageNo != expected {
|
||||
t.Errorf("allocated page[%d] = %d, want %d", i, pageNo, expected)
|
||||
}
|
||||
}
|
||||
|
||||
// 释放一些页面
|
||||
pdb.freePage(allocatedPages[1])
|
||||
pdb.freePage(allocatedPages[3])
|
||||
|
||||
// 再次分配页面,应该重用释放的页面
|
||||
pageNo1, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPage after free failed: %v", err)
|
||||
}
|
||||
|
||||
pageNo2, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPage after free failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证重用了释放的页面(LIFO顺序)
|
||||
if pageNo1 != allocatedPages[3] || pageNo2 != allocatedPages[1] {
|
||||
t.Errorf("reused pages = [%d, %d], want [%d, %d]",
|
||||
pageNo1, pageNo2, allocatedPages[3], allocatedPages[1])
|
||||
}
|
||||
}
|
||||
|
||||
// TestInsertToPage 测试页面内记录插入
|
||||
func TestInsertToPage(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test_insert_page_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 10}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 分配一个页面
|
||||
pageNo, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPage failed: %v", err)
|
||||
}
|
||||
|
||||
// 插入多条记录
|
||||
testData := []struct {
|
||||
id int64
|
||||
data []byte
|
||||
}{
|
||||
{1, []byte("record1")},
|
||||
{2, []byte("record2")},
|
||||
{3, []byte("record3")},
|
||||
}
|
||||
|
||||
for i, test := range testData {
|
||||
slotNo, err := pdb.insertToPage(pageNo, test.id, test.data)
|
||||
if err != nil {
|
||||
t.Fatalf("insertToPage(%d) failed: %v", test.id, err)
|
||||
}
|
||||
// 槽号从0开始,第一条记录应该在槽0
|
||||
expectedSlot := uint16(i)
|
||||
if slotNo != expectedSlot {
|
||||
t.Errorf("insertToPage(%d) returned slot %d, want %d", test.id, slotNo, expectedSlot)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证页面状态
|
||||
page, err := pdb.readPage(pageNo)
|
||||
if err != nil {
|
||||
t.Fatalf("readPage failed: %v", err)
|
||||
}
|
||||
|
||||
if page.numSlots() != uint16(len(testData)) {
|
||||
t.Errorf("numSlots = %d, want %d", page.numSlots(), len(testData))
|
||||
}
|
||||
}
|
||||
|
||||
// TestInsertToPageFull 测试页面满时的处理
|
||||
func TestInsertToPageFull(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test_page_full_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 10}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 分配一个页面
|
||||
pageNo, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPage failed: %v", err)
|
||||
}
|
||||
|
||||
// 插入大量记录直到页面满
|
||||
mediumData := make([]byte, 400) // 400字节数据
|
||||
for i := 0; i < len(mediumData); i++ {
|
||||
mediumData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
var insertCount int
|
||||
for i := int64(1); i <= 100; i++ {
|
||||
_, err := pdb.insertToPage(pageNo, i, mediumData)
|
||||
if err != nil {
|
||||
// 页面满了,这是预期的
|
||||
t.Logf("Page full after inserting %d records", insertCount)
|
||||
|
||||
// 验证确实是因为页面满而失败
|
||||
if err.Error() != "page full" {
|
||||
t.Errorf("expected 'page full' error, got: %v", err)
|
||||
}
|
||||
|
||||
// 测试通过,页面确实满了
|
||||
return
|
||||
}
|
||||
insertCount++
|
||||
}
|
||||
|
||||
// 如果循环结束还没有遇到页面满的情况,说明测试有问题
|
||||
t.Errorf("should have encountered page full condition, but inserted %d records", insertCount)
|
||||
}
|
||||
|
||||
// TestReadRecord 测试记录读取
|
||||
func TestReadRecord(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test_read_record_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 10}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 分配页面并插入记录
|
||||
pageNo, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPage failed: %v", err)
|
||||
}
|
||||
|
||||
testID := int64(42)
|
||||
testData := []byte("test record data")
|
||||
|
||||
slotNo, err := pdb.insertToPage(pageNo, testID, testData)
|
||||
if err != nil {
|
||||
t.Fatalf("insertToPage failed: %v", err)
|
||||
}
|
||||
|
||||
// 读取记录
|
||||
readData, err := pdb.readRecord(pageNo, slotNo, testID)
|
||||
if err != nil {
|
||||
t.Fatalf("readRecord failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证数据
|
||||
if string(readData) != string(testData) {
|
||||
t.Errorf("readRecord data = %q, want %q", string(readData), string(testData))
|
||||
}
|
||||
|
||||
// 测试读取不存在的记录
|
||||
_, err = pdb.readRecord(pageNo, slotNo, testID+1) // 错误的ID
|
||||
if err == nil {
|
||||
t.Error("readRecord should fail with wrong ID")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpdateInPlace 测试原地更新
|
||||
func TestUpdateInPlace(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test_update_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 10}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 分配页面并插入记录
|
||||
pageNo, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPage failed: %v", err)
|
||||
}
|
||||
|
||||
testID := int64(100)
|
||||
originalData := []byte("original data")
|
||||
|
||||
slotNo, err := pdb.insertToPage(pageNo, testID, originalData)
|
||||
if err != nil {
|
||||
t.Fatalf("insertToPage failed: %v", err)
|
||||
}
|
||||
|
||||
// 测试相同大小的更新(应该成功)
|
||||
newData := []byte("updated data") // 相同长度
|
||||
err = pdb.updateInPlace(pageNo, slotNo, testID, newData)
|
||||
if err != nil {
|
||||
t.Fatalf("updateInPlace with same size failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证更新后的数据
|
||||
readData, err := pdb.readRecord(pageNo, slotNo, testID)
|
||||
if err != nil {
|
||||
t.Fatalf("readRecord after update failed: %v", err)
|
||||
}
|
||||
|
||||
if string(readData) != string(newData) {
|
||||
t.Errorf("updated data = %q, want %q", string(readData), string(newData))
|
||||
}
|
||||
|
||||
// 测试更大数据的更新(应该失败)
|
||||
largerData := []byte("this is much larger data that should not fit")
|
||||
err = pdb.updateInPlace(pageNo, slotNo, testID, largerData)
|
||||
if err == nil {
|
||||
t.Error("updateInPlace with larger data should fail")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteRecord 测试记录删除
|
||||
func TestDeleteRecord(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test_delete_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 10}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 分配页面并插入记录
|
||||
pageNo, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPage failed: %v", err)
|
||||
}
|
||||
|
||||
testID := int64(200)
|
||||
testData := []byte("record to delete")
|
||||
|
||||
slotNo, err := pdb.insertToPage(pageNo, testID, testData)
|
||||
if err != nil {
|
||||
t.Fatalf("insertToPage failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证记录存在
|
||||
_, err = pdb.readRecord(pageNo, slotNo, testID)
|
||||
if err != nil {
|
||||
t.Fatalf("readRecord before delete failed: %v", err)
|
||||
}
|
||||
|
||||
// 删除记录(传入nil索引,简化测试)
|
||||
err = pdb.deleteRecord(pageNo, slotNo, testID, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("deleteRecord failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证读取已删除的记录会失败
|
||||
_, err = pdb.readRecord(pageNo, slotNo, testID)
|
||||
if err == nil {
|
||||
t.Error("readRecord should fail for deleted record")
|
||||
}
|
||||
}
|
||||
|
||||
// TestInsertToChain 测试链式插入
|
||||
func TestInsertToChain(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test_chain_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
tmpFile.Close()
|
||||
|
||||
config := &Config{CacheSize: 10}
|
||||
pdb, err := Open(Options{
|
||||
Filename: tmpFile.Name(),
|
||||
Config: config,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Open failed: %v", err)
|
||||
}
|
||||
defer pdb.Stop()
|
||||
|
||||
// 分配根页面
|
||||
rootPage, err := pdb.allocPage()
|
||||
if err != nil {
|
||||
t.Fatalf("allocPage failed: %v", err)
|
||||
}
|
||||
|
||||
// 插入大量记录,触发链式插入
|
||||
largeData := make([]byte, 500) // 500字节数据
|
||||
for i := 0; i < len(largeData); i++ {
|
||||
largeData[i] = byte(i % 256)
|
||||
}
|
||||
|
||||
var insertedIDs []int64
|
||||
for i := int64(1); i <= 20; i++ {
|
||||
pageNo, _, err := pdb.insertToChain(rootPage, i, largeData)
|
||||
if err != nil {
|
||||
t.Fatalf("insertToChain(%d) failed: %v", i, err)
|
||||
}
|
||||
if pageNo == 0 {
|
||||
t.Errorf("insertToChain(%d) returned invalid page 0", i)
|
||||
}
|
||||
insertedIDs = append(insertedIDs, i)
|
||||
}
|
||||
|
||||
// 验证至少插入了一些记录
|
||||
if len(insertedIDs) == 0 {
|
||||
t.Error("should be able to insert at least some records")
|
||||
}
|
||||
|
||||
// 验证可能创建了多个页面(链式结构)
|
||||
// 这里我们只验证功能正常,不验证具体的页面数量
|
||||
t.Logf("Successfully inserted %d records using chain insertion", len(insertedIDs))
|
||||
}
|
||||
|
||||
// TestHighLevelOperations 测试高级数据库操作
|
||||
func TestHighLevelOperations(t *testing.T) {
|
||||
// 创建临时文件
|
||||
tmpFile, err := os.CreateTemp("", "test_storage_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
defer tmpFile.Close()
|
||||
|
||||
// 创建数据库实例
|
||||
pdb := &PipelineDB{
|
||||
file: tmpFile,
|
||||
cache: NewPageCache(10),
|
||||
freePageMgr: NewFreePageManager(),
|
||||
indexMgr: NewIndexManager(),
|
||||
rowMutexes: make(map[int64]*sync.RWMutex),
|
||||
header: &Header{
|
||||
TotalPages: 1,
|
||||
RootPage: 1,
|
||||
},
|
||||
}
|
||||
|
||||
// 初始化根页面
|
||||
p := make(Page, PageSize)
|
||||
p.setNumSlots(0)
|
||||
p.setFreeOff(PageSize)
|
||||
p.setNextPage(0)
|
||||
pdb.writePage(1, p)
|
||||
|
||||
group := "test_group"
|
||||
testData := []byte("high level test data")
|
||||
|
||||
// 测试插入
|
||||
err = pdb.insert(group, 300, testData)
|
||||
if err != nil {
|
||||
t.Errorf("insert failed: %v", err)
|
||||
}
|
||||
|
||||
// 测试获取
|
||||
data, err := pdb.get(group, 300)
|
||||
if err != nil {
|
||||
t.Errorf("get failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(data, testData) {
|
||||
t.Errorf("get data = %s, want %s", string(data), string(testData))
|
||||
}
|
||||
|
||||
// 测试更新
|
||||
newData := []byte("updated high level data")
|
||||
err = pdb.update(group, 300, newData)
|
||||
if err != nil {
|
||||
t.Errorf("update failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证更新
|
||||
data, err = pdb.get(group, 300)
|
||||
if err != nil {
|
||||
t.Errorf("get after update failed: %v", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(data, newData) {
|
||||
t.Errorf("updated data = %s, want %s", string(data), string(newData))
|
||||
}
|
||||
|
||||
// 测试重复插入
|
||||
err = pdb.insert(group, 300, testData)
|
||||
if err == nil || err.Error() != "record already exists" {
|
||||
t.Errorf("expected 'record already exists' error, got %v", err)
|
||||
}
|
||||
|
||||
// 测试不存在的记录
|
||||
_, err = pdb.get(group, 999)
|
||||
if err == nil || err.Error() != "record not found" {
|
||||
t.Errorf("expected 'record not found' error, got %v", err)
|
||||
}
|
||||
|
||||
// 测试不存在的组
|
||||
_, err = pdb.get("non_existent_group", 300)
|
||||
if err == nil || err.Error() != "group not found" {
|
||||
t.Errorf("expected 'group not found' error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRangeQuery 测试范围查询
|
||||
func TestRangeQuery(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp("", "test_range_*.db")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create temp file: %v", err)
|
||||
}
|
||||
defer os.Remove(tmpFile.Name())
|
||||
defer tmpFile.Close()
|
||||
|
||||
pdb := &PipelineDB{
|
||||
file: tmpFile,
|
||||
cache: NewPageCache(10),
|
||||
freePageMgr: NewFreePageManager(),
|
||||
indexMgr: NewIndexManager(),
|
||||
rowMutexes: make(map[int64]*sync.RWMutex),
|
||||
header: &Header{
|
||||
TotalPages: 1,
|
||||
RootPage: 1,
|
||||
},
|
||||
}
|
||||
|
||||
// 初始化根页面
|
||||
p := make(Page, PageSize)
|
||||
p.setNumSlots(0)
|
||||
p.setFreeOff(PageSize)
|
||||
pdb.writePage(1, p)
|
||||
|
||||
group := "range_test_group"
|
||||
|
||||
// 插入测试数据
|
||||
testRecords := map[int64]string{
|
||||
100: "record_100",
|
||||
200: "record_200",
|
||||
300: "record_300",
|
||||
400: "record_400",
|
||||
500: "record_500",
|
||||
}
|
||||
|
||||
for id, data := range testRecords {
|
||||
err := pdb.insert(group, id, []byte(data))
|
||||
if err != nil {
|
||||
t.Errorf("insert failed for ID %d: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行范围查询
|
||||
var results []struct {
|
||||
id int64
|
||||
data string
|
||||
}
|
||||
|
||||
err = pdb.rangeQuery(group, 200, 400, func(id int64, data []byte) error {
|
||||
results = append(results, struct {
|
||||
id int64
|
||||
data string
|
||||
}{id, string(data)})
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("rangeQuery failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证结果
|
||||
expectedResults := []struct {
|
||||
id int64
|
||||
data string
|
||||
}{
|
||||
{200, "record_200"},
|
||||
{300, "record_300"},
|
||||
{400, "record_400"},
|
||||
}
|
||||
|
||||
if len(results) != len(expectedResults) {
|
||||
t.Errorf("result count = %d, want %d", len(results), len(expectedResults))
|
||||
}
|
||||
|
||||
for i, expected := range expectedResults {
|
||||
if i >= len(results) {
|
||||
t.Errorf("missing result %d", i)
|
||||
continue
|
||||
}
|
||||
|
||||
if results[i].id != expected.id || results[i].data != expected.data {
|
||||
t.Errorf("result[%d] = {%d, %s}, want {%d, %s}",
|
||||
i, results[i].id, results[i].data, expected.id, expected.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user