TRANDANH's avatar

Process money at the same time when making payment

Hello everyone,

I have an issue with my system:

Account 1 initially has a balance of $100.

Person A makes a payment to add $300 to Account 1 at 8:21:21.

Simultaneously, Person B makes a payment to deduct $10 from Account 1 at the same timestamp (8:21:21).

After these transactions, Account 1 shows a balance of $90.

I need assistance in understanding what happened and how to fix it using Laravel/MySQL.

Thank you, everyone.

0 likes
2 replies
nexxai's avatar

You're not using DB locking, so the second query read the original data and then just overwrote the "$400" with the "$90". This is a very simple race condition.

When doing transactions like this, you should be doing pessimistic locking as described here with the ->lockForUpdate() method and using DB::transaction()s to make sure that only one of these updates happens at a time.

I would also suggest you look into double entry accounting to ensure that even if your code somehow allows another one of these race conditions that you have an audit log that you can refer back to, to balance your books.

Merklin's avatar

@nexxai I am new to laravel and I admit I didn"t know about this lock function. so I have a few questions:

  1. If I use lockForUpdate() when getting the data to populate the update form, is it auto-unlocked when later the Model->update() is fired or it should be done mnually?

  2. Also, isn't using the transactions used by default in Laravel 10?

My edit/update is like this:

public function commonEditData(string $modelName, int $recordId): void
    {

        $this->useCachedRows();

        try {
            $modelClass = 'App\Models\' . $modelName;
            $object = $modelClass::findOrFail($recordId);
            if ($object) {
                $this->id = $object->id;
                $this->setDataFromObject(ucfirst($modelName), $object);
            } else {
                $this->dispatch('warning');
                $this->cancelAction();
            }
        } catch (Exception $exc) {
            Log::error($exc->getMessage() . ' for ' . __CLASS__ . '::' . __FUNCTION__);
            $this->dispatch('danger');
        }
    }

public function commonUpdateData(string $modelName): void
    {

        if (!App::runningUnitTests()) {
            $this->validate();
        }

        try {
            $modelClass = 'App\Models\' . $modelName;
            $object = $modelClass::findOrFail($this->id);
            $update = $object->update($this->getData(ucfirst($modelName)));
            $this->dispatch('updated');

            if (($modelName === 'Task' || $modelName === 'Project') && $update && !App::runningUnitTests()) {
                $this->handleProjectUpdates($object->project_id);
            }
        } catch (Exception $exc) {
            Log::error($exc->getMessage() . ' for ' . __CLASS__ . '::' . __FUNCTION__);
            $this->dispatch('danger');
        }

        $this->cancelAction();
    }

Can I use lockForUpdate() in the commonEditData()? And then DB::transaction() and DB::commit() in commonUpdateData() or it is done automatically by Laravel?

Please or to participate in this conversation.