docs(constitution): 新增数据库设计原则(v2.4.0)

在项目宪章中新增第九条原则"数据库设计原则",明确禁止使用数据库外键约束和ORM关联标签。

主要变更:
- 新增原则IX:数据库设计原则(Database Design Principles)
- 强制要求:数据库表不得使用外键约束
- 强制要求:GORM模型不得使用ORM关联标签(foreignKey、hasMany等)
- 强制要求:表关系必须通过ID字段手动维护
- 强制要求:关联数据查询必须显式编写,避免ORM魔法
- 强制要求:时间字段由GORM处理,不使用数据库触发器

设计理念:
- 提升业务逻辑灵活性(无数据库约束限制)
- 优化高并发性能(无外键检查开销)
- 增强代码可读性(显式查询,无隐式预加载)
- 简化数据库架构和迁移流程
- 支持分布式和微服务场景

版本升级:2.3.0 → 2.4.0(MINOR)
This commit is contained in:
2025-11-13 13:40:19 +08:00
parent ea0c6a8b16
commit 984ccccc63
63 changed files with 12099 additions and 83 deletions

View File

@@ -0,0 +1,901 @@
# Research: 数据持久化与异步任务处理集成
**Feature**: 002-gorm-postgres-asynq
**Date**: 2025-11-12
**Purpose**: 记录技术选型决策、最佳实践和架构考量
## 概述
本文档记录了 GORM + PostgreSQL + Asynq 集成的技术研究成果,包括技术选型理由、配置建议、最佳实践和常见陷阱。
---
## 1. GORM 与 PostgreSQL 集成
### 决策:选择 GORM 作为 ORM 框架
**理由**
- **官方支持**GORM 是 Go 生态系统中最流行的 ORM社区活跃文档完善
- **PostgreSQL 原生支持**:提供专门的 PostgreSQL 驱动和方言
- **功能完整**:支持复杂查询、关联关系、事务、钩子、软删除等
- **性能优秀**:支持预编译语句、批量操作、连接池管理
- **符合 Constitution**:项目技术栈要求使用 GORM
**替代方案**
- **sqlx**:更轻量,但功能不够完整,需要手写更多 SQL
- **ent**Facebook 开发,功能强大,但学习曲线陡峭,且不符合项目技术栈要求
### GORM 最佳实践
#### 1.1 连接初始化
```go
// pkg/database/postgres.go
import (
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func InitPostgres(cfg *config.DatabaseConfig, log *zap.Logger) (*gorm.DB, error) {
dsn := fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.DBName, cfg.SSLMode,
)
// GORM 配置
gormConfig := &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent), // 使用 Zap 替代 GORM 日志
NamingStrategy: schema.NamingStrategy{
TablePrefix: "tb_", // 表名前缀
SingularTable: true, // 使用单数表名
},
PrepareStmt: true, // 启用预编译语句缓存
}
db, err := gorm.Open(postgres.Open(dsn), gormConfig)
if err != nil {
return nil, fmt.Errorf("连接 PostgreSQL 失败: %w", err)
}
// 获取底层 sql.DB 进行连接池配置
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("获取 sql.DB 失败: %w", err)
}
// 连接池配置(参考 Constitution 性能要求)
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns) // 最大连接数25
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns) // 最大空闲连接10
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime) // 连接最大生命周期5m
// 验证连接
if err := sqlDB.Ping(); err != nil {
return nil, fmt.Errorf("PostgreSQL 连接验证失败: %w", err)
}
log.Info("PostgreSQL 连接成功",
zap.String("host", cfg.Host),
zap.Int("port", cfg.Port),
zap.String("database", cfg.DBName))
return db, nil
}
```
#### 1.2 连接池配置建议
| 参数 | 推荐值 | 理由 |
|------|--------|------|
| MaxOpenConns | 25 | 平衡性能和资源,避免 PostgreSQL 连接耗尽 |
| MaxIdleConns | 10 | 保持足够的空闲连接以应对突发流量 |
| ConnMaxLifetime | 5m | 定期回收连接,避免长连接问题 |
**计算公式**
```
MaxOpenConns = (可用内存 / 每连接内存) * 安全系数
每连接内存 ≈ 10MBPostgreSQL 典型值)
安全系数 = 0.7(为其他进程预留资源)
```
#### 1.3 模型定义规范
```go
// internal/model/user.go
type User struct {
ID uint `gorm:"primarykey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 软删除
Username string `gorm:"uniqueIndex;not null;size:50" json:"username"`
Email string `gorm:"uniqueIndex;not null;size:100" json:"email"`
Status string `gorm:"not null;size:20;default:'active'" json:"status"`
// 关联关系示例(如果需要)
// Orders []Order `gorm:"foreignKey:UserID" json:"orders,omitempty"`
}
// TableName 指定表名(如果不使用默认命名)
func (User) TableName() string {
return "tb_user" // 遵循 NamingStrategy 的 TablePrefix
}
```
**命名规范**
- 字段名使用 PascalCaseGo 约定)
- 数据库列名自动转换为 snake_case
- 表名使用 `tb_` 前缀(可配置)
- JSON tag 使用 snake_case
#### 1.4 事务处理
```go
// internal/store/postgres/transaction.go
func (s *Store) Transaction(ctx context.Context, fn func(*Store) error) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 创建事务内的 Store 实例
txStore := &Store{db: tx, logger: s.logger}
return fn(txStore)
})
}
// 使用示例
err := store.Transaction(ctx, func(tx *Store) error {
if err := tx.User.Create(ctx, user); err != nil {
return err // 自动回滚
}
if err := tx.Order.Create(ctx, order); err != nil {
return err // 自动回滚
}
return nil // 自动提交
})
```
**事务最佳实践**
- 使用 `context.Context` 传递超时和取消信号
- 事务内操作尽可能快(< 50ms避免长事务锁表
- 事务失败自动回滚,无需手动处理
- 避免事务嵌套GORM 使用 SavePoint 处理嵌套事务)
---
## 2. 数据库迁移golang-migrate
### 决策:使用 golang-migrate 而非 GORM AutoMigrate
**理由**
- **版本控制**:迁移文件版本化,可追溯数据库 schema 变更历史
- **可回滚**:每个迁移包含 up/down 脚本,支持安全回滚
- **生产安全**:明确的 SQL 语句,避免 AutoMigrate 的意外变更
- **团队协作**:迁移文件可 code review减少数据库变更风险
- **符合 Constitution**:项目规范要求使用外部迁移工具
**GORM AutoMigrate 的问题**
- 无法回滚
- 无法删除列(只能添加和修改)
- 不支持复杂的 schema 变更(如重命名列)
- 生产环境风险高
### golang-migrate 使用指南
#### 2.1 安装
```bash
# macOS
brew install golang-migrate
# Linux
curl -L https://github.com/golang-migrate/migrate/releases/download/v4.15.2/migrate.linux-amd64.tar.gz | tar xvz
sudo mv migrate /usr/local/bin/
# Go install
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest
```
#### 2.2 创建迁移文件
```bash
# 创建新迁移
migrate create -ext sql -dir migrations -seq init_schema
# 生成文件:
# migrations/000001_init_schema.up.sql
# migrations/000001_init_schema.down.sql
```
#### 2.3 迁移文件示例
```sql
-- migrations/000001_init_schema.up.sql
CREATE TABLE IF NOT EXISTS tb_user (
id SERIAL PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(20) NOT NULL DEFAULT 'active'
);
CREATE INDEX idx_user_deleted_at ON tb_user(deleted_at);
CREATE INDEX idx_user_status ON tb_user(status);
-- migrations/000001_init_schema.down.sql
DROP TABLE IF EXISTS tb_user;
```
#### 2.4 执行迁移
```bash
# 向上迁移(应用所有未执行的迁移)
migrate -path migrations -database "postgresql://user:password@localhost:5432/dbname?sslmode=disable" up
# 回滚最后一次迁移
migrate -path migrations -database "postgresql://user:password@localhost:5432/dbname?sslmode=disable" down 1
# 迁移到指定版本
migrate -path migrations -database "postgresql://user:password@localhost:5432/dbname?sslmode=disable" goto 3
# 强制设置版本(修复脏迁移)
migrate -path migrations -database "postgresql://user:password@localhost:5432/dbname?sslmode=disable" force 2
```
#### 2.5 迁移脚本封装
```bash
#!/bin/bash
# scripts/migrate.sh
set -e
DB_USER=${DB_USER:-"postgres"}
DB_PASSWORD=${DB_PASSWORD:-"password"}
DB_HOST=${DB_HOST:-"localhost"}
DB_PORT=${DB_PORT:-"5432"}
DB_NAME=${DB_NAME:-"junhong_cmp"}
DATABASE_URL="postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable"
case "$1" in
up)
migrate -path migrations -database "$DATABASE_URL" up
;;
down)
migrate -path migrations -database "$DATABASE_URL" down ${2:-1}
;;
create)
migrate create -ext sql -dir migrations -seq "$2"
;;
version)
migrate -path migrations -database "$DATABASE_URL" version
;;
*)
echo "Usage: $0 {up|down [n]|create <name>|version}"
exit 1
esac
```
---
## 3. Asynq 任务队列
### 决策:选择 Asynq 作为异步任务队列
**理由**
- **Redis 原生支持**:基于 Redis无需额外中间件
- **功能完整**:支持任务重试、优先级、定时任务、唯一性约束
- **高性能**:支持并发处理,可配置 worker 数量
- **可观测性**:提供 Web UI 监控面板asynqmon
- **符合 Constitution**:项目技术栈要求使用 Asynq
**替代方案**
- **Machinery**:功能类似,但社区活跃度不如 Asynq
- **RabbitMQ + amqp091-go**:更重量级,需要额外部署 RabbitMQ
- **Kafka**:适合大规模流处理,对本项目过于复杂
### Asynq 架构设计
#### 3.1 Client任务提交
```go
// pkg/queue/client.go
import (
"github.com/hibiken/asynq"
"github.com/redis/go-redis/v9"
)
type Client struct {
client *asynq.Client
logger *zap.Logger
}
func NewClient(rdb *redis.Client, logger *zap.Logger) *Client {
return &Client{
client: asynq.NewClient(asynq.RedisClientOpt{Addr: rdb.Options().Addr}),
logger: logger,
}
}
func (c *Client) EnqueueTask(ctx context.Context, taskType string, payload []byte, opts ...asynq.Option) error {
task := asynq.NewTask(taskType, payload, opts...)
info, err := c.client.EnqueueContext(ctx, task)
if err != nil {
c.logger.Error("任务入队失败",
zap.String("task_type", taskType),
zap.Error(err))
return err
}
c.logger.Info("任务入队成功",
zap.String("task_id", info.ID),
zap.String("queue", info.Queue))
return nil
}
```
#### 3.2 Server任务处理
```go
// pkg/queue/server.go
func NewServer(rdb *redis.Client, cfg *config.QueueConfig, logger *zap.Logger) *asynq.Server {
return asynq.NewServer(
asynq.RedisClientOpt{Addr: rdb.Options().Addr},
asynq.Config{
Concurrency: cfg.Concurrency, // 并发数(默认 10
Queues: map[string]int{
"critical": 6, // 权重60%
"default": 3, // 权重30%
"low": 1, // 权重10%
},
ErrorHandler: asynq.ErrorHandlerFunc(func(ctx context.Context, task *asynq.Task, err error) {
logger.Error("任务执行失败",
zap.String("task_type", task.Type()),
zap.Error(err))
}),
Logger: &AsynqLogger{logger: logger}, // 自定义日志适配器
},
)
}
// cmd/worker/main.go
func main() {
// ... 初始化配置、日志、Redis
srv := queue.NewServer(rdb, cfg.Queue, logger)
mux := asynq.NewServeMux()
// 注册任务处理器
mux.HandleFunc(constants.TaskTypeEmailSend, task.HandleEmailSend)
mux.HandleFunc(constants.TaskTypeDataSync, task.HandleDataSync)
if err := srv.Run(mux); err != nil {
logger.Fatal("Worker 启动失败", zap.Error(err))
}
}
```
#### 3.3 任务处理器Handler
```go
// internal/task/email.go
func HandleEmailSend(ctx context.Context, t *asynq.Task) error {
var payload EmailPayload
if err := json.Unmarshal(t.Payload(), &payload); err != nil {
return fmt.Errorf("解析任务参数失败: %w", err)
}
// 幂等性检查(使用 Redis 或数据库)
key := constants.RedisTaskLockKey(payload.RequestID)
if exists, _ := rdb.Exists(ctx, key).Result(); exists > 0 {
logger.Info("任务已处理,跳过",
zap.String("request_id", payload.RequestID))
return nil // 返回 nil 表示成功,避免重试
}
// 执行任务
if err := sendEmail(ctx, payload); err != nil {
return fmt.Errorf("发送邮件失败: %w", err) // 返回错误触发重试
}
// 标记任务已完成(设置过期时间,避免内存泄漏)
rdb.SetEx(ctx, key, "1", 24*time.Hour)
logger.Info("邮件发送成功",
zap.String("to", payload.To),
zap.String("request_id", payload.RequestID))
return nil
}
```
### Asynq 配置建议
#### 3.4 重试策略
```go
// 默认重试策略:指数退避
task := asynq.NewTask(
constants.TaskTypeDataSync,
payload,
asynq.MaxRetry(5), // 最大重试 5 次
asynq.Timeout(10*time.Minute), // 任务超时 10 分钟
asynq.Queue("default"), // 队列名称
asynq.Retention(24*time.Hour), // 保留成功任务 24 小时
)
// 自定义重试延迟指数退避1s, 2s, 4s, 8s, 16s
asynq.RetryDelayFunc(func(n int, e error, t *asynq.Task) time.Duration {
return time.Duration(1<<uint(n)) * time.Second
})
```
#### 3.5 并发配置
| 场景 | 并发数 | 理由 |
|------|--------|------|
| CPU 密集型任务 | CPU 核心数 | 避免上下文切换开销 |
| I/O 密集型任务 | CPU 核心数 × 2 | 充分利用等待时间 |
| 混合任务 | 10默认 | 平衡性能和资源 |
**水平扩展**
- 启动多个 Worker 进程(不同机器或容器)
- 所有 Worker 连接同一个 Redis
- Asynq 自动负载均衡
#### 3.6 监控与调试
```bash
# 安装 asynqmonWeb UI
go install github.com/hibiken/asynqmon@latest
# 启动监控面板
asynqmon --redis-addr=localhost:6379
# 访问 http://localhost:8080
# 查看任务状态、队列统计、失败任务、重试历史
```
---
## 4. 幂等性设计
### 4.1 为什么需要幂等性?
**场景**
- 系统重启时Asynq 自动重新排队未完成的任务
- 任务执行失败后自动重试
- 网络抖动导致任务重复提交
**风险**
- 重复发送邮件/短信
- 重复扣款/充值
- 重复创建订单
### 4.2 幂等性实现模式
#### 模式 1唯一键去重推荐
```go
func HandleOrderCreate(ctx context.Context, t *asynq.Task) error {
var payload OrderPayload
json.Unmarshal(t.Payload(), &payload)
// 使用业务唯一键(如订单号)去重
key := constants.RedisTaskLockKey(payload.OrderID)
// SetNX仅当 key 不存在时设置
ok, err := rdb.SetNX(ctx, key, "1", 24*time.Hour).Result()
if err != nil {
return fmt.Errorf("Redis 操作失败: %w", err)
}
if !ok {
logger.Info("订单已创建,跳过",
zap.String("order_id", payload.OrderID))
return nil // 幂等返回
}
// 执行业务逻辑
if err := createOrder(ctx, payload); err != nil {
rdb.Del(ctx, key) // 失败时删除锁,允许重试
return err
}
return nil
}
```
#### 模式 2数据库唯一约束
```sql
CREATE TABLE tb_order (
id SERIAL PRIMARY KEY,
order_id VARCHAR(50) NOT NULL UNIQUE, -- 业务唯一键
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL
);
```
```go
func createOrder(ctx context.Context, payload OrderPayload) error {
order := &model.Order{
OrderID: payload.OrderID,
Status: constants.OrderStatusPending,
}
// GORM 插入,如果 order_id 重复则返回错误
if err := db.WithContext(ctx).Create(order).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
logger.Info("订单已存在,跳过", zap.String("order_id", payload.OrderID))
return nil // 幂等返回
}
return err
}
return nil
}
```
#### 模式 3状态机复杂业务
```go
func HandleOrderProcess(ctx context.Context, t *asynq.Task) error {
var payload OrderPayload
json.Unmarshal(t.Payload(), &payload)
// 加载订单
order, err := store.Order.GetByID(ctx, payload.OrderID)
if err != nil {
return err
}
// 状态检查:仅处理特定状态的订单
if order.Status != constants.OrderStatusPending {
logger.Info("订单状态不匹配,跳过",
zap.String("order_id", payload.OrderID),
zap.String("current_status", order.Status))
return nil // 幂等返回
}
// 状态转换
order.Status = constants.OrderStatusProcessing
if err := store.Order.Update(ctx, order); err != nil {
return err
}
// 执行业务逻辑
// ...
order.Status = constants.OrderStatusCompleted
return store.Order.Update(ctx, order)
}
```
---
## 5. 配置管理
### 5.1 数据库配置结构
```go
// pkg/config/config.go
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"` // 明文存储(按需求)
DBName string `mapstructure:"dbname"`
SSLMode string `mapstructure:"sslmode"`
MaxOpenConns int `mapstructure:"max_open_conns"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
}
type QueueConfig struct {
Concurrency int `mapstructure:"concurrency"`
Queues map[string]int `mapstructure:"queues"`
RetryMax int `mapstructure:"retry_max"`
Timeout time.Duration `mapstructure:"timeout"`
}
```
### 5.2 配置文件示例
```yaml
# configs/config.yaml
database:
host: localhost
port: 5432
user: postgres
password: password # 明文存储(生产环境建议使用环境变量)
dbname: junhong_cmp
sslmode: disable
max_open_conns: 25
max_idle_conns: 10
conn_max_lifetime: 5m
queue:
concurrency: 10
queues:
critical: 6
default: 3
low: 1
retry_max: 5
timeout: 10m
```
---
## 6. 性能优化建议
### 6.1 数据库查询优化
**索引策略**
- 为 WHERE、JOIN、ORDER BY 常用字段添加索引
- 复合索引按选择性从高到低排列
- 避免过多索引(影响写入性能)
```sql
-- 单列索引
CREATE INDEX idx_user_status ON tb_user(status);
-- 复合索引(状态 + 创建时间)
CREATE INDEX idx_user_status_created ON tb_user(status, created_at);
-- 部分索引(仅索引活跃用户)
CREATE INDEX idx_user_active ON tb_user(status) WHERE status = 'active';
```
**批量操作**
```go
// 避免 N+1 查询
// ❌ 错误
for _, orderID := range orderIDs {
order, _ := db.Where("id = ?", orderID).First(&Order{}).Error
}
// ✅ 正确
var orders []Order
db.Where("id IN ?", orderIDs).Find(&orders)
// 批量插入
db.CreateInBatches(users, 100) // 每批 100 条
```
### 6.2 慢查询监控
```go
// GORM 慢查询日志
db.Logger = logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 100 * time.Millisecond, // 慢查询阈值
LogLevel: logger.Warn,
IgnoreRecordNotFoundError: true,
Colorful: false,
},
)
```
---
## 7. 故障处理与恢复
### 7.1 数据库连接失败
**重试策略**
```go
func InitPostgresWithRetry(cfg *config.DatabaseConfig, logger *zap.Logger) (*gorm.DB, error) {
maxRetries := 5
retryDelay := 2 * time.Second
for i := 0; i < maxRetries; i++ {
db, err := InitPostgres(cfg, logger)
if err == nil {
return db, nil
}
logger.Warn("数据库连接失败,重试中",
zap.Int("attempt", i+1),
zap.Int("max_retries", maxRetries),
zap.Error(err))
time.Sleep(retryDelay)
retryDelay *= 2 // 指数退避
}
return nil, fmt.Errorf("数据库连接失败,已重试 %d 次", maxRetries)
}
```
### 7.2 任务队列故障恢复
**Redis 断线重连**
- Asynq 自动处理 Redis 断线重连
- Worker 重启后自动从 Redis 恢复未完成任务
**脏任务清理**
```bash
# 使用 asynqmon 手动清理死信队列
# 或编写定时任务自动归档失败任务
```
---
## 8. 测试策略
### 8.1 数据库集成测试
```go
// tests/integration/database_test.go
func TestUserCRUD(t *testing.T) {
// 使用 testcontainers 启动 PostgreSQL
ctx := context.Background()
postgresContainer, err := postgres.RunContainer(ctx,
testcontainers.WithImage("postgres:14"),
postgres.WithDatabase("test_db"),
postgres.WithUsername("postgres"),
postgres.WithPassword("password"),
)
require.NoError(t, err)
defer postgresContainer.Terminate(ctx)
// 连接测试数据库
connStr, _ := postgresContainer.ConnectionString(ctx)
db, _ := gorm.Open(postgres.Open(connStr), &gorm.Config{})
// 运行迁移
db.AutoMigrate(&model.User{})
// 测试 CRUD
user := &model.User{Username: "test", Email: "test@example.com"}
assert.NoError(t, db.Create(user).Error)
var found model.User
assert.NoError(t, db.Where("username = ?", "test").First(&found).Error)
assert.Equal(t, "test@example.com", found.Email)
}
```
### 8.2 任务队列测试
```go
// tests/integration/task_test.go
func TestEmailTask(t *testing.T) {
// 启动内存模式的 Asynq测试用
srv := asynq.NewServer(
asynq.RedisClientOpt{Addr: "localhost:6379"},
asynq.Config{Concurrency: 1},
)
mux := asynq.NewServeMux()
mux.HandleFunc(constants.TaskTypeEmailSend, task.HandleEmailSend)
// 提交任务
client := asynq.NewClient(asynq.RedisClientOpt{Addr: "localhost:6379"})
payload, _ := json.Marshal(EmailPayload{To: "test@example.com"})
client.Enqueue(asynq.NewTask(constants.TaskTypeEmailSend, payload))
// 启动 worker 处理
go srv.Run(mux)
time.Sleep(2 * time.Second)
// 验证任务已处理
// ...
}
```
---
## 9. 安全考虑
### 9.1 SQL 注入防护
**✅ GORM 自动防护**
```go
// GORM 使用预编译语句,自动转义参数
db.Where("username = ?", userInput).First(&user)
```
**❌ 避免原始 SQL**
```go
// 危险SQL 注入风险
db.Raw("SELECT * FROM users WHERE username = '" + userInput + "'").Scan(&user)
// 安全:使用参数化查询
db.Raw("SELECT * FROM users WHERE username = ?", userInput).Scan(&user)
```
### 9.2 密码存储
```yaml
# configs/config.yaml
database:
password: ${DB_PASSWORD} # 从环境变量读取(生产环境推荐)
```
```bash
# .env 文件(不提交到 Git
export DB_PASSWORD=secret_password
```
---
## 10. 部署与运维
### 10.1 健康检查
```go
// internal/handler/health.go
func (h *Handler) HealthCheck(c *fiber.Ctx) error {
health := map[string]string{
"status": "ok",
}
// 检查 PostgreSQL
sqlDB, _ := h.db.DB()
if err := sqlDB.Ping(); err != nil {
health["postgres"] = "down"
health["status"] = "degraded"
} else {
health["postgres"] = "up"
}
// 检查 Redis任务队列
if err := h.rdb.Ping(c.Context()).Err(); err != nil {
health["redis"] = "down"
health["status"] = "degraded"
} else {
health["redis"] = "up"
}
statusCode := fiber.StatusOK
if health["status"] != "ok" {
statusCode = fiber.StatusServiceUnavailable
}
return c.Status(statusCode).JSON(health)
}
```
### 10.2 优雅关闭
```go
// cmd/worker/main.go
func main() {
// ... 初始化
srv := queue.NewServer(rdb, cfg.Queue, logger)
// 处理信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-quit
logger.Info("收到关闭信号,开始优雅关闭")
// 停止接收新任务,等待现有任务完成(最多 30 秒)
srv.Shutdown()
}()
// 启动 Worker
if err := srv.Run(mux); err != nil {
logger.Fatal("Worker 运行失败", zap.Error(err))
}
}
```
---
## 总结
| 技术选型 | 关键决策 | 核心理由 |
|---------|----------|----------|
| **GORM** | 使用 GORM 而非 sqlx | 功能完整,符合项目技术栈 |
| **golang-migrate** | 使用外部迁移工具而非 AutoMigrate | 版本控制,可回滚,生产安全 |
| **Asynq** | 使用 Asynq 而非 Machinery | Redis 原生,功能完整,监控友好 |
| **连接池** | MaxOpenConns=25, MaxIdleConns=10 | 平衡性能和资源消耗 |
| **重试策略** | 最大 5 次,指数退避 | 避免雪崩,给系统恢复时间 |
| **幂等性** | Redis 去重 + 数据库唯一约束 | 防止重复执行,确保数据一致性 |
**下一步**Phase 1 设计与契约生成data-model.md、contracts/、quickstart.md