问题复现
客户端并发请求扣费接口,导致重复扣费;
服务端的加锁逻辑不正确,锁加到了非核心逻辑上,即加到了消费记录表,而不是加到了核心业务表。
有问题的代码
- 首次进入聊天房则扣费,再次进入不重复扣费
- 这个思路无法规避并发问题
public function agoraToken(Request $request)
{
.
.
.
try {
DB::connection('footprint')->beginTransaction();
if ($this->_userid == $appointmentInfo->userid) {
//如果已经存在则不重复扣费 但是并发问题会导致重复扣费
if (!AppointmentAction::existAction($this->_userid, $request->appointmentId, AppointmentAction::TYPE_ACTION_ENTER)) {
$propCount = UserConsume::getAppointmentSendConsumeCouponCount($this->_userid, $request->otherUserid);
$userConsume = new UserConsume($this->_userid);
Log::error("营业中约会 \n消费啤酒个数:" . $propCount . " \n用户id:" . $this->_userid . " \n对方id:" . $request->otherUserid . " \n时间戳:" . time());
$userConsume->consumeCommon(CouponInfo::PROP_COUPON_CHAMPAGNE_ID, PropInfo::PROP_CHAMPAGNE_ID, $propCount);
DB::connection('footprint')->commit();
}
}
//记录进入房间
AppointmentAction::record($this->_userid, $request->appointmentId, AppointmentAction::TYPE_ACTION_ENTER);
} catch (\Exception $exception) {
Log::error("扣费失败:" . $exception);
DB::connection('footprint')->rollBack();
}
.
.
.
}
public function agoraToken(Request $request)
{
.
.
.
//try catch 捕获异常,避免崩溃
try {
//开启事务 因为只是事务中的锁才生效 锁包括lockForUpdate()和sharedLock()
DB::connection('footprint')->beginTransaction();
//校验获取token的合法性
$appointmentInfo = AppointmentInfo::query()->selectRaw('id,userid,"inviteeUserid","prepareId",status,"isConsume"')
->where('id', $request->appointmentId)->lockForUpdate()->first();
if (empty($appointmentInfo) || !in_array($this->_userid, [$appointmentInfo->inviteeUserid, $appointmentInfo->userid])) {
return ErrorCode::TYPE_ILLEGAL_REQUEST;
}
//这里是关键 根据isConsume标记是否扣费了
if ($appointmentInfo->isConsume == AppointmentInfo::TYPE_IS_NOT_CONSUME) {
$propCount = UserConsume::getAppointmentSendConsumeCouponCount($this->_userid, $request->otherUserid);
$userConsume = new UserConsume($this->_userid);
Log::error("营业中约会,个数:" . $propCount . " 用户id:" . $this->_userid . ' 对方id:' . $request->otherUserid);
$userConsume->consumeCommon(CouponInfo::PROP_COUPON_CHAMPAGNE_ID, PropInfo::PROP_CHAMPAGNE_ID, $propCount);
//消费成功后修改isConsume的值,提交事务
$appointmentInfo->isConsume = AppointmentInfo::TYPE_IS_CONSUME;
$appointmentInfo->save();
DB::connection('footprint')->commit();
}
.
.
.
} catch (\Exception $exception) {
//异常情况打印log 回滚事务
Log::error("约会扣费异常:" . \GuzzleHttp\json_encode($exception));
DB::connection('footprint')->rollBack();
}
.
.
.
}
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
期间的尝试和思考
- 要给核心逻辑加锁,进入聊天房只是一种记录,而不是核心逻辑,不能作为锁定条件