feat: backend core - auth, user, role, permission, device, webhook, monitoring, cache, repository, service, middleware, API handlers
This commit is contained in:
305
internal/repository/repo_bench_test.go
Normal file
305
internal/repository/repo_bench_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user