事务管理与循环依赖

Kratos 当中的数据流向是 server -> biz -> data

data 需要实现 biz 中定义的接口,利用注入的基础设施做 crud

但实际上我们需要在 biz层去开启事务的时候需要依赖于 data 层的 DB实例

如果在 data 层暴露一个 GetDB 的方法,看上去没什么问题,实际上会导致一个循环依赖的问题

对于这种情况,我们可以在 data 层去创建一个事务管理器,并在 biz 层去定义事务管理器需要实现的接口,这样抽离公共模块能保证代码的文件运行同时保证解耦。

代码相关实现

shortvid-service/internal/data/tx.go
package data

import (
	"shortvid-backend/app/shortvid-service/internal/biz"

	"github.com/go-kratos/kratos/v2/log"
	"gorm.io/gorm"
)

type txRepo struct {
	data *Data
}

func NewTxRepo(data *Data) biz.TxRepo {
	return &txRepo{
		data: data,
	}
}

func (r *txRepo) ExecFunc(fn func(tx *gorm.DB) error) error {
	err := r.data.db.Transaction(func(tx *gorm.DB) error {
		return fn(tx)
	})
	if err != nil {
		r.data.logger.Log(log.LevelError, "error", "transaction failed")
		return err
	}
	return nil
}

biz 层的接口

shortvid-service/internal/biz/tx.go
package biz

import "gorm.io/gorm"

// 事务聚合器
type TxRepo interface {
	ExecFunc(fn func(*gorm.DB) error) error
}

在真正用到事务的时候将TxRepo注入到 biz 的结构体当中

代码示例:

type UsersRepo interface {
	CreateUser(ctx context.Context, user *model.User) error
	CreateUserWithTx(ctx context.Context, tx *gorm.DB, user *model.User) error
	GetUserByEmailAndProvider(ctx context.Context, email string, provider string) (*model.User, error)
	GetUserByUID(ctx context.Context, UID int) (*model.User, error)
	UpdateLoginInfo(ctx context.Context, userUID int) error
}

type UsersUsecase struct {
	logger      log.Logger
	txRepo      TxRepo
	repo        UsersRepo
	accountRepo AccountRepo
}

// FirebaseFindOrCreateUser 查询或创建用户[Firebase]
func (uc *UsersUsecase) FirebaseFindOrCreateUser(ctx context.Context, dto *UserDTO) (*UserProfileVO, bool, error) {
	// 判断账户是否存在
	existAcc, err := uc.accountRepo.GetByEmailAndProvider(ctx, dto.Email, dto.Provider)
	if err != nil {
		uc.logger.Log(log.LevelError, "msg", "Get account by email and provider failed", "error", err)
		return nil, false, err
	}
	if existAcc != nil {
		// 账户存在, 继续查找用户
		existUser, err := uc.repo.GetUserByUID(ctx, existAcc.UID)
		if err != nil {
			uc.logger.Log(log.LevelError, "msg", "Get user by uid failed", "error", err)
			return nil, false, err
		}
		if existUser == nil {
			return nil, false, errors.New("account exists, but user not found")
		}

		return &UserProfileVO{
			ID:          existUser.ID,
			UID:         existUser.UID,
			Ctime:       existUser.CreatedAt.Unix(),
			Nickname:    existUser.Nickname,
			Avatar:      existUser.Avatar,
			Email:       existAcc.Email,
			Provider:    existAcc.Provider,
			ProviderUID: existAcc.ProviderUID,
		}, false, nil
	}

	// 创建账户和用户(强关联操作)
	user := &model.User{
		Nickname:    dto.Nickname,
		Avatar:      dto.Avatar,
		LastLoginAt: time.Now(),
	}
	account := &model.Account{
		Email:       dto.Email,
		Provider:    dto.Provider,
		ProviderUID: dto.ProviderUID,
	}
	err = uc.txRepo.ExecFunc(func(tx *gorm.DB) error {
		maxCount := 10
		for range maxCount {
			// 生成唯一的UID
			user.UID = uc.generateUniqueUserUID()
			err := uc.repo.CreateUserWithTx(ctx, tx, user)

			if err != nil {
				var mysqlErr *mysql.MySQLError
				if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
					uc.logger.Log(log.LevelInfo, "msg", "UserUID already exists, retrying", "userUID", user.UID)
					continue
				}
				uc.logger.Log(log.LevelError, "msg", "Create user failed", "error", err)
				return err
			}
			break
		}

		account.UID = user.UID
		err := uc.accountRepo.CreateAccountWithTx(ctx, tx, account)
		if err != nil {
			uc.logger.Log(log.LevelError, "msg", "Create account failed", "error", err)
			return err
		}

		return nil
	})

	if err != nil {
		uc.logger.Log(log.LevelError, "msg", "Create user and account failed", "error", err)
		return nil, false, err
	}

	return &UserProfileVO{
		ID:          user.ID,
		UID:         user.UID,
		Ctime:       user.CreatedAt.Unix(), // 创建时间
		Nickname:    user.Nickname,         // 昵称
		Avatar:      user.Avatar,           // 头像
		Email:       account.Email,         // 邮箱
		Provider:    account.Provider,      // 提供商
		ProviderUID: account.ProviderUID,   // 提供商UID
	}, true, nil
}