PHP使用 Redis做消息队列 秒杀案例

消息队列:我的理解是:排队执行的一些处理 比如任务队列

秒杀杂谈: 1. redis incr 使用原子计数可以解决, 并发情况下不会超发的问题 2. 用户锁, 解决同一用户同时多次秒杀, 除非你不 care 一个用户中了多次奖品 3. 防人机, 简单的方案是图形码 或者是 CSRF 都可以 1. 比如 redis incr ,因为是原子操作,你不需要担心并发的问题,你只需要判断每次 incr 后的值是否小于等于 10. 2. 使用有限资源的模式,比如使用 redis list ,先创建好 10 个资源,然后每次操作都是 pop ,因为只有 10 个资源,不会 pop 出来 11 个资源的,谁拿到资源谁就中奖。

Redis的消息队列(N年前的项目代码)

big_cat big_cat 发布于 2017/06/12 11:20 字数 401 阅读 199 收藏 0 点赞 0 评论 0 redis message queenredis消息队列 代码如下

工具库 tools.inc.php

<?php
/**
 * sys tools
 */

/**
 * [get_redis description]
 * @return [type] [description]
 */
function get_redis()
{
    $host = "127.0.0.1";
    $port = 6679;
    $pwd  = "password";

    try {
        $redis = new Redis();
        $redis->connect($host, $port);

        if ($pwd) {
            $result = $redis->auth($pwd);
            if (!$result) {
                log("redis_access", "redis auth failed");
                return false;
            }
        }
        
        return $redis;
    } catch (RedisException $e) {
        //redis 异常日志
        log("redis_access", $e->getMessage());
        return false;
    }
}

/**
 * 日记
 * @param  [type] $name [description]
 * @param  [type] $msg  [description]
 * @return [type]       [description]
 */
function log($name, $msg)
{
    $log_file = $name . date("Y_m_d") . ".log";
    $log_msg  = date("Y-m-d H:i:s") . "\t" . $msg . PHP_EOL;
    file_put_contents($log_file, $log_msg, FILE_APPEND);
}

队列 producer

<?php
/*
 *将收到的消息放入队列
 * 
 */
require_once('tools.inc.php');

$redis = get_redis();

if (!$redis) {
    log("queen.log", "redis connect failed!");
    exit();
}

//入队列
$channel = "queen_name";

$redis->rpush($channel, serialize($message));

队列 consumer

#! /usr/bin/env php
<?php
/*
 |-------------------------------------------------------------------------------------------------
 |$channel 队列名
 |$max_handle_size 每次处理的最大量
 |crontab task run peer 10 seconds
 |-------------------------------------------------------------------------------------------------
 |* * * * * sleep  0; /path/to/queen_handler.php >>/dev/null 2>&1
 |* * * * * sleep 10; /path/to/queen_handler.php >>/dev/null 2>&1
 |* * * * * sleep 20; /path/to/queen_handler.php >>/dev/null 2>&1
 |* * * * * sleep 30; /path/to/queen_handler.php >>/dev/null 2>&1
 |* * * * * sleep 40; /path/to/queen_handler.php >>/dev/null 2>&1
 |* * * * * sleep 50; /path/to/queen_handler.php >>/dev/null 2>&1
 |-------------------------------------------------------------------------------------------------
*/

require_once('tools.inc.php');

$channel = "queen_name";

$redis = get_redis();

if (!$redis) {
    log("queen.log", "redis connect failed!");
    exit();
}

//每次出队的最大值
$max_handle_size = 2000;

$queen_len = $redis->llen($channel);

if ($queen_len == 0) { //队列中没有数据则退出
    exit();
}

if (0 < $queen_len && $queen_len <= $max_handle_size) { //队列长度小于最大入库长度则直接全部入库
    $cursor_cur  = $queen_len - 1; //截断游标
    $cursor_next = $queen_len;
    $message_queen = $redis->lrange($channel, 0, $cursor_cur);
    $redis->ltrim($channel, $cursor_next, -1);
} elseif ($queen_len > $max_handle_size) { //队列长度小于最大入库长度则分批入库
    $cursor_cur  = $max_handle_size - 1;
    $cursor_next = $max_handle_size;
    $message_queen = $redis->lrange($channel, 0, $cursor_cur);
    $redis->ltrim($channel, $cursor_next, -1);
}

// 处理消息队列
$message_arr = array_map(function ($message) {
    return unserialize($message);
}, $message_queen);

// 下面就是你的处理数据的逻辑了
code to process message

转至 https://my.oschina.net/sallency/blog/919235

redis秒杀 基础思路

一 生产者producer部分

--------------------------------producer 部分注释------------------------------------------------------------

用户在页面请求之后, 获取到用户uid , 跳转到这个加入队列的方法 (这里直接在producer中模拟了多个uid)

在方法内部判断redis队列长度是否已经达到要求, 如果没有超出, 则执行加入队列的操作 (这里为了简洁,没有封装成方法)

注: producer.php没有进行数据库的操作,只有接受uid和其他值的操作, 数据库操作一律放在消费者consumer.php中

--------------------------------producer 注释结束-----------------------------------------------------------------------

生产者代码 producer.php:

    <?php
    //连接redis数据库
    $redis = new Redis();
    $redis->connect('127.0.0.1',6379);
    $redis_name = 'secKill3';

    //模拟100人请求秒杀(高压力)
    for ($i = 0; $i < 100; $i++) {
        $uid = rand(10000000, 99999999);
        //获取当前队列已经拥有的数量,如果人数少于十,则加入这个队列
        $num = 10;
        if ($redis->lLen($redis_name) < $num) {
            $redis->rPush($redis_name, $uid);
            echo $uid . "秒杀成功"."<br>";
        } else {
            //如果当前队列人数已经达到10人,则返回秒杀已完成
            echo "秒杀已结束<br>";
        }
    }
    //关闭redis连接
    $redis->close();
	

注: 执行完producer.php文件,本地redis数据库第0号数据库中应该有一个键名为"secKill3"的List队列,像这样

二 消费者consumer部分

------------------------------消费者部分注释---------------------------------------------

消费者一直读取redis数据库中指定队列,一有值,立即取出,并进行相应数据库操作

------------------------------消费者部分注释结束----------------------------------------

消费者代码 consumer.php

<?php
//设置redis数据库连接及键名
$redis = new Redis();
$redis->connect('127.0.0.1');
$key = 'secKill3';//redis数据库key [注:默认redis数据库选择第0号数据库]

//PDO连接mysql数据库
$dsn = "mysql:dbname=test;host=127.0.0.1";
$pdo = new PDO($dsn, 'root', '123456');

 //死循环
//从队列最前头取出一个值,判断这个值是否存在,取出时间和uid,保存到数据库
//数据库插入失败时,要有回滚机制
//注: rpush 和lpop是一对

while(1) {
    //从队列最前头取出一个值
    $uid = $redis->lPop($key);
    //判断值是否存在
    if(!$uid || $uid == 'nil'){
        sleep(2);
        continue;
    }
    //生成订单号
    $orderNum = build_order_no($uid);
    //生成订单时间
    $timeStamp = time();
    //构造插入数组
    $user_data = array('uid'=>$uid,'time_stamp'=>$timeStamp,'order_num'=>$orderNum);
    //将数据保存到数据库
    $sql = "insert into student (uid,time_stamp,order_num) values (:uid,:time_stamp,:order_num)";
    $stmt = $pdo->prepare($sql);
    $res = $stmt->execute($user_data);
    //数据库插入数据失败,回滚
    if(!$res){
        $redis->rPush($key,$uid);
    }
}

//生成唯一订单号
function build_order_no($uid){
    return  substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8).$uid;
}

注: 执行完consumer.php之后,数据库对应数据表应该有数值

到此,秒杀结束

备注:

用到的student数据表结构sql


    CREATE TABLE `student`  (
      `uid` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'uid',
      `username` varchar(20) NOT NULL DEFAULT '',
      `time_stamp` int(11) NOT NULL DEFAULT 0,
      `order_num` bigint(20) UNSIGNED NOT NULL DEFAULT 0,
      PRIMARY KEY (`uid`) USING BTREE,
      key (time_stamp)
    ) ENGINE = MyISAM default charset=utf8;

redis设置、查看和校验密码 Redis没有实现访问控制这个功能,但是它提供了一个轻量级的认证方式(密码),可以通过编辑【redis.conf】配置文件来启用认证,这里简单介绍一下Redis中如何设置、查看和校验密码(登录验证和操作时验证)。

通过配置文件修改Redis的密码

在【redis.conf】配置文件中有一个【requirepass】配置项,这个配置项就是配置Redis访问密码的参数:

requirepass yanggb 注意事项

1.修改访问密码之后要重启Redis。

2.设置的访问密码要尽可能复杂,尽可能长(正式上线)。因为Reids的查询速度是非常快的,如果一个不怀好意的外部用户想要破解密码的话,一秒钟可以尝试150K个密码,这时候只要设置的密码足够复杂足够长,就可以在一定程度上防止密码被轻易破解。

通过命令修改Redis的密码

修改Redis密码的命令是:

config set requirepass yanggb 注意事项

1.通过命令修改访问密码不需要重启Redis(即时生效)。

2.如果【redis.conf】配置文件中的【requirepass】配置项已经被配置,通过命令修改的访问密码会被记录到该参数中,这种情况下修改之后是一直有效的。

3.如果【redis.conf】配置文件中的【requirepass】配置项没有被配置,通过命令修改的访问密码不会被记录到该参数中,这种情况下重启Redis之后密码就会失效。

查询Redis的密码与权限认证

查询Redis密码的命令是:

config get requirepass 不出意外的话,会报无操纵权限的错误:(error) ERR operation not permitted。

这时候需要先进行权限认证(检验密码):

auth yanggb 再次执行前面的命令就能获取到密码了(??我知道密码还要查密码干嘛,神一样的操作)。

登录有密码校验的Redis

有密码校验的Redis需要在登录Redis的时候输入密码:

redis-cli -p 6379 -a yanggb 也可以先登录之后再进行权限认证:

redis-cli -p 6379 auth yanggb 注意事项

1.auth命令和其他Redis提供的命令一样都是没有加密的,阻止不了攻击者在网络上窃取你的密码。

总结

密码认证层的目的是提供多一层简单的保护,当防火墙或者其他用来保护Redis的系统防御外部攻击失败的时候,这层密码校验层就能有效防止无密码用户对Redis的数据访问。

记录你我
请先登录后发表评论
  • latest comments
  • 总共0条评论