• php在大并发下redis锁实现


      在现如今电商盛行的时期,会出现很多促销活动,最为常见的就是秒杀。在秒杀系统中最为常见的问题就是会出现超卖的情况,那么如何来杜绝超卖的情形了,在业务逻辑层面可以使用缓存以及加锁的手法来避免超卖的情形。

      现如今nosql已经非常流行和稳定了,在此我将通过redis和php来说明如何实现锁机制。当然我使用redis加锁并不是我的秒杀系统,而是最近做的一个项目有个用户提现,初期没有考虑到会有人恶意刷新接口,而导致用户无限制提现。经过查看nginx日志,发现用户在同一时间段,通过刷接口的方法超额提现,导致亏损

      起初的提现代码如下:

    $uid = $this->user_id;
            if (empty($uid)) {
                return $this->responseJson(300, '请先登录');
            }
            $money = $this->request->get('money', 'trim');
            $formId = $this->request->get('formId', 'trim', '');
            $user_model = new User();
            $user_info = $user_model->getUserInfoById($uid);
            $balance = $user_info['balance'] / 100;
            $phone = $this->request->get('phone', 'trim');
            if ($money < 1) {
                return $this->responseJson(300, '最小提现金额为1元');
            }
            if ($balance < $money) {
                return $this->responseJson(300, '账户余额不足');
            }
            $openid = $user_info['openId'];
            if (in_array($openid, ['oMl_x0DiSCYqQuqJOKV9bAqR1Ugk', 'oMl_x0B1zoY70dbiwxAt4lg2fmL4', 'oMl_x0E0jYOK6NpbzwmTVJowpfpk', 'oMl_x0GHeAdKCZ8Iv1KD0CmdZLQ0', 'oMl_x0FBU1eWya1fG5xtVxryUYG4', 'oMl_x0CIMr5tItEy1QPtpI9eFJak', 'oMl_x0JWFdGOnf80W5oZOX-XfGcw'])) {
                return $this->responseJson(300, '正在处理中');
            }
            if (!empty($phone)) {
                if (!isset($user_info['phone']) || (isset($user_info['phone']) && $user_info['phone'] != $phone)) {
                    $user_model->update(['_id' => $this->user_id], ['$set' => ['phone' => $phone]]);
                }
            }
            $order_id = Common::getOrder();
            $res = $user_model->updateBalanceById($uid, $money * 100);
            if ($res) {
                $trans_res = GlobalFunc::transfer($uid, $order_id, $openid, 'NO_CHECK', $money);
    
                if ($trans_res === false) {
                    $user_model->updateBalanceById($uid, $money * 100, 2);
                    $redis->del('with:draw:' . $uid);
                    if (!empty($formId)) {
                        $TemplateMsg = new TemplateMsg();
                        $to_user = $user_info['openId'];
                        $tem_id = 'ZSpYvjqdawADxr7j_8DJFuaoAdWbHhXdnAFlp5QF9L0';
                        $data = array(
                            'keyword1' => array('value' => '提现', 'color' => "#173177"),
                            'keyword2' => array('value' => date('Y-m-d H:i:s'), 'color' => "#173177"),
                            'keyword3' => array('value' => $trans_res['err_code_des'] . "。申请提现金额已自动退回账户余额中。", 'color' => '#173177'),
                        );
                        $page = 'pages/balance/balance';
                        $TemplateMsg->doSend($to_user, $tem_id, $formId, $data, $page);
                    }
                    return $this->responseJson(300, $trans_res['err_code_des']);
                }
                $redis->del('with:draw:' . $uid);
            } else {
                $redis->del('with:draw:' . $uid);
                return $this->responseJson(300, '提现失败,请稍后重试');
            }
    ....

    看上面代码逻辑感觉似乎没有什么问题,确实,在正常的情况下是不会出现问题,如果有人恶意的去刷接口的话,上述问题就出现了。为了防止用户恶意刷接口,所以对现有代码做了如下修改

    $uid = $this->user_id;
            if (empty($uid)) {
                return $this->responseJson(300, '请先登录');
            }
            $money = $this->request->get('money', 'trim');
            $formId = $this->request->get('formId', 'trim', '');
            $user_model = new User();
            $user_info = $user_model->getUserInfoById($uid);
            $balance = $user_info['balance'] / 100;
            $phone = $this->request->get('phone', 'trim');
            if ($money < 1) {
                return $this->responseJson(300, '最小提现金额为1元');
            }
            if ($balance < $money) {
                return $this->responseJson(300, '账户余额不足');
            }
            $redis = $this->cache('redis');
            $redis->incr('with:draw:' . $uid);
            if (intval($redis->get('with:draw:' . $uid)) > 1) {
                return $this->responseJson(300, '正在处理中');
            }
            $openid = $user_info['openId'];
            if (in_array($openid, ['oMl_x0DiSCYqQuqJOKV9bAqR1Ugk', 'oMl_x0B1zoY70dbiwxAt4lg2fmL4', 'oMl_x0E0jYOK6NpbzwmTVJowpfpk', 'oMl_x0GHeAdKCZ8Iv1KD0CmdZLQ0', 'oMl_x0FBU1eWya1fG5xtVxryUYG4', 'oMl_x0CIMr5tItEy1QPtpI9eFJak', 'oMl_x0JWFdGOnf80W5oZOX-XfGcw'])) {
                return $this->responseJson(300, '正在处理中');
            }
            if (!empty($phone)) {
                if (!isset($user_info['phone']) || (isset($user_info['phone']) && $user_info['phone'] != $phone)) {
                    $user_model->update(['_id' => $this->user_id], ['$set' => ['phone' => $phone]]);
                }
            }
            $order_id = Common::getOrder();
            $res = $user_model->updateBalanceById($uid, $money * 100);
            if ($res) {
                $trans_res = GlobalFunc::transfer($uid, $order_id, $openid, 'NO_CHECK', $money);
    
                if ($trans_res === false) {
                    $user_model->updateBalanceById($uid, $money * 100, 2);
                    $redis->del('with:draw:' . $uid);
                    if (!empty($formId)) {
                        $TemplateMsg = new TemplateMsg();
                        $to_user = $user_info['openId'];
                        $tem_id = 'ZSpYvjqdawADxr7j_8DJFuaoAdWbHhXdnAFlp5QF9L0';
                        $data = array(
                            'keyword1' => array('value' => '提现', 'color' => "#173177"),
                            'keyword2' => array('value' => date('Y-m-d H:i:s'), 'color' => "#173177"),
                            'keyword3' => array('value' => $trans_res['err_code_des'] . "。申请提现金额已自动退回账户余额中。", 'color' => '#173177'),
                        );
                        $page = 'pages/balance/balance';
                        $TemplateMsg->doSend($to_user, $tem_id, $formId, $data, $page);
                    }
                    return $this->responseJson(300, $trans_res['err_code_des']);
                }
                $redis->del('with:draw:' . $uid);
            } else {
                $redis->del('with:draw:' . $uid);
                return $this->responseJson(300, '提现失败,请稍后重试');
            }
    ...

      代码调整后似乎防止了用户刷接口的行为,但是后期有用户反映,自己提不了现了。经过一番查看,原来redis的值一直存在,虽然用户操作完成后会删除key,但是也会存在在用户没有完全操作完成而导致流程中断,所以会导致key删除失败,为了解决锁不释放的问题,又对上述代码进行修改,在设置锁的时候,设置一个过期时间,修复如下

    if (intval($redis->get('with:draw:' . $uid)) > 1) {
                if ($redis->ttl('with:draw:' . $uid) == -1) {
                    $redis->expire('with:draw:' . $uid, 30);
                }
                return $this->responseJson(300, '正在处理中');
            }

      这样就可以实现锁不释放的问题,但是上述代码除了使用incr操作外,还可以使用redis的setnx来代替,其实是一样的效果,但是无论你用那种还是有点问题就是,当你写入成功之后,突然断网或服务器宕机的情况,这时还会出现上述问题,那应该如何来解决呢。其实完全可以通过redis的 Multi/Exec结合来解决上述问题,其代码如下

    $redis->multi();
    $redis->setNX($key, $value);
    $redis->expire($key, $ttl);
    $redis->exec();

      这样就可以解决突然情况带来的妖怪问题了

      总结:通过redis可以实现大并发的数据请求操作,通过事务的操作来加锁和释放锁,达到数据完整性

      虽然上述问题解决了,但是代码还是有待优化,从 2.6.12 起,SET 涵盖了 SETEX 的功能,并且 SET 本身已经包含了设置过期时间的功能,也就是说,我们前面需要的功能只用 SET 就可以实现。所以上述代码可以简化为

      

    $redis->set($key, $random, array('nx', 'ex' => $ttl));

    以上就是如何解决并发和恶意刷接口的解决方法,以作记录

  • 相关阅读:
    django模板引擎自定义变量
    Ubuntu系统桌面任务栏和启动器全部消失解决方案
    ubuntu beyond compare到期后续期
    git 分支操作 与 远程新建分支后,本地查看不到
    jmeter 安装
    ubuntu apt-get install 时报错curl : Depends: libcurl4 (= 7.58.0-2ubuntu3.6) but 7.61.0-1ubuntu2 is to be installed或者 vim : Depends: vim-common (= 2:8.0.1453-1ubuntu1) but 2:8.0.1766-1ubuntu1 is to be ins
    【uWSGI】 listen queue of socket (fd: 3) 错误分析
    通过CONN_MAX_AGE优化Django的数据库连接
    Linux(CentOS) 查看当前占用CPU或内存最多的K个进程
    centos django Failed to load resource: net::ERR_INCOMPLETE_CHUNKED_ENCODING
  • 原文地址:https://www.cnblogs.com/tm2015/p/7845503.html
Copyright © 2020-2023  润新知