简体   繁体   English

如何进行交易管理

[英]How to do transaction Management

I have found a lot of golang tutorials on how to handle transactions, clean architecture, but none has satisfied my needs.我找到了很多关于如何处理事务、清理架构的 golang 教程,但没有一个能满足我的需求。 I have a go web application with a series of rest API each of one served by a three layers structure:我有一个go web 应用程序,其中包含一系列 rest ZDB974238714CA8DE634A7CE1D0 结构,每一个由 a8 结构提供

  • API API
    • Service服务
    • Dao
    • DB D B

This is the main:这是主要的:

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

Now, what I would realize is a transaction manager in the service layer.现在,我会意识到服务层中的事务管理器。 Something like this:像这样的东西:

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)
}

Dao should be like this:道应该是这样的:

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()
  }
}

This kind of POC has two problems:这种POC有两个问题:

  • fn func() error parameter passed in the transactionalExecute() method must use the dao.Tx variable of the dao instance transactionalExecute() 方法中传递的fn func() 错误参数必须使用 dao 实例的 dao.Tx 变量
  • This approach is not thread-safe: I am currently using gorilla mux and each http request will start a goroutine using a single instance of service, dao and DB.这种方法不是线程安全的:我目前正在使用 gorilla mux,每个 http 请求将使用单个服务实例、dao 和 DB 启动一个 goroutine。 These instances are shared between multiple threads and is not safe.这些实例在多个线程之间共享,并不安全。 Anyway I was thinking about the use of mutex to access to the dao.tx variable but I am concerned about performances.无论如何,我正在考虑使用互斥锁来访问 dao.tx 变量,但我担心性能。

Any suggestion?有什么建议吗? Or different approaches to the problem?还是解决问题的不同方法?

Transactions should not really be part of DAO layer, it's a completely separate concern.事务不应该真正成为 DAO 层的一部分,它是一个完全独立的关注点。 A transaction is simply a single connection that executes all statements atomically.事务只是以原子方式执行所有语句的单个连接。 DAO, on the other hand, is a collection of database functions which use connections/transactions.另一方面,DAO 是使用连接/事务的数据库函数的集合。

You can implement transactions and DAO nicely as two separate interfaces.您可以很好地将事务和 DAO 实现为两个独立的接口。 Transaction can be an interface that is implemented both by sql.DB (connection pool from the standard library) and sql.Tx (transaction).事务可以是由 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
}

We use both database/sql and sqlx extended library for easier data scanning, you can just drop sqlx functions if you do not need them.我们同时使用 database/sql 和 sqlx 扩展库来简化数据扫描,如果不需要,可以直接删除 sqlx 函数。 Then a helper function for easier work:然后是帮助器 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
}

Then you need a DAO interface, we have an initializer as a function that is then initialized by individual implementations, it takes context and transaction and returns DAO implementation:然后你需要一个 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)
}

Implementation looks something like this:实现看起来像这样:

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

And example use in a "service" package (without and with transaction):并在“服务”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
    }
}


This is incomplete code I did cut details.这是我删除细节的不完整代码。 Note I have both these interfaces in our dao package, these might be either separate or the same package.注意我在我们的 dao package 中有这两个接口,它们可能是单独的或相同的 package。

One important thing to know: you must not share DAO implementation just like that (as in your snippet).一件重要的事情要知道:你不能像那样共享 DAO 实现(就像在你的代码片段中一样)。 The standard library sql.DB interface is actually a connection pool and it is thread safe (as documented), you can call as many Query/Exec/Etc function as you want, the driver will use one or more connection as needed.标准库 sql.DB 接口实际上是一个连接池,它是线程安全的(如文档所述),您可以根据需要调用任意数量的 Query/Exec/Etc function,驱动程序将根据需要使用一个或多个连接。

Therefore the correct approach is to create new DAO type everytime a new request comes in, the connection pool will automatically acquire existing or new connection.因此正确的做法是,每次有新请求进来时,创建新的 DAO 类型,连接池会自动获取现有的或新的连接。 More about this in the Go documentation. Go 文档中有关此的更多信息。

you can modify the package dao to look like the following, which will make it thread-safe as func NewDao will be called in handler.您可以将 package dao 修改为如下所示,这将使其成为线程安全的,因为func NewDao将在处理程序中调用。 you make this work you need to pass db *sql.DB from package to handler.您完成这项工作需要将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