page contents

PHP+Redis实现延迟任务,实现自动取消与完成订单

我们应该每次都使用exit退出,而不应该每次在nohup执行成功后直接关闭终端。这样才能保证命令一直在后台运行。

attachments-2020-06-cdd2K7wj5ef94727b3f6e.png


简单定时任务解决方案:使用redis的keyspace notifications(键失效后通知事件) ;

(A)业务场景:

1、当一个业务触发以后需要启动一个定时任务,在指定时间内再去执行一个任务(如自动取消订单,自动完成订单等功能)

2、redis的keyspace notifications 会在key失效后发送一个事件,监听此事件的的客户端就可以收到通知

(B)服务准备:

1、修改reids配置文件(redis.conf)【window系统配置文件为:redis.windows.conf 】

redis默认不会开启keyspace notifications,因为开启后会对cpu有消耗

备注: E:keyevent事件,事件以__keyevent@<db>__为前缀进行发布;

x:过期事件,当某个键过期并删除时会产生该事件;

原配置为:

notify-keyspace-events ""

更改 配置如下:

notify-keyspace-events "Ex"

保存配置后,重启Redis服务,使配置生效

[root@chokingwin etc]#
service redis-server restart /usr/local/redis/etc/redis.conf 
Stopping redis-server: [ OK ] 
Starting redis-server: [ OK ]

window系统重启redis ,先切换到redis文件目录,然后关闭redis服务(redis-server --service-stop),再开启(redis-server --service-start)

v2-af84a71b294ffaadcb493031e1b2fba5_720w.jpg

(C)文件代码:

phpredis实现订阅Keyspace notification,可实现自动取消订单,自动完成订单。以下为测试例子

创建4个文件,然后自行修改数据库和redis配置参数

db.class.php

<?php
class mysql
{
    private $mysqli;
    private $result;
    /**
     * 数据库连接
     * @param $config 配置数组
     */

    public function connect()
    {
        $config=array(
            'host'=>'127.0.0.1',
            'username'=>'root',
            'password'=>'168168',
            'database'=>'test',
            'port'=>3306,
        );

        $host = $config['host'];    //主机地址
        $username = $config['username'];//用户名
        $password = $config['password'];//密码
        $database = $config['database'];//数据库
        $port = $config['port'];    //端口号
        $this->mysqli = new mysqli($host, $username, $password, $database, $port);

    }
    /**
     * 数据查询
     * @param $table 数据表
     * @param null $field 字段
     * @param null $where 条件
     * @return mixed 查询结果数目
     */
    public function select($table, $field = null, $where = null)
    {
        $sql = "SELECT * FROM `{$table}`";
        //echo $sql;exit;
        if (!empty($field)) {
            $field = '`' . implode('`,`', $field) . '`';
            $sql = str_replace('*', $field, $sql);
        }
        if (!empty($where)) {
            $sql = $sql . ' WHERE ' . $where;
        }


        $this->result = $this->mysqli->query($sql);

        return $this->result;
    }
    /**
     * @return mixed 获取全部结果
     */
    public function fetchAll()
    {
        return $this->result->fetch_all(MYSQLI_ASSOC);
    }
    /**
     * 插入数据
     * @param $table 数据表
     * @param $data 数据数组
     * @return mixed 插入ID
     */
    public function insert($table, $data)
    {
        foreach ($data as $key => $value) {
            $data[$key] = $this->mysqli->real_escape_string($value);
        }
        $keys = '`' . implode('`,`', array_keys($data)) . '`';
        $values = '\'' . implode("','", array_values($data)) . '\'';
        $sql = "INSERT INTO `{$table}`( {$keys} )VALUES( {$values} )";
        $this->mysqli->query($sql);
        return $this->mysqli->insert_id;
    }
    /**
     * 更新数据
     * @param $table 数据表
     * @param $data 数据数组
     * @param $where 过滤条件
     * @return mixed 受影响记录
     */
    public function update($table, $data, $where)
    {
        foreach ($data as $key => $value) {
            $data[$key] = $this->mysqli->real_escape_string($value);
        }
        $sets = array();
        foreach ($data as $key => $value) {
            $kstr = '`' . $key . '`';
            $vstr = '\'' . $value . '\'';
            array_push($sets, $kstr . '=' . $vstr);
        }
        $kav = implode(',', $sets);
        $sql = "UPDATE `{$table}` SET {$kav} WHERE {$where}";

        $this->mysqli->query($sql);
        return $this->mysqli->affected_rows;
    }
    /**
     * 删除数据
     * @param $table 数据表
     * @param $where 过滤条件
     * @return mixed 受影响记录
     */
    public function delete($table, $where)
    {
        $sql = "DELETE FROM `{$table}` WHERE {$where}";
        $this->mysqli->query($sql);
        return $this->mysqli->affected_rows;
    }
}

index.php

<?php

require_once 'Redis2.class.php';

$redis = new \Redis2('127.0.0.1','6379','','15');
$order_sn   = 'SN'.time().'T'.rand(10000000,99999999);

$use_mysql = 1;         //是否使用数据库,1使用,2不使用
if($use_mysql == 1){
   /*
    *   //数据表
    *   CREATE TABLE `order` (
    *      `ordersn` varchar(255) NOT NULL DEFAULT '',
    *      `status` varchar(255) NOT NULL DEFAULT '',
    *      `createtime` varchar(255) NOT NULL DEFAULT '',
    *      `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
    *       PRIMARY KEY (`id`)
    *   ) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4;
   */
    require_once 'db.class.php';
    $mysql      = new \mysql();
    $mysql->connect();
    $data       = ['ordersn'=>$order_sn,'status'=>0,'createtime'=>date('Y-m-d H:i:s',time())];
    $mysql->insert('order',$data);
}

$list = [$order_sn,$use_mysql];
$key = implode(':',$list);

$redis->setex($key,3,'redis延迟任务');      //3秒后回调



$test_del = false;      //测试删除缓存后是否会有过期回调。结果:没有回调
if($test_del == true){
    //sleep(1);
    $redis->delete($order_sn);
}

echo $order_sn;



/*
 *   测试其他key会不会有回调,结果:有回调
 *   $k = 'test';
 *   $redis2->set($k,'100');
 *   $redis2->expire($k,10);
 *
*/


psubscribe.php

<?php
ini_set('default_socket_timeout', -1);  //不超时
require_once 'Redis2.class.php';
$redis_db = '15';
$redis = new \Redis2('127.0.0.1','6379','',$redis_db);
// 解决Redis客户端订阅时候超时情况
$redis->setOption();
//当key过期的时候就看到通知,订阅的key __keyevent@<db>__:expired 这个格式是固定的,db代表的是数据库的编号,由于订阅开启之后这个库的所有key过期时间都会被推送过来,所以最好单独使用一个数据库来进行隔离
$redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), 'keyCallback');
// 回调函数,这里写处理逻辑
function keyCallback($redis, $pattern, $channel, $msg)
{
    echo PHP_EOL;
    echo "Pattern: $pattern\n";
    echo "Channel: $channel\n";
    echo "Payload: $msg\n\n";
    $list = explode(':',$msg);

    $order_sn = isset($list[0])?$list[0]:'0';
    $use_mysql = isset($list[1])?$list[1]:'0';

    if($use_mysql == 1){
        require_once 'db.class.php';
        $mysql = new \mysql();
        $mysql->connect();
        $where = "ordersn = '".$order_sn."'";
        $mysql->select('order','',$where);
        $finds=$mysql->fetchAll();
        print_r($finds);
        if(isset($finds[0]['status']) && $finds[0]['status']==0){
            $data   = array('status' => 3);
            $where  = " id = ".$finds[0]['id'];
            $mysql->update('order',$data,$where);
        }
    }

}

//或者
/*$redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), function ($redis, $pattern, $channel, $msg){
    echo PHP_EOL;
    echo "Pattern: $pattern\n";
    echo "Channel: $channel\n";
    echo "Payload: $msg\n\n";
    //................
});*/


Redis2.class.php

<?php

class Redis2
{
    private $redis;

    public function __construct($host = '127.0.0.1', $port = '6379',$password = '',$db = '15')
    {
        $this->redis = new Redis();
        $this->redis->connect($host, $port);    //连接Redis
        $this->redis->auth($password);      //密码验证
        $this->redis->select($db);    //选择数据库
    }

    public function setex($key, $time, $val)
    {
        return $this->redis->setex($key, $time, $val);
    }

    public function set($key, $val)
    {
        return $this->redis->set($key, $val);
    }

    public function get($key)
    {
        return $this->redis->get($key);
    }

    public function expire($key = null, $time = 0)
    {
        return $this->redis->expire($key, $time);
    }

    public function psubscribe($patterns = array(), $callback)
    {
        $this->redis->psubscribe($patterns, $callback);
    }

    public function setOption()
    {
        $this->redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
    }

    public function lRange($key,$start,$end)
    {
        return $this->redis->lRange($key,$start,$end);
    }

    public function lPush($key, $value1, $value2 = null, $valueN = null ){
        return $this->redis->lPush($key, $value1, $value2 = null, $valueN = null );
    }

    public function delete($key1, $key2 = null, $key3 = null)
    {
        return $this->redis->delete($key1, $key2 = null, $key3 = null);
    }

}


window系统测试方法:先在cmd命令界面运行psubscribe.php,然后网页打开index.php。3秒后效果如下

v2-8cab5496ca83db6e85042a1b41bd0347_720w.jpg


使监听后台始终运行(订阅)

有个问题 做到这一步,利用 phpredis 扩展,成功在代码里实现对过期 Key 的监听,并在 psCallback()里进行回调处理。 开头提出的两个需求已经实现。 可是这里有个问题:redis 在执行完订阅操作后,终端进入阻塞状态,需要一直挂在那。且此订阅脚本需要人为在命令行执行,不符合实际需求。

实际上,我们对过期监听回调的需求,是希望它像守护进程一样,在后台运行,当有过期事件的消息时,触发回调函数。 使监听后台始终运行 希望像守护进程一样在后台一样,

我是这样实现的。

Linux中有一个nohup命令。功能就是不挂断地运行命令。 同时nohup把脚本程序的所有输出,都放到当前目录的nohup.out文件中,如果文件不可写,则放到<用户主目录>/nohup.out 文件中。那么有了这个命令以后,不管我们终端窗口是否关闭,都能够让我们的php脚本一直运行。

编写psubscribe.php文件:

<?php
#! /usr/bin/env php
ini_set('default_socket_timeout', -1);  //不超时
require_once 'Redis2.class.php';
$redis_db = '15';
$redis = new \Redis2('127.0.0.1','6379','',$redis_db);
// 解决Redis客户端订阅时候超时情况
$redis->setOption();
//当key过期的时候就看到通知,订阅的key __keyevent@<db>__:expired 这个格式是固定的,db代表的是数据库的编号,由于订阅开启之后这个库的所有key过期时间都会被推送过来,所以最好单独使用一个数据库来进行隔离
$redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), 'keyCallback');
// 回调函数,这里写处理逻辑
function keyCallback($redis, $pattern, $channel, $msg)
{
    echo PHP_EOL;
    echo "Pattern: $pattern\n";
    echo "Channel: $channel\n";
    echo "Payload: $msg\n\n";
    $list = explode(':',$msg);

    $order_sn = isset($list[0])?$list[0]:'0';
    $use_mysql = isset($list[1])?$list[1]:'0';

    if($use_mysql == 1){
        require_once 'db.class.php';
        $mysql = new \mysql();
        $mysql->connect();
        $where = "ordersn = '".$order_sn."'";
        $mysql->select('order','',$where);
        $finds=$mysql->fetchAll();
        print_r($finds);
        if(isset($finds[0]['status']) && $finds[0]['status']==0){
            $data   = array('status' => 3);
            $where  = " id = ".$finds[0]['id'];
            $mysql->update('order',$data,$where);
        }
    }

}


//或者
/*$redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), function ($redis, $pattern, $channel, $msg){
    echo PHP_EOL;
    echo "Pattern: $pattern\n";
    echo "Channel: $channel\n";
    echo "Payload: $msg\n\n";
    //................
});*/


注意:我们在开头,申明 php 编译器的路径:

#! /usr/bin/env php

这是执行 php 脚本所必须的。

  • 发表于 2020-06-29 09:44
  • 阅读 ( 590 )
  • 分类:PHP开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
Pack
Pack

1135 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. 小柒 1658 文章
  3. Pack 1135 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章