306 lines
7.3 KiB
Go
306 lines
7.3 KiB
Go
|
|
// repo_bench_test.go — repository 层性能基准测试
|
|||
|
|
// 覆盖:批量写入、并发只读查询、分页列表、更新状态、软删除
|
|||
|
|
package repository
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"sync"
|
|||
|
|
"sync/atomic"
|
|||
|
|
"testing"
|
|||
|
|
|
|||
|
|
_ "modernc.org/sqlite"
|
|||
|
|
gormsqlite "gorm.io/driver/sqlite"
|
|||
|
|
"gorm.io/gorm"
|
|||
|
|
"gorm.io/gorm/logger"
|
|||
|
|
|
|||
|
|
"github.com/user-management-system/internal/domain"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var repoBenchCounter int64
|
|||
|
|
|
|||
|
|
// openBenchDB 为 Benchmark 打开独立内存 DB(不依赖 *testing.T)
|
|||
|
|
func openBenchDB(b *testing.B) *gorm.DB {
|
|||
|
|
b.Helper()
|
|||
|
|
id := atomic.AddInt64(&repoBenchCounter, 1)
|
|||
|
|
dsn := fmt.Sprintf("file:repobenchdb%d?mode=memory&cache=private", id)
|
|||
|
|
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
|
|||
|
|
DriverName: "sqlite",
|
|||
|
|
DSN: dsn,
|
|||
|
|
}), &gorm.Config{
|
|||
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
b.Fatalf("openBenchDB: %v", err)
|
|||
|
|
}
|
|||
|
|
if err := db.AutoMigrate(
|
|||
|
|
&domain.User{},
|
|||
|
|
&domain.Role{},
|
|||
|
|
&domain.Permission{},
|
|||
|
|
&domain.UserRole{},
|
|||
|
|
&domain.RolePermission{},
|
|||
|
|
); err != nil {
|
|||
|
|
b.Fatalf("AutoMigrate: %v", err)
|
|||
|
|
}
|
|||
|
|
return db
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// seedUsers 往 DB 插入 n 条用户
|
|||
|
|
func seedUsers(b *testing.B, repo *UserRepository, n int) {
|
|||
|
|
b.Helper()
|
|||
|
|
ctx := context.Background()
|
|||
|
|
for i := 0; i < n; i++ {
|
|||
|
|
if err := repo.Create(ctx, &domain.User{
|
|||
|
|
Username: fmt.Sprintf("benchuser%06d", i),
|
|||
|
|
Email: domain.StrPtr(fmt.Sprintf("bench%06d@example.com", i)),
|
|||
|
|
Phone: domain.StrPtr(fmt.Sprintf("1380000%04d", i%10000)),
|
|||
|
|
Password: "hashed_placeholder",
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
}); err != nil {
|
|||
|
|
b.Fatalf("seedUsers i=%d: %v", i, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_Create — 单条写入吞吐 ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_Create(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
repo.Create(ctx, &domain.User{ //nolint:errcheck
|
|||
|
|
Username: fmt.Sprintf("cr_%d_%d", b.N, i),
|
|||
|
|
Email: domain.StrPtr(fmt.Sprintf("cr_%d_%d@bench.com", b.N, i)),
|
|||
|
|
Password: "hash",
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_BulkCreate — 批量写入(串行) ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_BulkCreate(b *testing.B) {
|
|||
|
|
sizes := []int{10, 100, 500}
|
|||
|
|
for _, size := range sizes {
|
|||
|
|
size := size
|
|||
|
|
b.Run(fmt.Sprintf("batch=%d", size), func(b *testing.B) {
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
b.StopTimer()
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
users := make([]*domain.User, size)
|
|||
|
|
for j := 0; j < size; j++ {
|
|||
|
|
users[j] = &domain.User{
|
|||
|
|
Username: fmt.Sprintf("bulk_%d_%d_%d", i, j, size),
|
|||
|
|
Password: "hash",
|
|||
|
|
Status: domain.UserStatusActive,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
b.StartTimer()
|
|||
|
|
for _, u := range users {
|
|||
|
|
repo.Create(ctx, u) //nolint:errcheck
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_GetByID — 主键查询 ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_GetByID(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
seedUsers(b, repo, 1000)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
b.RunParallel(func(pb *testing.PB) {
|
|||
|
|
id := int64(1)
|
|||
|
|
for pb.Next() {
|
|||
|
|
repo.GetByID(ctx, id) //nolint:errcheck
|
|||
|
|
id++
|
|||
|
|
if id > 1000 {
|
|||
|
|
id = 1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_GetByUsername — 索引查询 ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_GetByUsername(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
seedUsers(b, repo, 500)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
repo.GetByUsername(ctx, fmt.Sprintf("benchuser%06d", i%500)) //nolint:errcheck
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_GetByEmail — 索引查询 ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_GetByEmail(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
seedUsers(b, repo, 500)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
repo.GetByEmail(ctx, fmt.Sprintf("bench%06d@example.com", i%500)) //nolint:errcheck
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_List — 分页列表 ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_List(b *testing.B) {
|
|||
|
|
pageSizes := []int{10, 50, 200}
|
|||
|
|
for _, ps := range pageSizes {
|
|||
|
|
ps := ps
|
|||
|
|
b.Run(fmt.Sprintf("pageSize=%d", ps), func(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
seedUsers(b, repo, 1000)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
repo.List(ctx, 0, ps) //nolint:errcheck
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_ListByStatus ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_ListByStatus(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
seedUsers(b, repo, 1000)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
repo.ListByStatus(ctx, domain.UserStatusActive, 0, 20) //nolint:errcheck
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_UpdateStatus ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_UpdateStatus(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
seedUsers(b, repo, 200)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
id := int64(i%200) + 1
|
|||
|
|
repo.UpdateStatus(ctx, id, domain.UserStatusActive) //nolint:errcheck
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_Update — 全字段更新 ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_Update(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
seedUsers(b, repo, 100)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
id := int64(i%100) + 1
|
|||
|
|
u, err := repo.GetByID(ctx, id)
|
|||
|
|
if err != nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
u.Nickname = fmt.Sprintf("nick%d", i)
|
|||
|
|
repo.Update(ctx, u) //nolint:errcheck
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_Delete — 软删除 ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_Delete(b *testing.B) {
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
b.StopTimer()
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
repo.Create(ctx, &domain.User{Username: "victim", Password: "hash", Status: domain.UserStatusActive}) //nolint:errcheck
|
|||
|
|
b.StartTimer()
|
|||
|
|
repo.Delete(ctx, 1) //nolint:errcheck
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_ExistsByUsername ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_ExistsByUsername(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
seedUsers(b, repo, 500)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
b.RunParallel(func(pb *testing.PB) {
|
|||
|
|
i := 0
|
|||
|
|
for pb.Next() {
|
|||
|
|
repo.ExistsByUsername(ctx, fmt.Sprintf("benchuser%06d", i%500)) //nolint:errcheck
|
|||
|
|
i++
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_ConcurrentReadWrite — 高并发读写混合 ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_ConcurrentReadWrite(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
seedUsers(b, repo, 200)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
var mu sync.Mutex // SQLite 不支持多写并发,需要序列化写入
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
b.RunParallel(func(pb *testing.PB) {
|
|||
|
|
i := int64(1)
|
|||
|
|
for pb.Next() {
|
|||
|
|
if i%10 == 0 {
|
|||
|
|
// 10% 写操作
|
|||
|
|
mu.Lock()
|
|||
|
|
repo.UpdateLastLogin(ctx, i%200+1, "10.0.0.1") //nolint:errcheck
|
|||
|
|
mu.Unlock()
|
|||
|
|
} else {
|
|||
|
|
// 90% 读操作
|
|||
|
|
repo.GetByID(ctx, i%200+1) //nolint:errcheck
|
|||
|
|
}
|
|||
|
|
i++
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- BenchmarkRepo_Search — 模糊搜索 ----------
|
|||
|
|
|
|||
|
|
func BenchmarkRepo_Search(b *testing.B) {
|
|||
|
|
db := openBenchDB(b)
|
|||
|
|
repo := NewUserRepository(db)
|
|||
|
|
seedUsers(b, repo, 2000)
|
|||
|
|
ctx := context.Background()
|
|||
|
|
b.ResetTimer()
|
|||
|
|
|
|||
|
|
keywords := []string{"benchuser000", "bench0001", "benchuser05"}
|
|||
|
|
for i := 0; i < b.N; i++ {
|
|||
|
|
repo.Search(ctx, keywords[i%len(keywords)], 0, 20) //nolint:errcheck
|
|||
|
|
}
|
|||
|
|
}
|