
PHP乐观锁与数据库事务结合扣除余额:问题分析与解决方案
本文探讨在PHP环境下,使用乐观锁和数据库事务进行余额扣除时,如何避免并发问题导致余额扣除失败或数据不一致的情况。 我们将分析错误代码,并提供正确的解决方案。
问题代码分析及错误原因:
以下代码片段试图通过乐观锁和事务保证并发环境下余额扣除的正确性,但存在缺陷:
立即学习“PHP免费学习笔记(深入)”;
错误代码片段一:
public function userbuy()
{
$user = $this->getuser();
$oldmoney = $user['balance'];
$orderoffer = $this->getordermoney($orderid);
if($oldmoney < $orderoffer['price']) $this->error('账户余额不足');
//乐观锁方案 (错误之处)
$newmoney = $oldmoney - $orderoffer['price'];
$newuser = smsuser::where(['id' => $user['id'],'balance' => $oldmoney])->find();
if(!$newuser) $this->error('用户不存在');
//开启数据库事务
db::transaction(function () use($newuser,$orderid,$newmoney){
$newuser->balance = $newmoney;
$result = $newuser->save();
if(!$result) $this->error('保存余额失败');
//创建订单 code
//扣除库存 code
//创建用户余额变动记录 code
db::commit(); //提交事务 (多余操作)
});
}
错误原因:
-
乐观锁位置错误:
find()方法在事务外部执行。多个并发请求都获取到相同的$oldmoney值。 只有第一个请求的save()操作成功,后续请求由于balance不再等于$oldmoney而失败。 乐观锁的条件判断应该在update语句中进行。 -
db::commit()多余:db::transaction()方法本身会自动提交事务,除非发生异常回滚。手动调用db::commit()冗余且可能掩盖错误。
错误代码片段二:
public function userbuy()
{
// ... (代码同上,省略部分) ...
//乐观锁方案 (错误之处)
$newUser->balance = $newMoney;
$result = $newUser->save();
if(!$result) $this->error('保存余额失败');
//开启数据库事务 (错误之处)
Db::transaction(function () use(){
//创建订单 code
//扣除库存 code
//创建用户余额变动记录 code
Db::commit(); //提交事务 (多余操作)
});
}
错误原因:
余额更新操作在事务外部执行。如果后续操作(创建订单、扣除库存等)失败,余额已经更新,导致数据不一致。
正确解决方案:
应该将乐观锁的条件判断放在数据库 update 语句中,并将所有需要保证原子性的操作都包含在同一个事务中。
正确代码:
public function userbuy()
{
$user = $this->getuser();
$oldmoney = $user['balance'];
$orderoffer = $this->getordermoney($orderid);
if ($oldmoney < $orderoffer['price']) $this->error('账户余额不足');
$newmoney = $oldmoney - $orderoffer['price'];
// 使用数据库的乐观锁机制 (例如MySQL)
$affectedRows = smsuser::where('id', $user['id'])
->where('balance', $oldmoney)
->update(['balance' => $newmoney]);
if ($affectedRows === 0) {
$this->error('余额更新失败,可能存在并发冲突'); // 乐观锁失败
return;
}
// 开启事务,包含所有后续操作
db::transaction(function () use ($orderid, $newmoney, $user) {
// 创建订单 code
// 扣除库存 code
// 创建用户余额变动记录 code (确保这些操作也包含在事务中)
});
}
这个解决方案将余额更新和后续操作都放在同一个事务中,并使用数据库提供的乐观锁机制来保证数据一致性。如果任何操作失败,事务会自动回滚,确保数据安全。 避免了手动提交事务,简化了代码并提高了可读性。 如果 update 语句影响的行数为0,则表示乐观锁失败,需要处理并发冲突。 可以使用重试机制或其他策略处理此类情况。 具体的乐观锁实现方式取决于使用的数据库系统。











