简体   繁体   中英

How to avoid race conditions in GORM

I am developing a system to enable patient registration with incremental queue number. I am using Go, GORM, and MySQL.

An issue happens when more than one patients are registering at the same time, they tend to get the same queue number which it should not happen.

I attempted using transactions and hooks to achieve that but I still got duplicate queue number. I have not found any resource about how to lock the database when a transaction is happening.

func (r repository) CreatePatient(pat *model.Patient) error {
    tx := r.db.Begin()
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()

    err := tx.Error
    if err != nil {
        return err
    }

    

    // 1. get latest queue number and assign it to patient object
    var queueNum int64
    err = tx.Model(&model.Patient{}).Where("registration_id", pat.RegistrationID).Select("queue_number").Order("created_at desc").First(&queueNum).Error

    if err != nil && err != gorm.ErrRecordNotFound {
        tx.Rollback()
        return err
    }
    pat.QueueNumber = queueNum + 1

    // 2. write patient data into the db
    err = tx.Create(pat).Error
    if err != nil {
        tx.Rollback()
        return err
    }

    return tx.Commit().Error
}

As stated by @O. Jones, transactions don't save you here because you're extracting the largest value of a column, incrementing it outside the db and then saving that new value. From the database's point of view the updated value has no dependence on the queried value.

You could try doing the update in a single query, which would make the dependence obvious:

UPDATE patient AS p
JOIN (
  SELECT max(queue_number) AS queue_number FROM patient WHERE registration_id = ?
) maxp
SET p.queue_number = maxp.queue_number + 1 
WHERE id = ?

In gorm you can't run a complex update like this, so you'll need to make use of Exec .

I'm not 100% certain the above will work because I'm less familiar with MySQL transaction isolation guarantees.

A cleaner way

Overall, it'd be cleaner to keep a table of queues (by reference_id) with a counter that you update atomically:

Start a transaction, then

SELECT queue_number FROM queues WHERE registration_id = ? FOR UPDATE;

Increment the queue number in your app code, then

UPDATE queues SET queue_number = ? WHERE registration_id = ?;

Now you can use the incremented queue number in your patient creation/update before transaction commit.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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