thinkphp3.2.3高并发锁是使用方法
案例,问题太抽象,请仔细阅读。
记录一个查询多次扣款的sql查询语句:
select *,count(`desc`) as count ,(select from_unixtime(time,'%Y-%m-%d %H:%i:%S')) as `转积分时间` from cn_memberchangeintegral group by `desc` having count>=2
商城系统出现客户同一个订单,同一秒扣款4次的情况。看了下业务代码先判断了下订单状态是否是已经支付状态,如果已经支付,则截止报错,如果没有支付情况有很多。在账户余额大于订单金额的情况下,需要在1.积分变更记录添加数据、2.减少会员账户余额、3.修改订单为已经支付状态 4.还有一些其他非必要的sql操作。
分析出现问题原因,客户点击了4次,可能是网络卡或者其他情况,这4个请求方法同时请求到了后端方法,如果不是同时的话,会判断出来订单状态已经支付而停止往下执行。第一个请求查询到了这个订单状态是未支付的,于是往下面执行,后面事务还没有执行到commit。第二个请求这个时候查询这个订单的状态还是未支付的状态,于是第二个请求还能继续往下走执行后面的事务。最终的结果就是后面的事务执行了4次,添加了4次积分变更记录,扣减了四次会员账户余额,修改了4次订单状态为已支付。最终结果就是一个订单,点击了4次支付,扣款了4次。
下面说下解决方案:
使用锁+事务,涉及到事务操作的几张表必须是innodb类型的。
第一步开启事务
第二步查询订单的时候要加锁(锁住之后这行记录不能再被其他其他进程对应的sql锁住)
第三步更新订单状态为已经支付,还有其他sql操作
第四步提交事务(或者出错回滚),这时候锁自动就解除了,其他几个并发的请求就又能查询这个订单了,在被锁住期间,其他几个请求一直是阻塞状态,等锁住的进程处理完毕后,这个进程就报错提示订单已经支付了。会把这个信息返回给客户端。
第五步 出现这种情况是客户端没有做点击提交之后禁用按钮,四个请求都能到后端,后端会给前端返回四个请求的处理结果,肯定是只有一次成功的,三次失败的。这个时候看客户端是怎么处理的。如果客户端获取返回值做跳转的话,这样跳转之后就收不到之前的请求的返回值了,如果只是在当前页面做提示服务端的返回值,那么这个时候能够看见1次提示成功,3次提示失败。先后顺序未知。看那个返回结果先到达客户端。
需要注意一点,更新会员账户余额那块的业务也涉及到这个问题。严格来说也要先查询账户余额同时锁住这行记录,防止被其他进程通过别的地方的业务代码给改掉,最后这里又执行了update。举个更形象的例子你要给自己手机充值20元话费(手机目前话费1元),这个时候你的好大哥,给你充值了50元,两次充值是同一个时间,你充值的时候获取到余额是1元,然后加上充值的20元,最后会被更新为21,你好大哥获取到你的余额是1元,然后充值50元,最后变成51。程序虽然是同时的,但是sql操作肯定是有先后的。如果你的作排在后面,那么你最终余额是21,如果你好大哥排在后面将会是51。最终你肯定亏了。如果是锁的模式,充值同时执行,但是你和你好大哥的充值肯定是只有一个人能锁住你的账户余额那条记录的。锁住之后只能等他的事务执行完毕才能解锁。锁着期间另外一个人获取余额操作只能等待,等上一个人释放 了锁,这个时候他去获取余额就是上一个操作更新后的了。这样子你最终余额是71元。
下面是粘贴下业务代码,除了支付下面还有卡号密码充值的操作。会员余额没有这么操作也是考虑到会员余额出现同时被改的概率低而已,实际上也得如上操作。
//添加支付记录修改版本 ,客户端不能规定金额,服务端自动生成金额版本。
private function add_pay_record ($request){
$keynum=$this->clientkeynum;
$orderkeynum = $request["order_keynum"]; //订单号唯一标识
$paymthod = $request["pay_method"]; //支付方式逗号拼接字符串
//从系统基本设置中取出来支付前缀
$webset=M("web_set")->where("basekeynum='$keynum'")->find();
//支付流水记录号前缀
$pay_prefix=$webset['pay_prefix']; //两个字母区分来自哪个商城
$orderid=$pay_prefix . date('Ymd') . str_pad(mt_rand(1, 99999), 5, '0', STR_PAD_LEFT);
//开启事务处理机制
$flag = true;
$Model = M();
$Model->startTrans();
try{
//获取订单信息增加悲观锁,->lock(true)
$orderinfo = $Model->table("cn_order")->lock(true)->where("keynum='$orderkeynum'")->find();
if(empty($orderinfo)){
$Model->rollback();
$data["sta"]="0";
$data["msg"]="获取订单信息失败!";
$this->send_back($data);die;
}
//如果订单已经支付成功返回错误提示
if($orderinfo['paystatus']=='1'){
$Model->rollback();
$data["sta"]="0";
$data["msg"]="对不起该订单已经支付成功!";
$this->send_back($data);die;
}
$memberid = $orderinfo['memberid'];
$memberinfo = $Model->table("cn_member")->where("id='$memberid'")->find();
if ($paymthod== "zfb") {
$paylog["orderid"] = $orderid;
$paylog["status"] = 0;
$paylog["paymethod"] = $paymthod;
$paylog["balance"] = 0;
$paylog["paymoney"] = $orderinfo["integral"];
$paylog["total_fee"] = $orderinfo["integral"];
$paylog["basekeynum"] = $keynum;
$paylog["order_keynum"] = $orderkeynum;
$Model->table("cn_paylog")->add($paylog);
//取出支付记录
$info=$Model->table("cn_paylog")->where("orderid='$orderid'")->find();
$Model->commit();
$data["sta"]="1";
$data["msg"]="创建成功";
$data['info']=$info;
$this->send_back($data);die;
} elseif ($paymthod == "wx") {
$paylog["orderid"] =$orderid;
$paylog["status"] = 0;
$paylog["paymethod"] =$paymthod;
$paylog["balance"] = 0;
$paylog["paymoney"] = $orderinfo["integral"];
$paylog["total_fee"] = $orderinfo["integral"];
$paylog["basekeynum"] = $keynum;
$paylog["order_keynum"] = $orderkeynum;
$Model->table("cn_paylog")->add($paylog);
//取出支付记录
$info=$Model->table("cn_paylog")->where("orderid='$orderid'")->find();
$Model->commit();
$data["sta"]="1";
$data["msg"]="创建成功";
$data['info']=$info;
$this->send_back($data);die;
} elseif ($paymthod== "yue") {
if ($memberinfo["integral"] >= $orderinfo["integral"]) { //直接下单成功
//向会员积分变更表添加数据
$date['integral'] = $orderinfo["integral"];
$date['type'] = '1';
$date['time'] = time();
$date['state'] = '已完成';
$date['userid'] = $orderinfo["memberid"];
$date['basekeynum'] = $keynum;
$date["desc"] = "购买商品,小程序订单号:" . $orderinfo['ordernumber'];
//向会员总收入表添加数据
$where2 = "userid=".$orderinfo["memberid"]." and clientkeynum='" . $orderinfo["clientkeynum"] . "'";
$integralallout = $Model->table("cn_memberallout")->where($where2)->find();
if ($integralallout) {
$data4["integral"] = $integralallout["integral"] + $orderinfo["integral"];
$Model->table("cn_memberallout")->where($where2)->save($data4);
} else {
$data4["userid"] = $orderinfo["memberid"];
$data4["clientkeynum"] = $orderinfo["clientkeynum"];
$data4["integral"] = $orderinfo["integral"];
$Model->table("cn_memberallout")->add($data4);
}
if ($Model->table('cn_memberchangeintegral')->add($date)) {
//修改会员积分
$date1['integral'] = $memberinfo["integral"] -$orderinfo["integral"];
$date1["integralallout"] = $memberinfo["integralallout"] + $orderinfo["integral"];
$bb = $Model->table('cn_member')->where("id='$memberid'")->save($date1);
}
//修改订单状态
$order_save["paymethod"]="yue";
$order_save["paybalance"]=$orderinfo["integral"];
$order_save["paystatus"]=1;
$order_save["paytime"]=time();
$Model->table("cn_order")->where("keynum='$orderkeynum'")->save($order_save);
//支付记录
$paylog["orderid"] =$orderid;
$paylog["status"] = 1;
$paylog["paymethod"] ="yue";
$paylog["balance"] = $orderinfo["integral"];
$paylog["paymoney"] = "0";
$paylog["total_fee"] = $orderinfo["integral"];
$paylog["basekeynum"] = $keynum;
$paylog["order_keynum"] = $orderkeynum;
$Model->table("cn_paylog")->add($paylog);
//取出支付记录
$info=$Model->table("cn_paylog")->where("orderid='$orderid'")->find();
$Model->commit();
$data["sta"]="1";
$data["msg"]="创建成功";
$data['info']=$info;
$this->send_back($data);die;
} else { //余额不够还需要在线支付一部分
//支付记录
$paylog["orderid"] =$orderid;
$paylog["status"] = "0";
$paylog["paymethod"] ="yue";
$paylog["balance"] = $memberinfo["integral"];
$paylog["paymoney"] = $orderinfo["integral"]-$memberinfo["integral"];
$paylog["total_fee"] = $orderinfo["integral"];
$paylog["basekeynum"] = $keynum;
$paylog["order_keynum"] = $orderkeynum;
$Model->table("cn_paylog")->add($paylog);
//取出支付记录
$info=$Model->table("cn_paylog")->where("orderid='$orderid'")->find();
$Model->commit();
$data["sta"]="1";
$data["msg"]="创建成功";
$data['info']=$info;
$this->send_back($data);die;
}
}else {
$Model->rollback();
$data["sta"]="0";
$data["msg"]="支付方式未找到";
$this->send_back($data);die;
}
}
//这里的\Exception不加斜杠的话回使用think的Exception类
catch(\Exception $e){
$Model->rollback();
$data["sta"]="0";
$data["msg"]= $e->getMessage();
$this->send_back($data);die;
}
}
//储值卡充值接口
private function to_convert ($request){
$keynum=$this->clientkeynum;
$card=$request["card"];
$pwd=kencrypt($request["pwd"]);
//获取会员信息
$member_info=$this->check_memberinfo($request);
//开启事务处理机制
$flag = true;
$Model = M();
$Model->startTrans();
//获取卡号信息 增加悲观锁,->lock(true) ,后面请求过来回阻塞不会走下去
try{
$info= $Model->table("cn_client_cardnum")->lock(true)->where("card_num='$card' and card_key='$pwd' and clientkeynum='$keynum'")->find();
if(empty($info)){
$msg["sta"]='0';
$msg["msg"]="卡号或密码错误";
$this->send_back($msg);die;
}else{
if($info["card_status"]=="0"){
$msg["sta"]='0';
$msg["msg"]="该卡未绑定";
$this->send_back($msg);die;
}elseif($info["card_status"]=="2"){
$msg["sta"]='0';
$msg["msg"]="该卡已兑换";
$this->send_back($msg);die;
}elseif($info["card_status"]=="3"){
$msg["sta"]='0';
$msg["msg"]="该卡已退卡";
$this->send_back($msg);die;
}elseif($info["card_status"]=="4"){
$msg["sta"]='0';
$msg["msg"]="该卡已作废";
$this->send_back($msg);die;
}
if($info["kai_status"]=="0"){
$msg["sta"]='0';
$msg["msg"]="该卡未开卡";
$this->send_back($msg);die;
}elseif($info["kai_status"]=="2"){
$msg["sta"]='0';
$msg["msg"]="该卡已关卡";
$this->send_back($msg);die;
}
$time=time();
if($info["start_time"]>$time){
$msg["sta"]='0';
$msg["msg"]="该卡未开始兑换";
$this->send_back($msg);die;
}
else if($info["end_time"]<$time){
$msg["sta"]='0';
$msg["msg"]="该卡已结束兑换";
$this->send_back($msg);die;
}else{
$cardtype=$Model->table("cn_client_cardtype")->where("id=".$info["cardtype"])->find();
$integral=$info["card_money"];
$memebrinfo=$Model->table("cn_member")->where("id=".$member_info["id"])->find();
//修改会员余额
$save_integral["integral"]=$integral+$memebrinfo["integral"];
$save_integral["integralallin"]=$integral+$memberinfo["integralallin"];
$result=$Model->table("cn_member")->where("id=".$member_info["id"])->save($save_integral);
//添加充值记录
$recharge["cardnum"]=$card;
$recharge["ConvertCode"]=$member_info["keynum"];
$recharge["integral"]=$integral;
$recharge["type"]=2;
$recharge["state"]="已完成";
$recharge["time"]=time();
$recharge["userid"]=$member_info["id"];
$recharge["desc"]="小程序储值卡充值,卡号:$card";
$recharge["basekeynum"]=$member_info["basekeynum"];
$Model->table("cn_memberchangeintegral")->add($recharge);
//修改卡状态
$card_save["card_status"]=2;
$card_save["exchange_time"]=time();
$card_save["exchange_member"]=$member_info["id"];
$Model->table("cn_client_cardnum")->where("card_num='$card'")->save($card_save);
//卡号日志记录
$card_log["time"]=time();
$card_log["type"]="卡号兑换";
$card_log["card_num"]=$card;
$card_log["operator"]=$member_info["name"];
$card_log["desc"]="小程序端客户兑换,卡标记已兑换";
$card_log["merchantkeynum"]="";
$card_log["clientkeynum"]=$keynum;
$Model->table("cn_client_cardlog")->add($card_log);
//卡号兑换记录
$exchange["card_num"]=$card;
$exchange["exchange_balance"]=$integral;
$exchange["time"]=time();
$exchange["memberid"]=$member_info["id"];
$exchange["desc"]="";
$exchange["merchantkeynum"]=$keynum;
$exchange["clientkeynum"]=$keynum;
$Model->table("cn_client_card_exchange")->add($exchange);
$card_status=$Model->table("cn_client_cardnum")->where("card_num='$card' and card_key='$pwd' and clientkeynum='$keynum'")->getField('card_status');
if($card_status==2){
$Model->commit();
$msg["sta"]='1';
$msg["msg"]="充值成功";
$this->send_back($msg);die;
}else{
$Model->rollback();
$msg["sta"]='0';
$msg["msg"]="充值失败";
$this->send_back($msg);die;
}
}
}
}
//这里的\Exception不加斜杠的话回使用think的Exception类
catch(\Exception $e){
$Model->rollback();
$data["sta"]="0";
$data["msg"]= $e->getMessage();
$this->send_back($data);die;
}
}
版权声明:若无特殊注明,本文皆为《菜鸟站长》原创,转载请保留文章出处。
本文链接:thinkphp3.2.3高并发锁是使用方法 - https://wziyi.com.cn/?post=378