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(); //提交事务
})
}
我用上面这种组织代码,我测试同时创建请求5个订单,同时买5个商品,于是出现一个问题,就扣了用户1个订单的钱。比如单价,1块。买5个也就是花5块钱,总余额买之前是100.最后扣了5块钱,总余额就变成95元,现在是99元。明显不对。
于是我又开始折腾,改了代码,是这样。
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('用户不存在');
$newUser->balance = $newMoney;
$result = $newUser->save();
if(!$result) $this->error('保存余额失败');
//开启数据库事务
Db::transaction(function () use(){
//创建订单 code
//扣除库存 code
//创建用户余额变动记录 code
Db::commit(); //提交事务
})
}
就是可以正常扣5单的钱了,现在还搞不清什么原因,谁能告诉我什么原因吗?
代码我用的thinkphp6,用的。
把扣除余额的代码,拎出来,就可以了。但是这种又引发了另一个问题,数据一致性不能统一,如果后面的代码,创建订单,扣库存,===一系列操作有一个发生异常,他这个余额就不能回滚了。
先说结论:你的使用方式不对。
这里主要提一个点,就是事务四大特性之一的「隔离性」。
只是你前一段代码中,实际执行的过程。
存在的问题
事务范围错误:,当我们在使用事务,事务所涉及的数据,都应该放到事务中去,让 MySQL 把这一部分数据隔离起来,把这些查询当成一个主体。
乐观锁使用错误,你说你想使用乐观锁,实际上并没有使用到,这是为什么?因为
find
查询的存在,find
查询实际上是会执行一个select
语句把对应的数据查询出来,这时候拿到了主键 ID,然后你在下面调用save
时,再把新金额写入。乐观锁的预期 SQL 应该是这样的
即只有再 balance 为 100 时,才更新 balance 为 99,而实际上,你这里的 SQL 最终结果应该是这样的:
实际上不应该这样去使用。
Db::transaction 启动的事务不需要手动的调用
Db::commit()
,因为 transaction 内部会自动调用 commit,不需要你手动 commit, transaction 默认你的操作都是可以 commit 的,如果你需要 rollback ,那你只需要在代码中抛出 (throw) 一个异常即可,transaction 内部会自动调用 rollback ,并且继续把异常抛出来。看完第一个问题,再来看第二个问题。
为什么第二种方式又可以了呢?因为「隔离」了。
当然,其实如果你只是在第一个的基础上,把
$newUser = SmsUser::where(['id' => $user['id'],'balance' => $oldMoney])->find();
这一句放到事务里也就可以了,但是在大量请求的时候,这种用法或许会出问题。当然,你这里的“乐观锁”这个使用本身就是错误的。