PHP+Redis can delay tasks and automatically cancel and complete orders

Simple timing task solution: use redis's keyspace notifications (notify events after key failure);

(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

1 [root@chokingwin etc]#
2 service redis-server restart /usr/local/redis/etc/redis.conf 
3 Stopping redis-server: [ OK ] 
4 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

  1 <?php
  2 class mysql
  3 {
  4     private $mysqli;
  5     private $result;
  6     /**
  7      * Database connection
  8      * @param $config Configuration array
  9      */
 10 
 11     public function connect()
 12     {
 13         $config=array(
 14             'host'=>'127.0.0.1',
 15             'username'=>'root',
 16             'password'=>'168168',
 17             'database'=>'test',
 18             'port'=>3306,
 19         );
 20 
 21         $host = $config['host'];    //Host address
 22         $username = $config['username'];//User name
 23         $password = $config['password'];//Password
 24         $database = $config['database'];//data base
 25         $port = $config['port'];    //Port number
 26         $this->mysqli = new mysqli($host, $username, $password, $database, $port);
 27 
 28     }
 29     /**
 30      * Data query
 31      * @param $table Data sheet
 32      * @param null $field field
 33      * @param null $where condition
 34      * @return mixed Number of query results
 35      */
 36     public function select($table, $field = null, $where = null)
 37     {
 38         $sql = "SELECT * FROM `{$table}`";
 39         //echo $sql;exit;
 40         if (!empty($field)) {
 41             $field = '`' . implode('`,`', $field) . '`';
 42             $sql = str_replace('*', $field, $sql);
 43         }
 44         if (!empty($where)) {
 45             $sql = $sql . ' WHERE ' . $where;
 46         }
 47 
 48 
 49         $this->result = $this->mysqli->query($sql);
 50 
 51         return $this->result;
 52     }
 53     /**
 54      * @return mixed Get all results
 55      */
 56     public function fetchAll()
 57     {
 58         return $this->result->fetch_all(MYSQLI_ASSOC);
 59     }
 60     /**
 61      * insert data
 62      * @param $table Data sheet
 63      * @param $data Data array
 64      * @return mixed Insert ID
 65      */
 66     public function insert($table, $data)
 67     {
 68         foreach ($data as $key => $value) {
 69             $data[$key] = $this->mysqli->real_escape_string($value);
 70         }
 71         $keys = '`' . implode('`,`', array_keys($data)) . '`';
 72         $values = '\'' . implode("','", array_values($data)) . '\'';
 73         $sql = "INSERT INTO `{$table}`( {$keys} )VALUES( {$values} )";
 74         $this->mysqli->query($sql);
 75         return $this->mysqli->insert_id;
 76     }
 77     /**
 78      * Update data
 79      * @param $table Data sheet
 80      * @param $data Data array
 81      * @param $where Filter condition
 82      * @return mixed Affected records
 83      */
 84     public function update($table, $data, $where)
 85     {
 86         foreach ($data as $key => $value) {
 87             $data[$key] = $this->mysqli->real_escape_string($value);
 88         }
 89         $sets = array();
 90         foreach ($data as $key => $value) {
 91             $kstr = '`' . $key . '`';
 92             $vstr = '\'' . $value . '\'';
 93             array_push($sets, $kstr . '=' . $vstr);
 94         }
 95         $kav = implode(',', $sets);
 96         $sql = "UPDATE `{$table}` SET {$kav} WHERE {$where}";
 97 
 98         $this->mysqli->query($sql);
 99         return $this->mysqli->affected_rows;
100     }
101     /**
102      * Delete data
103      * @param $table Data sheet
104      * @param $where Filter condition
105      * @return mixed Affected records
106      */
107     public function delete($table, $where)
108     {
109         $sql = "DELETE FROM `{$table}` WHERE {$where}";
110         $this->mysqli->query($sql);
111         return $this->mysqli->affected_rows;
112     }
113 }

index.php

 1 <?php
 2 
 3 require_once 'Redis2.class.php';
 4 
 5 $redis = new \Redis2('127.0.0.1','6379','','15');
 6 $order_sn   = 'SN'.time().'T'.rand(10000000,99999999);
 7 
 8 $use_mysql = 1;         //Use database or not, 1 use, 2 do not use
 9 if($use_mysql == 1){
10    /*
11     *   //Data sheet
12     *   CREATE TABLE `order` (
13     *      `ordersn` varchar(255) NOT NULL DEFAULT '',
14     *      `status` varchar(255) NOT NULL DEFAULT '',
15     *      `createtime` varchar(255) NOT NULL DEFAULT '',
16     *      `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
17     *       PRIMARY KEY (`id`)
18     *   ) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4;
19    */
20     require_once 'db.class.php';
21     $mysql      = new \mysql();
22     $mysql->connect();
23     $data       = ['ordersn'=>$order_sn,'status'=>0,'createtime'=>date('Y-m-d H:i:s',time())];
24     $mysql->insert('order',$data);
25 }
26 
27 $list = [$order_sn,$use_mysql];
28 $key = implode(':',$list);
29 
30 $redis->setex($key,3,'redis Delayed task');      //3 Second second callback
31 
32 
33 
34 $test_del = false;      //Test whether there will be an expiration callback after deleting the cache. Result: no callback
35 if($test_del == true){
36     //sleep(1);
37     $redis->delete($order_sn);
38 }
39 
40 echo $order_sn;
41 
42 
43 
44 /*
45  *   Test whether there is callback for other key s. Result: there is callback
46  *   $k = 'test';
47  *   $redis2->set($k,'100');
48  *   $redis2->expire($k,10);
49  *
50 */

 

 

psubscribe.php

 1 <?php
 2 ini_set('default_socket_timeout', -1);  //No timeout
 3 require_once 'Redis2.class.php';
 4 $redis_db = '15';
 5 $redis = new \Redis2('127.0.0.1','6379','',$redis_db);
 6 // Solve Redis Timeout during client subscription
 7 $redis->setOption();
 8 //When key You will see the notice when it expires. You can subscribe to it key __keyevent@<db>__:expired This format is fixed, db Represents the number of the database. Since the subscription is enabled, all key Expiration times are pushed, so it's best to use a single database for isolation
 9 $redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), 'keyCallback');
10 // Callback function,Write processing logic here
11 function keyCallback($redis, $pattern, $channel, $msg)
12 {
13     echo PHP_EOL;
14     echo "Pattern: $pattern\n";
15     echo "Channel: $channel\n";
16     echo "Payload: $msg\n\n";
17     $list = explode(':',$msg);
18 
19     $order_sn = isset($list[0])?$list[0]:'0';
20     $use_mysql = isset($list[1])?$list[1]:'0';
21 
22     if($use_mysql == 1){
23         require_once 'db.class.php';
24         $mysql = new \mysql();
25         $mysql->connect();
26         $where = "ordersn = '".$order_sn."'";
27         $mysql->select('order','',$where);
28         $finds=$mysql->fetchAll();
29         print_r($finds);
30         if(isset($finds[0]['status']) && $finds[0]['status']==0){
31             $data   = array('status' => 3);
32             $where  = " id = ".$finds[0]['id'];
33             $mysql->update('order',$data,$where);
34         }
35     }
36 
37 }
38 
39 //perhaps
40 /*$redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), function ($redis, $pattern, $channel, $msg){
41     echo PHP_EOL;
42     echo "Pattern: $pattern\n";
43     echo "Channel: $channel\n";
44     echo "Payload: $msg\n\n";
45     //................
46 });*/
47  

 

Redis2.class.php

 1 <?php
 2 
 3 class Redis2
 4 {
 5     private $redis;
 6 
 7     public function __construct($host = '127.0.0.1', $port = '6379',$password = '',$db = '15')
 8     {
 9         $this->redis = new Redis();
10         $this->redis->connect($host, $port);    //Connect Redis
11         $this->redis->auth($password);      //Password verification
12         $this->redis->select($db);    //Select database
13     }
14 
15     public function setex($key, $time, $val)
16     {
17         return $this->redis->setex($key, $time, $val);
18     }
19 
20     public function set($key, $val)
21     {
22         return $this->redis->set($key, $val);
23     }
24 
25     public function get($key)
26     {
27         return $this->redis->get($key);
28     }
29 
30     public function expire($key = null, $time = 0)
31     {
32         return $this->redis->expire($key, $time);
33     }
34 
35     public function psubscribe($patterns = array(), $callback)
36     {
37         $this->redis->psubscribe($patterns, $callback);
38     }
39 
40     public function setOption()
41     {
42         $this->redis->setOption(\Redis::OPT_READ_TIMEOUT, -1);
43     }
44 
45     public function lRange($key,$start,$end)
46     {
47         return $this->redis->lRange($key,$start,$end);
48     }
49 
50     public function lPush($key, $value1, $value2 = null, $valueN = null ){
51         return $this->redis->lPush($key, $value1, $value2 = null, $valueN = null );
52     }
53 
54     public function delete($key1, $key2 = null, $key3 = null)
55     {
56         return $this->redis->delete($key1, $key2 = null, $key3 = null);
57     }
58 
59 }

 

 

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:

 1 <?php
 2 #! /usr/bin/env php
 3 ini_set('default_socket_timeout', -1);  //No timeout
 4 require_once 'Redis2.class.php';
 5 $redis_db = '15';
 6 $redis = new \Redis2('127.0.0.1','6379','',$redis_db);
 7 // Solve Redis Timeout during client subscription
 8 $redis->setOption();
 9 //When key You will see the notice when it expires. You can subscribe to it key __keyevent@<db>__:expired This format is fixed, db Represents the number of the database. Since the subscription is enabled, all key Expiration times are pushed, so it's best to use a single database for isolation
10 $redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), 'keyCallback');
11 // Callback function,Write processing logic here
12 function keyCallback($redis, $pattern, $channel, $msg)
13 {
14     echo PHP_EOL;
15     echo "Pattern: $pattern\n";
16     echo "Channel: $channel\n";
17     echo "Payload: $msg\n\n";
18     $list = explode(':',$msg);
19 
20     $order_sn = isset($list[0])?$list[0]:'0';
21     $use_mysql = isset($list[1])?$list[1]:'0';
22 
23     if($use_mysql == 1){
24         require_once 'db.class.php';
25         $mysql = new \mysql();
26         $mysql->connect();
27         $where = "ordersn = '".$order_sn."'";
28         $mysql->select('order','',$where);
29         $finds=$mysql->fetchAll();
30         print_r($finds);
31         if(isset($finds[0]['status']) && $finds[0]['status']==0){
32             $data   = array('status' => 3);
33             $where  = " id = ".$finds[0]['id'];
34             $mysql->update('order',$data,$where);
35         }
36     }
37 
38 }
39 
40 
41 //perhaps
42 /*$redis->psubscribe(array('__keyevent@'.$redis_db.'__:expired'), function ($redis, $pattern, $channel, $msg){
43     echo PHP_EOL;
44     echo "Pattern: $pattern\n";
45     echo "Channel: $channel\n";
46     echo "Payload: $msg\n\n";
47     //................
48 });*/

 

 

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&

1 [root@chokingwin HiGirl]# nohup ./psubscribe.php & 
2 [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:

1 [root@chokingwin HiGirl]# cat nohup.out 
2 Pattern:__keyevent@0__:expired 
3 Channel: __keyevent@0__:expired 
4 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 you deal with it After all business logic is completed, close the database connection actively, that is, close the database connection actively to solve the problem

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.

Tags: PHP Redis MySQL Database

Posted on Mon, 02 Dec 2019 23:38:21 -0500 by Dorky