繁体   English   中英

如何进行交易管理

[英]How to do transaction Management

我找到了很多关于如何处理事务、清理架构的 golang 教程,但没有一个能满足我的需求。 我有一个go web 应用程序,其中包含一系列 rest ZDB974238714CA8DE634A7CE1D0 结构,每一个由 a8 结构提供

  • API
    • 服务
    • D B

这是主要的:

func main() {
  DB = db.New()
  dao = dao.New(DB)
  service := service.New(dao)
  server := &http.Server(....)
  server.ListenAndServe()
}

现在,我会意识到服务层中的事务管理器。 像这样的东西:

type Service interface {
  TransactionalExecute(fn func() error)
}

type ServiceImpl struct {
  dao DaoImpl
}

func (service *ServiceImpl) TransactionalExecute(fn func() error) {
  service.dao.TxBegin()
  err := fn()
  service.dao.TxEnd(err)
}

道应该是这样的:

type Dao interface {
  TxBegin()
  TxEnd(err error)
}

type DaoImpl struct {
  DB db
  tx *sql.Tx
}

func (dao *DaoImpl) TxBegin() {
  dao.tx = dao.db.Begin()
}

func (dao *DaoImpl) TxEnd(err error) {
if p:= recover(); p != nil {
     dao.tx.Rollback()
     panic(p)
  } else if err != nil {
     dao.tx.Rollback()
  } else {
     dao.tx.Commit()
  }
}

这种POC有两个问题:

  • transactionalExecute() 方法中传递的fn func() 错误参数必须使用 dao 实例的 dao.Tx 变量
  • 这种方法不是线程安全的:我目前正在使用 gorilla mux,每个 http 请求将使用单个服务实例、dao 和 DB 启动一个 goroutine。 这些实例在多个线程之间共享,并不安全。 无论如何,我正在考虑使用互斥锁来访问 dao.tx 变量,但我担心性能。

有什么建议吗? 还是解决问题的不同方法?

事务不应该真正成为 DAO 层的一部分,它是一个完全独立的关注点。 事务只是以原子方式执行所有语句的单个连接。 另一方面,DAO 是使用连接/事务的数据库函数的集合。

您可以很好地将事务和 DAO 实现为两个独立的接口。 事务可以是由 sql.DB(标准库中的连接池)和 sql.Tx(事务)实现的接口。

// Transaction represents both database connection pool and a single transaction/connection
type Transaction interface {
    // ExecContext from database/sql library
    ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
    // PrepareContext from database/sql library
    PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
    // QueryContext from database/sql library
    QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
    // QueryRowContext from database/sql library
    QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row

    // QueryxContext from sqlx library
    QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
    // QueryRowxContext from sqlx library
    QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row
    // PreparexContext from sqlx library
    PreparexContext(ctx context.Context, query string) (*sqlx.Stmt, error)
    // GetContext from sqlx library
    GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
    // SelectContext from sqlx library
    SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
}

我们同时使用 database/sql 和 sqlx 扩展库来简化数据扫描,如果不需要,可以直接删除 sqlx 函数。 然后是帮助器 function 以便于工作:

// A TxFn is a function that will be called with an initialized `Transaction` object
// that can be used for executing statements and queries against a database.
type TxFn func(Transaction) error

// WithTransaction creates a new transaction and handles rollback/commit based on the
// error object returned by the `TxFn` or when it panics.
func WithTransaction(ctx context.Context, db *sqlx.DB, fn TxFn) error {
    logger := ctxval.GetLogger(ctx)
    tx, err := db.BeginTxx(ctx, &sql.TxOptions{
        Isolation: sql.LevelDefault,
        ReadOnly:  false,
    })
    if err != nil {
        logger.Error().Err(err).Msg("Cannot begin database transaction")
        return fmt.Errorf("cannot begin transaction: %w", err)
    }

    defer func() {
        if p := recover(); p != nil {
            logger.Trace().Msg("Rolling database transaction back")
            err := tx.Rollback()
            if err != nil {
                logger.Error().Err(err).Msg("Cannot rollback database transaction")
                return
            }
            panic(p)
        } else if err != nil {
            logger.Trace().Msg("Rolling database transaction back")
            err := tx.Rollback()
            if err != nil {
                logger.Error().Err(err).Msg("Cannot rollback database transaction")
                return
            }
        } else {
            logger.Trace().Msg("Committing database transaction")
            err = tx.Commit()
            if err != nil {
                logger.Error().Err(err).Msg("Cannot rollback database transaction")
                return
            }
        }
    }()

    logger.Trace().Msg("Starting database transaction")
    err = fn(tx)
    return err
}

然后你需要一个 DAO 接口,我们有一个作为 function 的初始化器,然后由各个实现初始化,它接受上下文和事务并返回 DAO 实现:

var GetAccountDao func(ctx context.Context, tx Transaction) (AccountDao, error)

type AccountDao interface {
    GetById(ctx context.Context, id uint64) (*models.Account, error)
    GetByAccountNumber(ctx context.Context, number string) (*models.Account, error)
    GetByOrgId(ctx context.Context, orgId string) (*models.Account, error)
    List(ctx context.Context, limit, offset uint64) ([]*models.Account, error)
}

实现看起来像这样:

const (
    getAccountById            = `SELECT * FROM accounts WHERE id = $1 LIMIT 1`
)


type accountDaoSqlx struct {
    name               string
    getById            *sqlx.Stmt
}

func getAccountDao(ctx context.Context, tx dao.Transaction) (dao.AccountDao, error) {
    var err error
    daoImpl := accountDaoSqlx{}
    daoImpl.name = "account"

    daoImpl.getById, err = tx.PreparexContext(ctx, getAccountById)
    if err != nil {
        return nil, NewPrepareStatementError(ctx, &daoImpl, getAccountById, err)
    }

    return &daoImpl, nil
}

func init() {
    dao.GetAccountDao = getAccountDao
}

func (dao *accountDaoSqlx) GetById(ctx context.Context, id uint64) (*models.Account, error) {
    query := getAccountById
    stmt := dao.getById
    result := &models.Account{}

    err := stmt.GetContext(ctx, result, id)
    if err != nil {
        return nil, NewGetError(ctx, dao, query, err)
    }
    return result, nil
}

// etc

并在“服务”package 中使用示例(没有和有事务):


// import sqlx implementation which calls init()
_ "internal/dao/sqlx"
"internal/dao"
// etc

func CreatePubkey(w http.ResponseWriter, r *http.Request) {
    payload := &payloads.PubkeyRequest{}
    if err := render.Bind(r, payload); err != nil {
        renderError(w, r, payloads.NewInvalidRequestError(r.Context(), err))
        return
    }

    // example with transaction
    err := dao.WithTransaction(r.Context(), db.DB, func(tx dao.Transaction) error {
        pkDao, err := dao.GetPubkeyDao(r.Context(), tx)
        if err != nil {
            return payloads.NewInitializeDAOError(r.Context(), "pubkey DAO", err)
        }

        err = pkDao.Create(r.Context(), payload.Pubkey)
        if err != nil {
            return payloads.NewDAOError(r.Context(), "create pubkey", err)
        }

        // ... cut ...

        return nil
    })
    // ignore errors etc
}

func ListPubkeys(w http.ResponseWriter, r *http.Request) {
    // example without transaction
    pubkeyDao, err := dao.GetPubkeyDao(r.Context(), db.DB)
    if err != nil {
        renderError(w, r, payloads.NewInitializeDAOError(r.Context(), "pubkey DAO", err))
        return
    }

    pubkeys, err := pubkeyDao.List(r.Context(), 100, 0)
    if err != nil {
        renderError(w, r, payloads.NewDAOError(r.Context(), "list pubkeys", err))
        return
    }

    if err := render.RenderList(w, r, payloads.NewPubkeyListResponse(pubkeys)); err != nil {
        renderError(w, r, payloads.NewRenderError(r.Context(), "list pubkeys", err))
        return
    }
}


这是我删除细节的不完整代码。 注意我在我们的 dao package 中有这两个接口,它们可能是单独的或相同的 package。

一件重要的事情要知道:你不能像那样共享 DAO 实现(就像在你的代码片段中一样)。 标准库 sql.DB 接口实际上是一个连接池,它是线程安全的(如文档所述),您可以根据需要调用任意数量的 Query/Exec/Etc function,驱动程序将根据需要使用一个或多个连接。

因此正确的做法是,每次有新请求进来时,创建新的 DAO 类型,连接池会自动获取现有的或新的连接。 Go 文档中有关此的更多信息。

您可以将 package dao 修改为如下所示,这将使其成为线程安全的,因为func NewDao将在处理程序中调用。 您完成这项工作需要将db *sql.DB从 package 传递给处理程序。

type Dao interface {
    TxEnd(err error)
}

type DaoImpl struct {
    tx       *sql.Tx
    db       *sql.DB
    txStared bool
}

func NewDao(db *sql.DB, txStared bool) Dao {
    if txStared {
        tx, _ := db.Begin()
        return &DaoImpl{tx: tx, db: db, txStared: txStared}
    }

    return &DaoImpl{tx: nil, db: db, txStared: txStared}
}

func (dao *DaoImpl) TxEnd(err error) {
    
    if !dao.txStared  {
        return
    }
    
    if p:= recover(); p != nil {
        dao.tx.Rollback()
        panic(p)
    } else if err != nil {
        dao.tx.Rollback()
    } else {
        dao.tx.Commit()
    }
}
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM