使用 redis 处理高并发原理??

假设一个抢购系统,每天 16:16:0016:17:001分钟每10s 放出一个产品进行抢购,中奖用户的计算是 单次每轮(每 10 s内),第一个点击 我要抢购 按钮的人为中奖用户。

情况是:在短暂的 10s 内假设有 5千万个用户 参与了,且在 短短的 第1s内 ,集中了 4千万个用户 点击了 我要抢购按钮

由于 php 只能够精确到 us(微妙,microtime 函数可获取),1s = 100 0000us ,而在短暂的 1s 内却集中了 4千万个用户同时点击我要抢购按钮

于是,并发问题就来了。

总会发生在 1us内有多个用户同时点击我要抢购按钮,于是这轮抢购结束后,统计结果,就会出现错误的现象:一轮抢购居然有多个人同时中奖了!

所以,我一开始还打算用 microtime() 来解决并发问题,但是如果出现了我上面描述的情况时,先不论数据库能不能再短时间内承受住如此大量的插入操作,即使承受住了,程序也照样会出错!

我听说 redis 是解决并发的良药,不知道他解决这种问题的原理是什么??

阅读 13.9k
8 个回答

题主总是过于纠结 “时间精确度”,这个是没有意义而且是错误的用词。

php单机实测理想情况下每秒能处理1000个请求/每秒左右。题主架设的40000000/1000=40000台php服务器做负载均衡才能实现。这时候nginx将成为瓶颈,将需要基于TCP负载均衡的路由将请求发送到多个nginx负载均衡器。

我们看看题主真正的问题。

我们假设服务器的处理速度是100并发每秒。而现在有一个商品,在某一秒开启抢购。有2个用户参与抢购,按照题主的理解,这就可以直接无压力搞定了。

但是实际上并不是,哪怕只是两个用户抢购一个商品。甚至一个用户抢购一个商品,也要需要借助数据库并发锁实现防止重复读取数据。

所以,抢购程序的开发,关键在于,而不是并发数,或者“时间精确度”

你需要解决的不是「高并发」,而是在并发时会发生的竞态。跟多线程编程一样的,所以也可以用类似的思路:用锁。

你用 SQL 数据库的话,可以用事务来解决。但是你这么高的并发交给数据库显然会太慢。Redis 作为内存数据库,实现得又好,所以速度飞快。然后呢,解决竞态,还是靠人来编程解决。Redis 只是提供了足以解决问题的工具。

推荐《Redis 实战》一书,写得很好。你可以直接跳过基础(或者扫两眼,如果你没用过的话),看后边实践的部分(这部分在我给的链接上不能试读。下单吧,值得的。)

仔细读了一下你的需求,好像解决起来挺容易的。你到时间把一个 token 放到特定的 list 里去,然后大家来取(lpop 或者 rpop),谁取到算谁的。(单个)Redis 是不支持并行的,所以不会取重。不过 40m/s 的请求,单个 Redis 实例搞不定的样子……你先试试看吧。不知道你 PHP 是怎么搞定这么高的并发的。

楼主的提问确实会误导一批人啊,时间的精确度和能承受多少并发量应该是没任何关联的吧!!!

1、4000w的并发绝不是简单的处理吞吐能力,而是需要业务优化流程、中间件算法、消息队列、数据缓冲层等等取支撑的

2、单靠redis绝对不可能

3、时间精确度决定了你的业务处理,况且4000w并发请问你做的是淘宝还是京东,如果只是个很low的商城的话清把命题设置的合理些,不要动不动就是千万级并发。。。并发不是QPS,这个需要正确理解业务的

4、我能提供的方向就是通过c获取到纳秒级别的时间精确指针,至于php的话适合快速开发和千级并发能力,上万级别的并发单台php是撑不住的,况且你还会遇到单台server 65535端口的硬性限制,除非你是独立硬件设备改系统底层才能支持超出65535端口数量,但实际情况是独立服务器硬件都必须高配并搭载类似dock的环境,这样折算下来和云server没什么区别,如果按单台server php支撑3000 - 5000并发的话你算算要多少台吧

去年,在做 pv | uv 统计的时候我采用了redis,看了一些书,因为其操作都是内存中进行的,并且拥有表的概念,所以速度飞快,只是解决了数据库压力,楼主说的4000w并发...倒是没有真正应用上,其实楼下知道的麻烦告知大家,贴一个以前写的redis类帮助各位大佬快速回忆一下

<?php
namespace Cache;
use Redis;
use Yaf;
/**
 * RedisServer [REmote DIctionary Serve 驱动器] 
 * 
 * @uses Redis 
 * @version ${Id}$
 * @author Shaowei Pu <pushaowei@sporte.cn>
 * @license sporte.cn
 * @copyright 2016-12-23 20:39:57  
 * @final __construct
 *
 * ============================================================
 * 五种数据结构:String / Hash / List / Set / Ordered Set。
 * ============================================================
 * 
 * Redis默认最大连接数是一万。
 * Redis默认不对Client做Timeout处理,可以用timeout 项配置,但即使配了也不会非常精确。
 * 
 * - Key
 * Key 不能太长,比如1024字节,但antirez也不喜欢太短如"u:1000:pwd",要表达清楚意思才好。
 * 私人建议用":"分隔域,用"."作为单词间的连接,如"comment:1234:reply.to"。
 *
 * - Set
 * Set就是Set,可以将重复的元素随便放入而Set会自动去重,底层实现也是hash table。
 * 
 * - String
 * 最普通的key-value类型,说是String,其实是任意的byte[],比如图片,最大512M。
 * 所有常用命令的复杂度都是O(1),普通的Get/Set方法,可以用来做Cache,存Session,为了简化架构甚至可以替换掉Memcached。
 *
 * - Hash
 * Key-HashMap结构,相比String类型将这整个对象持久化成JSON格式,Hash将对象的各个属性存入Map里,
 * 可以只读取/更新对象的某些属性。这样有些属性超长就让它一边呆着不动,另外不同的模块可以只更新自己关心的属性而不会互相并发覆盖冲突
 *
 * - List
 * List是一个双向链表,支持双向的Pop/Push,江湖规矩一般从左端Push,右端Pop——LPush/RPop,而且还有Blocking的版本BLPop/BRPop.
 * 客户端可以阻塞在那直到有消息到来,所有操作都是O(1)的好孩子,可以当Message Queue来用。
 * 当多个Client并发阻塞等待,有消息入列时谁先被阻塞谁先被服务。任务队列系统Resque是其典型应用。
 * 
 */
class RedisServer 
{
    private static $_main;
    private static $_follow;
    private $_redis;

    /**
     * [__construct 初始化Redis连接]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:50:16+0800
     */
    final private function __construct($config,$select)
    {
        $this->_redis = new Redis();
        if(!empty($config))
        {
            // 创建连接
            $this->_redis ->connect(
                $config->redis_host,
                $config->redis_port,
                $config->redis_timeout
            );
            if(is_resource($this->_redis->socket) && !empty($config->redis_auth)){
                $this->_redis->auth($config->redis_auth);
            }
            $this->_redis->select($select);
        }
        return $this->_redis;
    }

    /**
     * [__clone 禁用克隆]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-23T20:51:24+0800
     * @return                              [type] [boole]
     */
    final private function __clone(){} 
    /**
     * [getInstance 创建单例]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-23T20:44:29+0800
     * @return                              [object] [object]
     */
    static public function getMain($select = 0)
    {
        if(!(self::$_main instanceof self))
        {
            $config = new Yaf\Config\Ini(dirname(__FILE__).'/../../config.ini');
            self::$_main = new self($config->redisMain,$select);
        }
        return self::$_main;
    }
    static public function getFollow($select = 0)
    {
     if(!(self::$_follow instanceof self))
        {
            $config = new Yaf\Config\Ini(dirname(__FILE__).'/../../config.ini');
            self::$_follow = new self($config->redisFollw,$select);
        }
        return self::$_follow;
    }

    /**
     * [setStr 设定一条String]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:06:15+0800
     * @param                               [type] $key    [String]
     * @param                               [type] $text   [String]
     * @param                               [type] $expire [Boole]
     */
    public function setStr($key, $text, $expire = null)
    {
        $key = 'string:' . $key;
        $this->_redis->set($key, $text);
        if (!is_null($expire)) {
            $this->_redis->setTimeout($key, $expire);
        }
    }
       /**
        * [getStr 获取一条String]
        * @author         Shaowei Pu <pushaowei@sporte.cn>
        * @CreateTime    2016-12-24T16:13:25+0800
        * @param                               [type] $key [String]
        * @return                              [type]      [Data]
        */
    public function getStr($key)
    {
        $key = 'string:' . $key;
        $text = $this->_redis->get($key);
        return empty($text) ? null : $text;
    }

       /**
        * [delStr 删除一条String]
        * @author         Shaowei Pu <pushaowei@sporte.cn>
        * @CreateTime    2016-12-24T16:13:51+0800
        * @param                               [type] $key [String]
        * @return                              [type]      [Boole]
        */
    public function delStr($key)
    {
        $key = 'string:' . $key;
        $this->_redis->del($key);
    }
    /**
     * [setHash 设置一条Hash]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:14:38+0800
     * @param                               [type] $key    [String]
     * @param                               [type] $arr    [String || Array]
     * @param                               [type] $expire [description]
     */
    public function setHash($key='', $arr='', $expire = null)
    {
        if(empty($key) || empty($arr)) return false;
        
        $key = 'hash:' . $key;
        $this->_redis->hMset($key, $arr);
        if (!is_null($expire)) {
            $this->_redis->setTimeout($key, $expire);
        }
    }
    /**
     * [getHash 获取一条Hash]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:15:09+0800
     * @param                               [type] $key    [String]
     * @param                               [type] $fields [String || Array]
     * @return                              [type]         [Data]
     */
    public function getHash($key, $fields = null)
    {
        $key = 'hash:' . $key;
        if (is_null($fields)) {
            $arr = $this->_redis->hGetAll($key);
        } else {
            if (is_array($fields)) {
                $arr = $this->_redis->hmGet($key, $fields);
                foreach ($arr as $key => $value) {
                    if ($value === false) {
                        unset($arr[$key]);
                    }
                }
            } else {
                $arr = $this->_redis->hGet($key, $fields);
            }
        }
        return empty($arr) ? null : (is_array($arr) ? $arr : array($fields => $arr));
    }
    /**
     * [delHash 删除一条Hash]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:15:38+0800
     * @param                               [type] $key   [String]
     * @param                               [type] $field [String]
     * @return                              [type]        [Boole]
     */
    public function delHash($key, $field = null)
    {
        $key = 'hash:' . $key;
        if (is_null($field)) {
            $this->_redis->del($key);
        } else {
            $this->_redis->hDel($key, $field);
        }
    }
    /**
     * [fieldAddVal 在Hash的field内增加一个值(值之间使用“,”分隔)]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:16:50+0800
     * @param                               [type] $key   [String]
     * @param                               [type] $field [String]
     * @param                               [type] $val   [String]
     * @return                              [type]        [Boole]
     */
    public function fieldAddVal($key, $field, $val)
    {
        $arr = $this->getHash($key, $field);
        if (!is_null($arr)) {
            $str = reset($arr);
            $arr = explode(',', $str);
            foreach ($arr as $v) {
                if ($v == $val) {
                    return;
                }
            }
            $str .= ",{$val}";
            $this->setHash($key, array($field => $str));
        } else {
            $this->setHash($key, array($field => $val));
        }
    }
    /**
     * [fieldDelVal 在Hash的field内删除一个值]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:17:24+0800
     * @param                               [type] $key   [String]
     * @param                               [type] $field [String]
     * @param                               [type] $val   [String]
     * @return                              [type]        [Boole]
     */
    public function fieldDelVal($key, $field, $val)
    {
        $arr = $this->getHash($key, $field);
        if (!is_null($arr)) {
            $arr = explode(',', reset($arr));
            $tmpStr = '';
            foreach ($arr as $v) {
                if ($v != $val) {
                    $tmpStr .= ",{$v}";
                }
            }
            if ($tmpStr == '') {
                $this->delHash($key, $field);
            } else {
                $this->setHash($key, array($field => substr($tmpStr, 1)));
            }
        }
    }
    /**
     * [setTableRow 设置表格的一行数据]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:17:35+0800
     * @param                               [type] $table  [String]
     * @param                               [type] $id     [String]
     * @param                               [type] $arr    [String]
     * @param                               [type] $expire [Boole]
     */
    public function setTableRow($table, $id, $arr, $expire = null)
    {
        $key = '' . $table . ':' . $id;
        $this->_redis->hMset($key, $arr);
        if (!is_null($expire)) {
            $this->_redis->setTimeout($key, $expire);
        }
    }
    /**
     * [getTableRow 获取表格的一行数据]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:17:54+0800
     * @param                               [type] $table  [String]
     * @param                               [type] $id     [String]
     * @param                               [type] $fields [String || Array]
     * @return                              [type]         [Data]
     */
    public function getTableRow($table, $id, $fields = null)
    {
        $key = '' . $table . ':' . $id;
        if (is_null($fields)) {
            $arr = $this->_redis->hGetAll($key);
        } else {
            if (is_array($fields)) {
                $arr = $this->_redis->hmGet($key, $fields);
                foreach ($arr as $key => $value) {
                    if ($value === false) {
                        unset($arr[$key]);
                    }
                }
            } else {
                $arr = $this->_redis->hGet($key, $fields);
            }
        }
        return empty($arr) ? null : (is_array($arr) ? $arr : array($fields => $arr));
    }
    /**
     * [delTableRow 删除表格的一行数据]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:18:31+0800
     * @param                               [type] $table [String]
     * @param                               [type] $id    [String]
     * @return                              [type]        [Boole]
     */
    public function delTableRow($table, $id)
    {
        $key = '' . $table . ':' . $id;
        $this->_redis->del($key);
    }
    /**
     * [pushList 推送一条数据至列表,头部]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:18:40+0800
     * @param                               [type] $key [String]
     * @param                               [type] $arr [String]
     * @return                              [type]      [Boole]
     */
    public function pushList($key, $arr)
    {
        $key = 'list:' . $key;
        $this->_redis->lPush($key, json_encode($arr));
    }
    /**
     * [pullList 从列表拉取一条数据,尾部(堵塞时间受限于php的default_socket_timeout)]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:18:52+0800
     * @param                               [type]  $key     [String]
     * @param                               integer $timeout [String]
     * @return                              [type]           [Boole]
     */
    public function pullList($key, $timeout = 0)
    {
        $key = 'list:' . $key;
        if ($timeout > 0) {
            $val = $this->_redis->brPop($key, $timeout); // 该函数返回的是一个数组, 0=key 1=value
        } else {
            $val = $this->_redis->rPop($key);
        }
        $val = is_array($val) && isset($val[1]) ? $val[1] : $val;
        return empty($val) ? null : $this->objectToArray(json_decode($val));
    }
    /**
     * [getListSize 取得列表的数据总条数]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:19:19+0800
     * @param                               [type] $key [String]
     * @return                              [type]      [Data]
     */
    public function getListSize($key)
    {
        $key = 'list:' . $key;
        return $this->_redis->lSize($key);
    }
    /**
     * [delList 删除列表]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:19:35+0800
     * @param                               [type] $key [String]
     * @return                              [type]      [Boole]
     */
    public function delList($key)
    {
        $key = 'list:' . $key;
        $this->_redis->del($key);
    }
    /**
     * [objectToArray 用递归,将stdClass转为array]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-24T16:19:44+0800
     * @param                               [type] $obj [obj]
     * @return                              [type]      [obj]
     */
    protected function objectToArray($obj)
    {
        if (is_object($obj)) {
            $arr = (array) $obj;
        }
        if (is_array($obj)) {
            foreach ($obj as $key => $value) {
                $arr[$key] = $this->objectToArray($value);
            }
        }
        return !isset($arr) ? $obj : $arr;
    }
    /**
     * [__call 其他属性]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-26T16:31:32+0800
     * @param                               [type] $name  [description]
     * @param                               [type] $param [description]
     * @return                              [type]        [description]
     */
    public function __call($name,$param)
    {
        if(method_exists($this->_redis,$name))
        {
            call_user_func_array(array($this->_redis, $name), $param);
        }
    }

    /**
     * [getStatus 获取当前实例状态]
     * @author         Shaowei Pu <pushaowei@sporte.cn>
     * @CreateTime    2016-12-23T21:01:54+0800
     * @return                              [type] [Boole]
     */
    public function getStatus()
    {
        return is_resource($this->_redis->socket) && ($this->_redis->ping() == '+PONG');
    }
}

在单机情况下很简单,在redis中使用一个hash或时间索引存储一下抢购状态

如:snatch1807211616:0,
当收到抢购请求的时候查询状态,如果没有用户则进行更新:
snatch1807211616:["userid1"] ,

如果支持多用户,就分两个分别存抢购的人数,成功用户id
snatch1807211616:2 , list1807211616:["userid1","userid2"]

一旦超过限制,直接返回失败。

成功的用户,进行数据库操作。

redis实现了一个异步库,有人把它拿出来了叫libae,感兴趣可以看一下代码,GitHub上有。另外,我觉得,4000w的并发,redis也hold不住吧

自己看REDIS的源码吧。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题