Contents

river

Contents

River

简单来说,River 是一个专为 Go 设计的作业队列库,但它不是那种「又一个轮子」的存在。它最大的特点是什么?把队列和 PostgreSQL深度绑定。

传统方案里,我们通常会用 Redis、RabbitMQ 这些专门的消息中间件来做队列。这没问题,但也带来了一个老大难问题:分布式事物

想象一个场景:你在处理订单支付,需要先更新数据库的订单状态,再往队列里扔一个「发送支付成功短信」的任务。如果数据库更新成功了,但往队列里塞任务失败了呢?或者反过来?这就是分布式事务的经典困境。

River 的解决方案很直接:既然都用 PostgreSQL 存数据,那队列也放 PostgreSQL 里不就完了? 一个事务搞定所有事情,要么全成功,要么全回滚。是不是有种「大道至简」的感觉?

package main

import (
	"context"
	"database/sql"
	"errors"
	"fmt"
	"log"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/riverqueue/river"
	"github.com/riverqueue/river/riverdriver/riverdatabasesql"
	"github.com/riverqueue/river/rivermigrate"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

const (
	OrderStatusPending    = "pending"
	OrderStatusProcessing = "processing"
	OrderStatusCompleted  = "completed"

	OrderQueue = "order"
)

// Order 是业务表,由 GORM 管理。
type Order struct {
	ID        int64     `gorm:"primaryKey;autoIncrement"`
	OrderNo   string    `gorm:"size:64;not null;uniqueIndex"`
	Product   string    `gorm:"size:128;not null"`
	Quantity  int       `gorm:"not null"`
	Status    string    `gorm:"size:32;not null;index"`
	CreatedAt time.Time `gorm:"not null"`
	UpdatedAt time.Time `gorm:"not null"`
}

func (Order) TableName() string {
	return "orders"
}

// ProcessOrderArgs 是 River 任务参数。
// River 会将其序列化成 JSON 存入 river_job 表。
type ProcessOrderArgs struct {
	OrderID int64 `json:"order_id"`
}

// Kind 必须保持稳定,不能随意修改。
// River 根据 Kind 找到对应的 Worker。
func (ProcessOrderArgs) Kind() string {
	return "process_order"
}

// InsertOpts 设置任务默认参数。
func (ProcessOrderArgs) InsertOpts() river.InsertOpts {
	return river.InsertOpts{
		Queue:       OrderQueue,
		MaxAttempts: 5,
	}
}

// ProcessOrderWorker 负责处理订单任务。
type ProcessOrderWorker struct {
	river.WorkerDefaults[ProcessOrderArgs]

	db *gorm.DB
}

func NewProcessOrderWorker(db *gorm.DB) *ProcessOrderWorker {
	return &ProcessOrderWorker{
		db: db,
	}
}

// Work 执行具体任务。
// 返回 nil 表示任务成功;返回 error 表示任务失败并触发重试。
func (w *ProcessOrderWorker) Work(
	ctx context.Context,
	job *river.Job[ProcessOrderArgs],
) error {
	orderID := job.Args.OrderID

	log.Printf(
		"开始处理订单任务: job_id=%d order_id=%d attempt=%d",
		job.ID,
		orderID,
		job.Attempt,
	)

	var order Order
	err := w.db.WithContext(ctx).
		First(&order, orderID).
		Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			// 订单已经不存在,继续重试通常没有意义。
			// 返回 nil,将任务标记为完成。
			log.Printf("订单不存在,忽略任务: order_id=%d", orderID)
			return nil
		}

		return fmt.Errorf("查询订单失败: %w", err)
	}

	// Worker 必须尽量保证幂等。
	// River 任务可能因为进程退出、网络异常等原因被再次执行。
	if order.Status == OrderStatusCompleted {
		log.Printf("订单已经完成,跳过重复处理: order_id=%d", orderID)
		return nil
	}

	result := w.db.WithContext(ctx).
		Model(&Order{}).
		Where(
			"id = ? AND status != ?",
			orderID,
			OrderStatusCompleted,
		).
		Update("status", OrderStatusProcessing)
	if result.Error != nil {
		return fmt.Errorf("更新订单为处理中失败: %w", result.Error)
	}

	// 模拟耗时业务,并响应任务取消。
	select {
	case <-time.After(2 * time.Second):
	case <-ctx.Done():
		return ctx.Err()
	}

	result = w.db.WithContext(ctx).
		Model(&Order{}).
		Where("id = ?", orderID).
		Update("status", OrderStatusCompleted)
	if result.Error != nil {
		return fmt.Errorf("更新订单为已完成失败: %w", result.Error)
	}

	log.Printf(
		"订单处理完成: job_id=%d order_id=%d",
		job.ID,
		orderID,
	)

	return nil
}

// CreateOrderAndEnqueue 在同一个事务中:
// 1. 使用 GORM 创建订单;
// 2. 使用 River 创建异步任务;
// 3. 一起提交或一起回滚。
func CreateOrderAndEnqueue(
	ctx context.Context,
	db *gorm.DB,
	riverClient *river.Client[*sql.Tx],
	product string,
	quantity int,
) (*Order, error) {
	tx := db.WithContext(ctx).Begin()
	if tx.Error != nil {
		return nil, fmt.Errorf("开启事务失败: %w", tx.Error)
	}

	committed := false

	defer func() {
		if !committed {
			_ = tx.Rollback().Error
		}
	}()

	order := &Order{
		OrderNo:  fmt.Sprintf("ORD-%d", time.Now().UnixNano()),
		Product:  product,
		Quantity: quantity,
		Status:   OrderStatusPending,
	}

	if err := tx.Create(order).Error; err != nil {
		return nil, fmt.Errorf("创建订单失败: %w", err)
	}

	// GORM 开启事务后,底层连接是 *sql.Tx。
	// 将同一个 *sql.Tx 交给 River,实现事务性投递任务。
	sqlTx, ok := tx.Statement.ConnPool.(*sql.Tx)
	if !ok {
		return nil, errors.New("无法从 GORM 事务获取 *sql.Tx")
	}

	insertResult, err := riverClient.InsertTx(
		ctx,
		sqlTx,
		ProcessOrderArgs{
			OrderID: order.ID,
		},
		nil,
	)
	if err != nil {
		return nil, fmt.Errorf("创建 River 任务失败: %w", err)
	}

	if err := tx.Commit().Error; err != nil {
		return nil, fmt.Errorf("提交事务失败: %w", err)
	}

	committed = true

	log.Printf(
		"订单及任务创建成功: order_id=%d job_id=%d",
		order.ID,
		insertResult.Job.ID,
	)

	return order, nil
}
func migrateRiver(ctx context.Context, sqlDB *sql.DB) error {
	migrator, err := rivermigrate.New(
		riverdatabasesql.New(sqlDB),
		nil,
	)
	if err != nil {
		return fmt.Errorf("创建 River Migrator 失败: %w", err)
	}

	result, err := migrator.Migrate(
		ctx,
		rivermigrate.DirectionUp,
		nil,
	)
	if err != nil {
		return fmt.Errorf("执行 River 数据库迁移失败: %w", err)
	}

	if len(result.Versions) == 0 {
		log.Println("River 数据库结构已经是最新版本")
		return nil
	}

	for _, version := range result.Versions {
		log.Printf(
			"River 数据库迁移完成: version=%d name=%s duration=%s",
			version.Version,
			version.Name,
			version.Duration,
		)
	}

	return nil
}

func openDatabase(
	ctx context.Context,
	databaseURL string,
) (sqlDB *sql.DB, gormDB *gorm.DB, err error) {

	// sqlDB.SetMaxOpenConns(30)
	// sqlDB.SetMaxIdleConns(10)
	// sqlDB.SetConnMaxLifetime(30 * time.Minute)
	// sqlDB.SetConnMaxIdleTime(10 * time.Minute)

	// if err := sqlDB.PingContext(ctx); err != nil {
	// 	_ = sqlDB.Close()
	// 	return nil, nil, fmt.Errorf("连接数据库失败: %w", err)
	// }

	gormDB, err = gorm.Open(
		postgres.Open(databaseURL),
		&gorm.Config{
			Logger: logger.Default.LogMode(logger.Info),
		},
	)
	if err != nil {
		return
	}
	sqlDB, err = gormDB.DB()
	if err != nil {
		return
	}
	sqlDB.SetMaxIdleConns(10)
	sqlDB.SetMaxOpenConns(50)
	sqlDB.SetConnMaxLifetime(time.Hour)

	if err := sqlDB.PingContext(ctx); err != nil {
		_ = sqlDB.Close()
		return nil, nil, fmt.Errorf("连接数据库失败: %w", err)
	}

	return sqlDB, gormDB, nil
}

func main() {
	databaseURL := os.Getenv("DATABASE_URL")
	if databaseURL == "" {
		databaseURL = "postgres://kk:kkm@localhost:5432/river_demo?sslmode=disable"
	}

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sqlDB, gormDB, err := openDatabase(ctx, databaseURL)
	if err != nil {
		log.Fatal(err)
	}
	defer sqlDB.Close()

	// 必须先创建和升级 River 所需的数据库表。
	if err := migrateRiver(ctx, sqlDB); err != nil {
		log.Fatalf("River 数据库迁移失败: %v", err)
	}

	// 这里只迁移业务表。
	// River 自己的表应通过 river migrate-up 创建。
	if err := gormDB.AutoMigrate(&Order{}); err != nil {
		log.Fatalf("迁移业务表失败: %v", err)
	}

	workers := river.NewWorkers()

	// 注册任务 Worker。
	river.AddWorker(
		workers,
		NewProcessOrderWorker(gormDB),
	)

	// riverdatabasesql 允许 River 与 GORM 共用 sql.DB/sql.Tx。
	riverClient, err := river.NewClient(
		riverdatabasesql.New(sqlDB),
		&river.Config{
			Queues: map[string]river.QueueConfig{
				OrderQueue: {
					MaxWorkers: 5,
				},
			},
			Workers:         workers,
			SoftStopTimeout: 15 * time.Second,
		},
	)
	if err != nil {
		log.Fatalf("创建 River Client 失败: %v", err)
	}

	appCtx, stop := signal.NotifyContext(
		ctx,
		syscall.SIGINT,
		syscall.SIGTERM,
	)
	defer stop()

	// 启动 River Worker。
	if err := riverClient.Start(appCtx); err != nil {
		log.Fatalf("启动 River Client 失败: %v", err)
	}

	log.Println("River Worker 已启动")

	// 创建一条示例订单,并在同一事务中投递任务。
	order, err := CreateOrderAndEnqueue(
		appCtx,
		gormDB,
		riverClient,
		"MacBook Pro",
		1,
	)
	if err != nil {
		log.Fatalf("创建示例订单失败: %v", err)
	}

	log.Printf(
		"示例订单已创建: id=%d order_no=%s status=%s",
		order.ID,
		order.OrderNo,
		order.Status,
	)

	// 等待 SIGINT 或 SIGTERM。
	<-appCtx.Done()

	log.Println("正在停止 River Worker")

	// Start 使用的 context 被取消后,River 会进入优雅停止流程。
	<-riverClient.Stopped()

	log.Println("River Worker 已停止")
}