PHP乐观锁和事务结合扣除余额失败:如何避免只扣款一次?(余额.扣除.乐观.失败.事务...)
本文分析了在thinkphp6框架下,使用乐观锁和数据库事务机制并发扣除用户余额时,出现余额扣除不准确或数据不一致的问题,并提供解决方案。问题表现为:有时只扣除一次余额,有时虽然扣除多次,但数据最终不一致。
问题分析:
文中提到的两种方案都存在缺陷:
-
方案一: 乐观锁的验证(SmsUser::where(['id' => $user['id'],'balance' => $oldMoney])->find();)在事务外进行,导致$oldMoney可能在事务开始前已被其他请求修改,乐观锁失效,只扣除一次余额。此外,事务范围过小,只包含余额更新,其他操作(创建订单、扣除库存、记录余额变动)在事务外执行,缺乏原子性保证。
-
方案二: 将余额更新移到事务外,避免了方案一中的乐观锁失效问题。但是,如果事务内任何操作失败,余额已扣除,但其他操作回滚,造成数据不一致。
根本原因及正确解决方案:
问题的核心在于乐观锁和事务的错误使用。正确的做法是:
-
数据库层面实现乐观锁: 不要在代码层面模拟乐观锁,而是利用数据库提供的机制。例如,使用UPDATE sms_user SET balance = newMoney WHERE id = userId AND balance = oldMoney;。这保证了只有当余额与预期一致时才进行更新。
-
将所有相关操作包含在一个事务中: 余额扣除、订单创建、库存扣除、余额变动记录创建等所有相关操作必须在一个事务内执行,确保原子性。事务的成功或失败将影响所有操作,保证数据一致性。
-
避免手动提交事务: 依赖框架自动管理事务的提交或回滚。如果事务内发生异常,框架会自动回滚,避免数据不一致。
改进后的代码示例 (伪代码):
Db::startTrans(); // 开启事务 try { // 1. 获取用户信息和订单价格 (可能需要加锁,防止读取脏数据) $user = SmsUser::lockForUpdate()->find($userId); // 使用数据库行锁 $orderPrice = ...; // 2. 验证余额是否充足 if ($user->balance < $orderPrice) { throw new Exception('余额不足'); } // 3. 更新余额,使用数据库乐观锁 $affectedRows = Db::table('sms_user') ->where('id', $userId) ->where('balance', $user->balance) // 乐观锁条件 ->update(['balance' => $user->balance - $orderPrice]); if ($affectedRows === 0) { throw new Exception('乐观锁失败,余额已被修改'); } // 4. 创建订单、扣除库存、创建余额变动记录 // ... Db::commit(); // 提交事务 } catch (Exception $e) { Db::rollback(); // 回滚事务 // 处理异常 }
通过以上改进,可以有效避免只扣款一次以及数据不一致的问题,确保数据安全和业务逻辑的正确性。 记住选择合适的数据库锁机制,例如行锁或表锁,以满足并发控制的需求。 如果并发量极高,可能需要考虑更高级的分布式锁机制。
以上就是PHP乐观锁和事务结合扣除余额失败:如何避免只扣款一次?的详细内容,更多请关注知识资源分享宝库其它相关文章!