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
|
||
}
|
||
}
|