并发浅谈-锁和Token的应用

并发

即在同一时刻内有多个完成同一任务的进程或线程在同时运行。
并发一般发生在大流量集中访问如抢购或秒杀等业务场景中,它所带来的影响主要表现在以下两个方面:
1:造成系统的负载压力过大。比如说mysql天生在处理大并发时表现的异常吃力,并发大时经常可以造成数据库挂掉。
2:造成业务资源的竞争出现。比如说兑换一个激活码,并发下可能会出现两个人同时兑换到的同一个激活码。

从开发的经验来看,一般开发者在写程序逻辑时,绝大多数的情况下是没有考虑并发问题的;这其中有两个方面,一是与业务有关,二是与经验有关;其中经验是最重要的,缺乏经验的开发者甚至很难分析一个业务中是否要考虑并发问题。从一般的经验来说:凡是有竞争资源存在的业务中,一般都要考虑到并发问题。

既然并发竟然这么重要,那应该如何来测试了?
测试并发的问题上,开发者不要太把希望寄托在测试人员身上了,很多一般的测试人员可以把你的功能测得基本没有BUG,但对并发这种性能性的测试缺少相关经验。最好的办法是自己写一个并发专用测试用例,然后采用 Apache  ab 工具进行并发的模似测试,有关Apache   ab 工具的使用请自行查google。

锁是为了保障数据一致性的一种保护方式,举例来说:如果多个人同时对同一个文件进行读写操作,如果不给文件加锁则会产生意想不到的结果。

锁一般用得多的是:共享锁定(其它程序可以同时读);独占锁定(其它程序靠边站)

我们在PHP中应用最多的有以下三种锁:
1:内存锁
       在PHP中可以利用如共享内存的机制来实现,或者直接使用opcode扩展中的eaccelerator(PS)直接提供的相关锁函数.在常规操作中,内存锁的效率是最高的。
2:文件锁
       PHP中打开一个文件时可以加不同类型的锁.
3:mysql表锁
       mysql内部数据在操作时它会采用队列的方式来处理同一时发来的查询,所以大家不要担心并发查询时它会处理异常的情况。对外它提供的表锁,主要是为了满足我们的业务需要,它是基于线程的。有一点要注意:表锁应用时mysql要损很大的性能。并发大时发现突出。

[经验之谈]:
当我们没有可用的资源来实现内存锁时,可以采用linux下的 /dev/shm 挂接点,这个目录是内存区域的一个映射,即在这个目录中存入文件相当于存入内存中,IO性能肯定远高于磁盘文件的IO了。所以我们可以对这个目录下的特定文件进行加锁,从到达到内存锁的高性能。

(PS):
opcode优化扩展有:(APC,XCache,eAccelerator)具体使用和优化可以看资料整理http://www.cnblogs.com/cuoreqzt/p/3824757.html
从服务器性能优化来讲,opcode优化扩展是一个非常重要的环节,从专业的性能测试可以看出,opcode优化能提高PHP的执行性能很多,表现出来就是搞高并发数。

[Token]

Token 是令牌的意思,有点像任我任的黑木令,一种检验身份/会话合法性的一种机制,一般在SSO这种系统中应用得比较多。
Token 一般有以下几个特性:
1:唯一性,即每个ID都是唯一的。
2:时间有效性,即存在过期时间。
3:一次性使用,即使用一次后就失效。

综合以上特性,我们很自然的想到用缓存机制可以很方便实现Token功能,基于扩展性和性能的考虑,memcache是首选,但不仅限于它,只要可以符合这三点,其它方法也行,比如说 apc,file 等。

[实例应用]

业务场景说明:

网站免费发放购物优惠卷激活码,但每天只放100个免费的,这样就会造成用户每天在 24:00 时集中来兑换。这个需求好像很简单,但存在着并发问题。

以下从最简单的版本开始讲解:

----------------------------------
第1个版本的代码:
----------------------------------

function getCode(){
    // 得到一个没有使用的激活码
    $row = $db->fetchRow(‘select id,code from codes where stat=0‘);
    // 将激活码锁定
    $db->execute(‘update codes set stat=1 where id=‘.$row[‘id‘]);
    return $row[‘code‘];
}

解说:
这个代码单个执行时是没有BUG吧,但这里存在严重的并发问题,因为此时slelct后的结果都按默认的排序,所以多个进程同时取时,就取到了同一个激活码。
----------------------------------

----------------------------------
第2个版本的代码:
----------------------------------

function getCode(){
    // 得到一个没有使用的激活码
    $row = $db->fetchRow(‘select id,code from codes where stat=0 order by rand()‘);
    // 将激活码锁定
    $db->execute(‘update codes set stat=1 where id=‘.$row[‘id‘]);
    return $row[‘code‘];
}

解说:

采用随机排序后可以降低并发时出现同一个激活码,但并发大时还是会出现大量重复的情况。
----------------------------------

----------------------------------
第3个版本的代码:
----------------------------------

function getCode(){
    // 防止并发
    usleep(mt_rand(1000,10000));
    // 得到一个没有使用的激活码
    $row = $db->fetchRow(‘select id,code from codes where stat=0 order by rand()‘);
    // 将激活码锁定
    $db->execute(‘update codes set stat=1 where id=‘.$row[‘id‘]);
    return $row[‘code‘];
}

解说:

这里加了随机休眠进程的机制,再结合随机排序,比版本2是优化了很多,但还是不能从根本上解决重复的问题。并且这种方式又会带来新的并发性能问题。因为你增加了响应时间。
----------------------------------

----------------------------------
第4个版本的代码:
----------------------------------

function getCode(){
    // 锁表
    $db->execute(‘lock tables codes write‘);
    // 得到一个没有使用的激活码
    $row = $db->fetchRow(‘select id,code from codes where stat=0 order by rand()‘);
    // 将激活码锁定
    $db->execute(‘update codes set stat=1 where id=‘.$row[‘id‘]);
    // 解锁
    $db->execute(‘unlock tables‘);
    return $row[‘code‘];
}

解说:

这里给表加了独占的写锁,其它MYSQL线和在我没有处理完前都要靠边站;但这里有性能问题,前面我说过mysql的锁表机制很损性能的,并且这样有很大的风险,因为一但表没有得到解锁的话,越来越多的连接线程就全卡着不动了,变动sleep状态了。一个网站的性能瓶颈很大程度上就是DB的并发处理能力,这样更降低的DB的并发能力。所以这个方案性价比不是很高。
----------------------------------

----------------------------------
第5个版本的代码:
----------------------------------

/*
 [内存锁]
 如果服务器没安装 eAccelerator 扩展,则可以采用 eaccelerator_lock() eaccelerator_unlock() 性能更好.
 这里采用拆中方案: /dev/shm
*/
class memLock{
    static private $_fp = null;
    // 加锁
    static public function lock(){
        if(null === self::$_fp){
            self::$_fp = fopen(‘/dev/shm/score-exchange.txt‘, ‘w+‘);
        }
        return flock($_fp, LOCK_EX);
    }
    // 解锁
    static public function unlock(){
        flock($_fp, LOCK_UN);
        clearstatcache();
    }
}

function getCode(){
    // 锁进程
    memLock::lock();
    $code = _get();
    // 解锁
    memLock::unlock();
    return $code;
}

function _get(){
    // 得到一个没有使用的激活码
    $row = $db->fetchRow(‘select id,code from codes where stat=0 order by rand()‘);
    // 将激活码锁定
    $db->execute(‘update codes set stat=1 where id=‘.$row[‘id‘]);
    return $row[‘code‘];
}

解说:

这里将表锁的性能开销换成了性能更好的内存进程锁,与上一个版本相比,这个性能比有所改进,提高了性能。但这个方案还是可能会出现异常现象,特别是被恶意机器人来刷激活码时。因为一般的兑换请求可能是:

GET /exchange?userid=5

要写个机器人来刷还是不难,可以利用工具或利用 curl,类似以下过程:
curl ‘<登录>‘
curl ‘/exchange?userid=5‘

我们可以优化一点,考虑从源头来控制被刷的问题。
----------------------------------

----------------------------------
最后版的代码:
----------------------------------

/*
 [内存锁]
 如果服务器没安装 eAccelerator 扩展,则可以采用 eaccelerator_lock() eaccelerator_unlock() 性能更好.
 这里采用拆中方案: /dev/shm
*/
class memLock{
    static private $_fp = null;
    // 加锁
    static public function lock(){
        if(null === self::$_fp){
            self::$_fp = fopen(‘/dev/shm/score-exchange.txt‘, ‘w+‘);
        }
        return flock($_fp, LOCK_EX);
    }
    // 解锁
    static public function unlock(){
        flock($_fp, LOCK_UN);
        clearstatcache();
    }
}

/**
 * Token 处理
 */
class Token{

    private $_cache = null;

    /**
     * 缓存对象实例
     *
     */
    private static $instance = null;

    /**
     * 以单例模式返回实例
     *
     */
    static public function getInstance()
    {
        if (null === self::$instance)
        {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * 构造函数
     *
     */
    public function __construct(){
        $this->_cache = new Memcache;
        $this->_cache->addServer(‘10.10.2.104‘,‘11211‘);
    }

    /**
     * 验证 Token
     *
     * @param unknown_type $token : Token值
     */
    public function check($tokenid){
        $id = $this->_get();
        if(!$id || $id!=$tokenid){
            return false;
        }else{
            // Token特性1:一次性用品
            $this->_set(‘‘);
            return true;
        }
    }

    /**
     * 得到 Token ID
     *
     */
    public function get(){
        // Token特性2:唯一性
        $token = md5(uniqid(time().rand().$_COOKIE[‘userid‘]));
        $this->_set($token);
        return $token;
    }

    // 得到缓存key
    private function _key(){
        return ‘tokon‘.$_COOKIE[‘userid‘];
    }

    // 设置缓存
    private function _set($token){
        // 轮循算法是为了尽量的处理TCP连接失效
        $i = 0;
        while($i < 5){
            // Token特性3:时效性
            $ret = $this->_cache->set($this->_key() , $token, MEMCACHE_COMPRESSED, 10);
            if($ret) break;
            ++$i;
        }
    }

    // 取缓存
    private function _get(){
        // 轮循算法
        $i = 1;
        while($i < 5){
            $ret = $this->_cache->get($this->_key() , MEMCACHE_COMPRESSED);
            if($ret !== FALSE) break;
            ++$i;
        }
        return $ret;
    }
}

========================================

兑换流程步骤拆分(任务拆分为3步)
========================================

步骤1:
登录后写一个特殊的COOKIE用于标识用户是在浏览器中正常登录的行为:
-----------------------------------------------------------

function loginCallBack(){
    $cokname = md5(‘exchange‘.$this->userid.$this->sessionid);
    if(!isset($_COOKIE[$cokname])){
        setcookie($cokname, 1);
    }
}

说明:

这个算法是为了保证每个用户每次正常登录的COOKIE都不一样,注意在实际中不要写得太明显了,你可以考虑在另一个不相干的任务做做这个事情,劈开破解者的注意视线,增加破解难度。同时写好注释。
-----------------------------------------------------------

步骤2:
得到一次兑换请求的Token信息
-----------------------------------------------------------

function getToken(){
    // 是否是正常登录的用户
    $cokname = md5(‘exchange‘.$this->userid.$this->sessionid);
    if(!isset($_COOKIE[$cokname]) || $_COOKIE[$cokname]!=1){
        $token = 0;
    }else{
        $token = Token::getInstance()->get();
    }
    $this->outputJson(0,‘ok‘, $token);
}

-----------------------------------------------------------

步骤3:
改变前端javascript兑换的逻辑代码如下:
----------- 原逻辑 ----------------------------------------

function exchange(){
    var url = ‘/exchange?userid=5‘;
    $.get(url,function(ret){
        alert(ret.data);
    },‘json‘);
}

----------- 新逻辑 ----------------------------------------

function exchange(){
    var url = ‘/getToken‘;
    $.get(url,function(ret){
        url = ‘/exchange?userid=5&tk=‘+ret.data;
        $.get(url,function(ret){
            alert(ret.data);
        },‘json‘);
    },‘json‘);
}

-----------------------------------------------------------

function getCode(){
    // Token 信息是否正确
    $token = $this->getGet(‘tk‘,0);
    if($token == 0 || !Token::getInstance()->check($token)){
        $this->outputJson(-1,‘非法请求‘);
    }
    // 锁进程
    memLock::lock();
    $code = _get();
    // 解锁
    memLock::unlock();
    return $code;
}

function _get(){
    // 得到一个没有使用的激活码
    $row = $db->fetchRow(‘select id,code from codes where stat=0 order by rand()‘);
    // 将激活码锁定
    $db->execute(‘update codes set stat=1 where id=‘.$row[‘id‘]);
    return $row[‘code‘];
}

解说:

现在可以最大限制的防止恶意刷的行为了,当同一个兑换请求: /exchange?userid=5&tk=xxxxx 再次执行时将会失效,因为它的Token信息已经失效了.
----------------------------------

总结:

1:这里只是对并发的处理进行的简单的描述,给读者一点启发。

2:也可以采用 Innodb 的事务来处理或存储过程来处理。

3:解决并发最好的算法应该是采用队列的机制,据我所了解的资料,解决并发其实最方便编程的应该是 MongoDB 中的 findAndModify 操作,因为MongoDB 是专为Web开发所设计的一种NoSql型的DBMS系统,它天生对大请求量的并发处理有着非常高效的性能,天生支持原子操作。
有关 MongoDB 的详细资源推荐看看《MongoDB权威指南》
有关 MongoDB 的安装配置可以参考: http://vquickphp.com/?a=blogview&id=31
有关 MongoDB 的PHP应用可以参考: http://vquickphp.com/?a=blogview&id=32

并发浅谈-锁和Token的应用

时间: 2024-10-08 13:24:25

并发浅谈-锁和Token的应用的相关文章

浅谈数据库并发控制 - 锁和 MVCC

在学习几年编程之后,你会发现所有的问题都没有简单.快捷的解决方案,很多问题都需要权衡和妥协,而本文介绍的就是数据库在并发性能和可串行化之间做的权衡和妥协 - 并发控制机制. 如果数据库中的所有事务都是串行执行的,那么它非常容易成为整个应用的性能瓶颈,虽然说没法水平扩展的节点在最后都会成为瓶颈,但是串行执行事务的数据库会加速这一过程:而并发(Concurrency)使一切事情的发生都有了可能,它能够解决一定的性能问题,但是它会带来更多诡异的错误. 引入了并发事务之后,如果不对事务的执行进行控制就会

搞懂分布式技术16:浅谈分布式锁的几种方案

搞懂分布式技术16:浅谈分布式锁的几种方案 前言 随着互联网技术的不断发展,数据量的不断增加,业务逻辑日趋复杂,在这种背景下,传统的集中式系统已经无法满足我们的业务需求,分布式系统被应用在更多的场景,而在分布式系统中访问共享资源就需要一种互斥机制,来防止彼此之间的互相干扰,以保证一致性,在这种情况下,我们就需要用到分布式锁. 分布式一致性问题 首先我们先来看一个小例子: 假设某商城有一个商品库存剩10个,用户A想要买6个,用户B想要买5个,在理想状态下,用户A先买走了6了,库存减少6个还剩4个,

浅谈Mysql共享锁、排他锁、悲观锁、乐观锁及其使用场景

浅谈Mysql共享锁.排他锁.悲观锁.乐观锁及其使用场景 Mysql共享锁.排他锁.悲观锁.乐观锁及其使用场景 一.相关名词 |--表级锁(锁定整个表) |--页级锁(锁定一页) |--行级锁(锁定一行) |--共享锁(S锁,MyISAM 叫做读锁) |--排他锁(X锁,MyISAM 叫做写锁) |--悲观锁(抽象性,不真实存在这个锁) |--乐观锁(抽象性,不真实存在这个锁) 二.InnoDB与MyISAM Mysql 在5.5之前默认使用 MyISAM 存储引擎,之后使用 InnoDB .查

浅谈高并发架构

本篇文章主要是浅谈一些高并发的方案,指出一个大致方向,如果有需要优化提高系统性能,可以从以下方法中找出合适的使用. 随着淘宝.京东.唯品会等很多电商的出现,所谓互联网公司也就经常听到了,这些互联网公司给我们的第一印象,用户活跃交易量大.为了给用户一个好的交互体验,我们需要根据具体的业务场景来设计适合自己的高并发处理方案.服务器的架构我们在网上也看到过很多文档描述,像美团的火热.饿了么的崛起都有提到服务器的架构演变,基本都是从相对单一到集群,再到分布式服务.从一开始交易量小知名度低软件开发工期紧张

浅谈千万级PV/IP规模高性能高并发网站架构(转自老男孩)

原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://oldboy.blog.51cto.com/2561410/736710 如果把来访用户比作来犯的"敌人",我们一定要把他们挡在800里地以外,即不能让他们的请求一下打到我们的指挥部(指挥部就是数据库及分布式存储). 如:能缓存在用户电脑本地的,就不要让他去访问CDN. 能缓存CDN服务器上的,就不要让CDN去访问源(静态服务器)了.能访问静态服务器的,就不要去访问动态

(转)浅谈千万级PV/IP规模高性能高并发网站架构

浅谈千万级PV/IP规模高性能高并发网站架构 原文:http://blog.51cto.com/oldboy/736710 文章架构简图:   高并发访问的核心原则其实就一句话"把所有的用户访问请求都尽量往前推". 如果把来访用户比作来犯的"敌人",我们一定要把他们挡在800里地以外,即不能让他们的请求一下打到我们的指挥部(指挥部就是数据库及分布式存储). 如:能缓存在用户电脑本地的,就不要让他去访问CDN. 能缓存CDN服务器上的,就不要让CDN去访问源(静态服务

浅谈Java锁

每当遇到Java面试,"锁"是个必然会被提到的东西.那么,在面试中,谈"锁"都会谈论些什么呢,诸位看官又是否对"锁"有足够的了解? 本文旨在剖析锁的底层原理,以及锁的应用场景. 一.Synchronized 1.一道面试题 同一个对象在A.B两个线程中分别访问该对象的两个同步方法writer和reader,是否会产生互斥? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

浅谈图片服务器的架构演进

浅谈图片服务器的架构演进 现在几乎任何一个网站.Web App以及移动APP等应用都需要有图片展示的功能,对于图片功能从下至上都是很重要的.必须要具有前瞻性的规划好图片服务器,图片的上传和下载速度至关重要,当然这并不是说一上来就搞很NB的架构,至少具备一定扩展性和稳定性.虽然各种架构设计都有,在这里我只是谈谈我的一些个人想法. 对于图片服务器来说IO无疑是消耗资源最为严重的,对于web应用来说需要将图片服务器做一定的分离,否则很可能因为图片服务器的IO负载导致应用 崩溃.因此尤其对于大型网站和应

浅谈 编译器 &amp; 自然语言处理

============================================== copyright: KIRA-lzn ============================================== 转载请注明出处,这篇是我原创,翻版必究!!!!!!!!!!!!!!!!!!! ============================================== 如果觉得写个好,请留个言,点个赞. 自我介绍:本人13届 USTC 研一学生,菜鸟一枚,目前在int