Use PHP+Redis to implement delayed tasks and automatically cancel orders

Simple timing task solution: use redis keyspace notifications (notify events after key failure). Note that this function was launched after redis version 2.8, so the reids on your server must be at least version 2.8 or higher;
(A) Business scenario:

  1. When a business is triggered, it is necessary to start a timed task, and then perform a task within a specified time (such as automatic order cancellation, automatic order completion, etc.)
  2. The keyspace notifications of redis will send an event after the key fails, and the client that listens to this event can receive the notification

(B) Service preparation:

Modify the redis configuration file (redis.conf) [window system configuration file is: redis.windows.conf] Redis
does not enable keyspace notifications by default, because it will consume the cpu after opening.
Note: E: keyevent event, the event is __keyevent@ __ is the prefix for publishing;
x: expiration event, which will be generated when a key expires and is deleted; the

original configuration is:

notify-keyspace-events “”

Change the configuration as follows:

notify-keyspace-events “Ex”

After saving the configuration, restart the Redis service to make the configuration take effect

[root@chokingwin etc]# service redis-server restart
/usr/local/redis/etc/redis.conf Stopping redis-server: [OK]
Starting redis-server: [OK] window The system restarts redis
, first switch to the redis file Directory, then close the redis service (redis-server
--service-stop), and then open (redis-server --service-start)

C) File code:

  • phpredis realizes subscribing to Keyspace notification, which can realize automatic cancellation of orders and automatic completion of orders. The following is a test example

    Create 4 files, then modify the database and redis configuration parameters
    db.class.php


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

    public function connect()
    {
        $config=array(
            'host'=>'172.17.0.1',
            'username'=>'root',
            'password'=>'123456',
            'database'=>'learn_knowleadge',
        );
        $host = $config['host']; //主机地址
        $username = $config['username'];//用户名
        $password = $config['password'];//密码
        $database = $config['database'];//数据库
        $this->mysqli = new mysqli($host, $username, $password, $database);
    }
    /**
     * 数据查询
     * @param $table 数据表
     * @param null $field 字段
     * @param null $where 条件
     * @return mixed 查询结果数目
     */
    public function select($table, $field = null, $where = null)
    {
        $sql = "SELECT * FROM `{$table}`";
        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_array();
//        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;
    }
}

Redis.2.class.php


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

    public function connect()
    {
        $config=array(
            'host'=>'172.17.0.1',
            'username'=>'root',
            'password'=>'123456',
            'database'=>'learn_knowleadge',
        );
        $host = $config['host']; //主机地址
        $username = $config['username'];//用户名
        $password = $config['password'];//密码
        $database = $config['database'];//数据库
        $this->mysqli = new mysqli($host, $username, $password, $database);
    }
    /**
     * 数据查询
     * @param $table 数据表
     * @param null $field 字段
     * @param null $where 条件
     * @return mixed 查询结果数目
     */
    public function select($table, $field = null, $where = null)
    {
        $sql = "SELECT * FROM `{$table}`";
        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_array();
//        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('172.17.0.1','6379','','15');
$order_sn = 'SN'.time().'T'.rand(10000000,99999999);

$use_mysql = 1; //是否使用数据库,1使用,2不使用
if($use_mysql == 1){
    require_once 'db.class.php';
    $mysql = new mysql();
    $mysql->connect();
    $data = array('ordersn'=>$order_sn,'status'=>0,'createtime'=>date('Y-m-d H:i:s',time()));
    $mysql->insert('order',$data);
}
$list = array($order_sn,$use_mysql);
$key = implode(':',$list);
$redis->setex($key,10,'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('172.17.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['status']) && $finds['status']==0){
            $data = array('status' => 3);
            $where = " id = ".$finds['id'];
            $mysql->update('order',$data,$where);
        }
    }
}

Window system test method: first run psubscribe.php in the cmd command interface, and then open index.php on the web page.

Make the listening background always run (subscription)

There is a problem to do this step. Use phpredis extension to successfully monitor the expired Key in the code, and perform callback processing in psCallback(). The two requirements mentioned at the beginning have been fulfilled. But there is a problem here: after redis finishes the subscription operation, the terminal enters a blocking state and needs to hang there all the time. And this subscription script needs to be executed manually on the command line, which does not meet actual needs.

In fact, our requirement for overdue monitoring callbacks is that we want it to run in the background like a daemon, and trigger the callback function when there is an overdue event message. Make the listening background always run, hoping to be in the background like a daemon,

This is how I achieved it.

There is a nohup command in Linux. The function is to run commands without hanging up. At the same time, nohup puts all the output of the script program in the nohup.out file in the current directory. If the file is not writable, put it in the <user home directory>/nohup.out file. So with this command, no matter whether our terminal window is closed or not, we can keep our php script running.

Note : At the beginning, we declare the path of the php compiler:

#! /usr/bin/env php

This is necessary to execute php scripts.

Then, nohup does not hang to execute psubscribe.php, pay attention to the & at the end

[root@chokingwin HiGirl]# nohup ./psubscribe.php & [1] 4456 nohup:
ignoring input and appending output to `nohup.out’

Explanation: The script has indeed been run on the 4456 process.

Check nohup.out cat Check nohuo.out to see if there is outdated output:

[root@chokingwin HiGirl]# cat nohup.out
Pattern:keyevent@0:expired Channel: keyevent@0:expired
Payload: name

Run index.php, after 3 seconds, the effect is as above and it is successful

Encountered a problem: use the command line mode to open the monitoring script, after a period of time an error is reported: Error while sending QUERY packet. PID=xxx

Solution: Because the waiting message queue is a long connection, and there is a database connection before waiting for the callback, the database's wait_timeout=28800, so as long as the next message is more than 8 hours away from the previous message, this error will occur. Set wait_timeout to 10, and caught the exception, found that the real error is that MySQL server has gone away,
so as long as all business logic is processed, the database connection is actively closed, that is, the database connection is actively closed to solve the problem

The yii solution is as follows:

Yii::$app->db->close();

View process method:

ps -aux | grep psubscribe.php

a: Display all programs
u : Display all programs in a user-oriented format
x: Display all programs, not distinguished by terminal.
View jobs process ID: [jobs -l] command

www@iZ232eoxo41Z:~/tinywan $ jobs -l [1]- 1365 Stopped (tty output)
sudo nohup psubscribe.php > /dev/null 2>&1 [2]+ 1370 Stopped (tty
output) sudo nohup psubscribe.php > /dev/null 2>&1

Method to terminate the process running in the background:

kill -9 process number

Clear nohup.out file method:

cat /dev/null> nohup.out When
we use nohup, we usually use it with &, but in actual use, many people hang up the program in the background and just ignore it. In fact, it may be abnormal in the current account. When exiting or ending, the command ends by itself.

So after using the nohup command to run the command in the background, we need to do the following:

  1. Press Enter to exit the nohup prompt.
  2. Then execute exit to exit the current account normally.
  3. Then go to the link terminal. Make the program run normally in the background.

We should use exit every time to exit, instead of closing the terminal every time after nohup is executed successfully. This will ensure that the command has been running in the background.

Guess you like

Origin blog.csdn.net/kevlin_V/article/details/104540422