码迷,mamicode.com
首页 > 其他好文 > 详细

关于程序并发

时间:2016-12-11 18:36:08      阅读:158      评论:0      收藏:0      [点我收藏+]

标签:status   http   更改   资源问题   check   var   单线程   get   加锁   

关于程序并发是老生常谈的话题了,工作中也经常去碰到,有必要来总结一下,其实并发与之关联的解决办法就是锁,加锁会消耗程序的性能和一些资源这是肯定的,当然如果能利用本身的原子性操作(指令的完整执行,在执行期间并不会被其他线程去中断,也不会存在上下文的切换),实现无锁编程是最好的。

1.防止重复请求

  最近经常发现数据库中有连续数据结构完全相同的数据,可能是用户连续点击,造成数据库的重复操作,我们需要防止这种操作。解决办法有很多,可以加锁,或者利用自身php指令的原子性,将uri,userid,parms(确保没有随机数据),md5校验后存到seession中,先get(),没有说明有效请求,有说明无效请求,看似很合理,但是get(),set()这种存在坑,比如一个用户连续点击多次,多个请求被多个线程执行,此时执行指令顺序是无法预测的,比如说get()无值可以操作,然后就有set进session,在set到session的过程中可能另一个线程也get()到了没有结果,这样也就连续插入两次了,所以我们选择了redis操作,redis都是原子性的操作而且是单线程的。setnx()方法很好,如果有这个键值就返回false,没有就写入成功,原子性操作,完美!

 /**
     * @return array 检查重复提交
     */
    public function checkRepeatSub()
    {
        $uri = app(request)->getRequestUri();
        $userid = \App\Services\SaleCarCall\Util\Utils::getInstance()->getUserId();
        $params = app(request)->all();
        if (empty($params)) {
            return true;
        }
        $info = [
            uri => $uri,
            userid => $userid,
            params => $params
        ];
        $key = RepeatSub. . md5(json_encode($info));
        $redis = RedisHelper::getRedisHandle();
        if (!$redis->setnx($key,1)) {
            throw new DuplicateException(Duplicate request);
        } else {
            $redis->expire($key,60);
            return true;
        }
    }

2.并发申请资源问题

  我们的系统有时候会出现两个客服申请到了同一个资源的问题,这也是典型的并发的问题,原因有好几点,

  2.1 因为采用的数据库的架构是主从分离,读写分离,一主多从的架构,自然而然的就是读落在了从库中,写落在了主库上,从库io线程读取主库的binlog,然后sql线程执行,而且从库的执行是单线程,主库是多线程并发的,所以无论如何主从一定会有延迟,这就会造成A申请到了一条线索,处理完状态应该是已处理,写到主库上面,因为延迟,从库中这条线索还没有改为已处理,那么这条线索就可能二次处理。所以对于及时性很高,状态变化很快的数据,建议还是读写都在主库。

  2.2 我们客服的工作流程典型的是读->改->写的过程,其实是++i的原理是一样的,涉及到两次操作,++i涉及到两次的内存操作,我们是两次数据库的操作,数据库天然的锁机制为我们提供了方便。所以尽量将读写改封装成一个原子性的操作。对于++i的操作,个人也习惯了redis操作,也就是重复提交那一套。对于数据库的那一套个人认为redis操作也是有一些坑。

  比如:我们加锁redis一定是要有过期时间的,如果执行过程中遇到错误或者抛出异常导致最后的delete锁没有执行,那么这个资源将一直被锁住,无法被其他人申请到,但是我们使用过期时间也是有坑的,正常额流程应该是A申请到了一条线索,申请一个redis锁(setnx),返回false,说明锁被占用,重新申请另一条线索,如果得到锁之后,do something,删除锁,看似读写改 在锁的机制下是原子性的操作,但是这个过期时间的设置很容易出错,比如说过期时间是2S,A申请到了id=1的线索,然后执行处理逻辑,但是处理逻辑执行了5S(可能发生),再第3S的时候,其实这个锁已经失效了,B可以申请到id=1的这个线索了,结果B执行花了1S,然后A执行完,更改数据库,B的操作记录就被覆盖了;或者A的执行完成时间在B之后,那么A删除的锁就不是删除自己加的那个锁了,A删除的就是B的锁,那么C就又可能申请到ID=1的这个线索,导致整个执行过程并不是原子性的了,出现混乱。所以最终我们选择了一种可以天然的利用数据库的行锁的机制(基于索引)。

原理是:在INNODB的存储引擎下,基于多版本的控制,读是不加锁的,(在读改写的机制下可能出现死锁),也就是ABC可能都申请到了id=1的资源,然后都要修改为自己名下的已处理,基于innodb的行锁机制,写操作是原子性的,A先修改,然后BC阻塞等待,A修改完,B修改,然后C修改,最后 A B的更新操作被C覆盖,这是典型的更新丢失的情况。所以,我们在设计数据库的时候,updatetime是数据库层面的,当ABc执行更新操作时候,where id=1 and updatetime=读取出来的时间,这样A更新完成后,updatetime被改变,BC不满足他们读取出来的时间和数据库时间一致的条件,更新失败,重新申请下一条的线索。

技术分享

 

   $retry = 0;
        while ($retry < 3) {
            $commonclue = SaleClueQueue::onWriteConnection()->select(id, updated_at)->
            where(operator, 0)>AppointCallVars::NEXT_CALL_TYPE_MOREN)->where(is_double_appoint, self::DOUBLE_TYPE)->
            orderBy(weight, asc)->first();
            if (!empty($commonclue)) {

                $updateData = [
                    appoint_status => AppointCallVars::APPOINT_STATUS_CALLING,
                    operator => Utils::getInstance()->getUserId(),
                ];
                $flag = SaleClueQueue::where(id, $commonclue->id)->where(updated_at, $commonclue->updated_at)->update($updateData);
                if ($flag) {
                    $log = self::$logPress . 申请到线索 : 结果 :  . json_encode($nextcall);
                    LogUtils::getInstance()->logInfo($log);
                    break;
                } else {
                    $retry++;
                }
            } else {
                $retry++;
            }
        }

 

关于程序并发

标签:status   http   更改   资源问题   check   var   单线程   get   加锁   

原文地址:http://www.cnblogs.com/tianye8123/p/6160010.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!