Using PHP+Redis to delay tasks and automatically cancel orders

Keywords: PHP Redis MySQL SQL

Simple timing task solution: when using redis's keyspace notifications, you need to note that this function is launched after redis version 2.8, so the reids on your server should be at least 2.8 or above;

(A) business scenario:

1. When a business is triggered, a scheduled task needs to be started, and another task needs to be executed within the specified time (such as auto cancel order, auto complete order, etc.)

2. redis's keyspace notifications will send an event after the key fails. The client listening to this event will receive the notification

(B) service preparation:

1. Modify the reids configuration file (redis.conf) [the window system configuration file is redis.windows.conf]

redis will not turn on keyspace notifications by default, because it will consume cpu

Note: E: keyevent event, which is published with the prefix of \\\\\\\\\;

x: expiration event. It will be generated when a key expires and is deleted;

The original configuration was:

notify-keyspace-events ""

Change the configuration as follows:

notify-keyspace-events "Ex"

After saving the configuration, restart the Redis service to make the configuration effective

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

Windows system restarts redis, first switches to the redis file directory, then turns off the redis service (redis server -- service stop), and then turns on (redis server -- service start)

(C) document code:

phpredis can subscribe to Keyspace notification, automatically cancel orders and complete orders. Here is an example of the test

Create 4 files, and then modify the database and redis configuration parameters by yourself

db.class.php

<?php
class mysql
{

private $mysqli;
private $result;
/**
 * Database connection
 * @param $config Configuration array
 */

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

    $host = $config['host'];    //Host address
    $username = $config['username'];//User name
    $password = $config['password'];//Password
    $database = $config['database'];//data base
    $port = $config['port'];    //Port number
    $this->mysqli = new mysqli($host, $username, $password, $database, $port);

}
/**
 * Data query
 * @param $table Data sheet
 * @param null $field field
 * @param null $where condition
 * @return mixed Number of query results
 */
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 Get all results
 */
public function fetchAll()
{
    return $this->result->fetch_all(MYSQLI_ASSOC);
}
/**
 * insert data
 * @param $table Data sheet
 * @param $data Data array
 * @return mixed Insert 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;
}
/**
 * Update data
 * @param $table Data sheet
 * @param $data Data array
 * @param $where Filter condition
 * @return mixed Affected records
 */
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;
}
/**
 * Delete data
 * @param $table Data sheet
 * @param $where Filter condition
 * @return mixed Affected records
 */
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; / / use database, 1 use, 2 do not use
if($use_mysql == 1){
/*

*   //Data sheet
*   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 delay task'); / / callback after 3 seconds

$test_del = false; / / test whether there will be expired callbacks after deleting the cache. Result: no callback
if($test_del == true){

//sleep(1);
$redis->delete($order_sn);

}

echo $order_sn;

/*

  • Test whether there is callback for other key s. Result: there is callback
  • $k = 'test';
  • $redis2->set($k,'100');
  • $redis2->expire($k,10);

*
*/

psubscribe.php

<?php
Ini set ('default socket timeout '- 1); / / no timeout
require_once 'Redis2.class.php';
$redis_db = '15';
$redis = new \Redis2('127.0.0.1','6379','',$redis_db);
//Resolve the timeout situation when the Redis client subscribes
$redis->setOption();
//When the key expires, you will see the notification. The subscription key \\\\\\\\\\\\\\
$redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), 'keyCallback');
//Callback function, write processing logic here
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);
    }
}

}

/ / or
/*$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);    //Connect Redis
    $this->redis->auth($password);      //Password verification
    $this->redis->select($db);    //Select database
}

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 system test method: first run psubscribe.php in the cmd command interface, and then open index.php on the web page. After 3 seconds, the effect is as follows

Keep listening background running (subscription)
There is a problem to do this step. By using the phpredis extension, the expired Key is successfully monitored in the code, and the callback is processed in psCallback(). The first two requirements have been implemented. But there is a problem here: after the subscription operation of redis is completed, the terminal enters the 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 the actual needs.

In fact, our requirement for expiration monitoring callbacks is that they run in the background like daemons, and trigger the callback function when there are messages of expiration events. Make the listening background always run like a daemons in the background,

This is how I realized 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 into the file nohup.out in the current directory. If the file is not writable, it is put into the file "user Home Directory > / nohup.out". So with this command, whether our terminal window is closed or not, our php script can run all the time.

Write psubscribe.php file:

<?php

! /usr/bin/env php

Ini set ('default socket timeout '- 1); / / no timeout
require_once 'Redis2.class.php';
$redis_db = '15';
$redis = new \Redis2('127.0.0.1','6379','',$redis_db);
//Resolve the timeout situation when the Redis client subscribes
$redis->setOption();
//When the key expires, you will see the notification. The subscription key \\\\\\\\\\\\\\
$redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), 'keyCallback');
//Callback function, write processing logic here
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);
    }
}

}

/ / or
/*$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";
//................

});*/

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

! /usr/bin/env php

This is required to execute php scripts.

Then, nohup does not suspend the execution of psubscribe.php, note the following&

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

Note: the script did run on process 4456.

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

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

Run index.php, and the effect will be successful after 3 seconds

Problem encountered: use command line mode to start monitoring script, and error will be reported after a period of time: Error while sending QUERY packet. PID=xxx

Solution: because the waiting message queue is a long connection, and there is a database connection before the waiting callback, the wait timeout = 28800 of the database, so as long as the next message is more than 8 hours away from the previous message, this error will appear. Set the wait timeout to 10, and catch the exception, and find that the real error is MySQL server has gone away,
So as long as the database connection is actively closed after all business logic is processed, that is, the database connection is actively closed, the problem can be solved

The yii solution is as follows:

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

View process method:

ps -aux|grep psubscribe.php

a: show all programs
u: display in user oriented format
x: display all programs, not distinguished by terminals

View job 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

To terminate a background running process:

kill -9 process number

To empty the nohup.out file:

cat /dev/null > nohup.out

When we use nohup, we usually use it with & but in the actual use process, many people just hang up the program in the background. In fact, it is possible that when the current account exits or ends abnormally, the command will end by itself.

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

1. Enter first, and exit the prompt of nohup.

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 instead of shutting down the terminal every time the nohup is executed successfully. This ensures that the command runs in the background all the time.

Many PHPer will encounter some problems and bottlenecks when they are advanced. There is no sense of direction when they write too much business code. I don't know where to start to improve. For this, I collated some data, including but not limited to: distributed architecture, high scalability, high performance, high concurrency, server performance tuning, TP6, laravel, YII2, Redis, Swoole, Swoft, Kafka, Mysql optimization, s You can share what you need for advanced dry goods free of charge, such as shell script, Docker, microservice, Nginx, etc Please poke here.

Posted by Sanjib Sinha on Wed, 20 Nov 2019 00:18:31 -0800