Simple scheduled task solution: Use Redis keyspace Notifications (after key expiration notifications). Note that this feature is available in Redis version 2.8, so reIDS on your server must be at least version 2.8 or higher.

(A) Business scenarios:

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 automatic order cancellation, automatic order completion and other functions).

Redis keyspace Notifications sends an event when a key fails, and the client listening for the event can be notified

(B) Preparation of services:

1, Modify the reids configuration file (redis.conf).

Redis does not enable Keyspace Notifications by default, as enabling keyspace notifications can consume CPU

Note: E: keyevent event, which is published with the prefix __keyevent@__.

X: expiration event. This event is generated when a key expires and is deleted.

The original configuration was:

notify-keyspace-events ""
Copy the code

Change the configuration as follows:

notify-keyspace-events "Ex"
Copy the code

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

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

Windows restart redis, switch to the redis file directory, stop the redis service (redis-server –service-start), and start the redis service (redis-server –service-start).

! [](https://pic4.zhimg.com/80/v2-af84a71b294ffaadcb493031e1b2fba5_720w.jpg)

(C) File code:

Phpredis implements subscription to Keyspace Notification, which automatically cancels and completes orders. The following is a test example

Create four files and modify the database and redis configuration parameters yourself

db.class.php

<? php class mysql { private $mysqli; private $result; 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 condition * @return mixed 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; Public function fetchAll() {return $this->result->fetch_all(MYSQLI_ASSOC); Public function insert($table, $table, $table, $table, $table, $table, $table, $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 * @param $data array * @param $WHERE filter * @return mixed affected record */ 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; Public function delete($table, $table, $table, $table, $table, $table, $table) $where) { $sql = "DELETE FROM `{$table}` WHERE {$where}"; $this->mysqli->query($sql); return $this->mysqli->affected_rows; }}Copy the code

index.php

<? php require_once 'Redis2.class.php'; $redis = new \ Redis2 (' 127.0.0.1 ', '6379', ', '15'); $order_sn = 'SN. Time (). The' T '. The rand (10000000999999); $use_mysql = 1; // Whether to use database, 1 yes, 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 delay task '); $test_del = false; // Tests whether there is an expiration callback after removing the cache. If ($test_del == true){//sleep(1); $redis->delete($order_sn); } echo $order_sn; $k = 'test'; * $redis2->set($k,'100'); * $redis2->expire($k,10); * * /Copy the code

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); $Redis ->setOption(); Key __keyevent@<db>__:expired; db is the number of the database; after the subscription is enabled, all key expiration times will be pushed. $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); }}} / / 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"; / /... }); * /Copy the code

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); $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); }}Copy the code

Windows system test method: first in the CMD interface run subscribe. PHP, then open the webpage index.php. After 3 seconds, the effect is as follows

! [](https://pic1.zhimg.com/80/v2-86499693fc597b65484cb2425ac3012e_720w.jpg)

Make listening background always running (subscribe)

There is a problem with doing this, using the phpredis extension, successfully implementing the code to listen for expired keys and callback processing in psCallback(). The two requirements outlined at the beginning have already been implemented. However, there is a problem: after Redis completes the subscription operation, the terminal becomes blocked and needs to hang there all the time. The subscription script must be executed manually on the cli, which does not meet actual requirements.

In fact, our requirement for an expiration listener callback is that it runs in the background, like a daemon, and fires the callback when there is a message with an expiration event. I want the listener to always be running in the background like a daemon,

Here’s how I did it.

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

Write the subscribe. 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); $Redis ->setOption(); Key __keyevent@<db>__:expired; db is the number of the database; after the subscription is enabled, all key expiration times will be pushed. $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); }}} / / 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"; / /... }); * /Copy the code

Note: We declare the path to the PHP compiler at the beginning:

#! /usr/bin/env php
Copy the code

This is required for executing PHP scripts.

Nohup then executes subscribe. PHP without suspending, noting the & at the end

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

Note: The script is indeed running on process 4456.

Check nohup.out cat for nohuo. Out to see if there is an out-of-date output:

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

Run index. PHP and the result will be successful after 3 seconds

Error: Error while sending QUERY packet. PID= XXX is displayed after a period of time when the monitoring script is started in command line mode

Solutions: Since the waiting message queue is a long connection and there is a database connection waiting for the callback, the database wait_timeout=28800, so this error will occur whenever the next message is more than 8 hours from the last message. Set wait_TIMEOUT to 10 and catch the exception. The error is that the MySQL server has gone away. Therefore, you only need to close the database after processing all the service logic, that is, the database connection is actively closed

The solution of YII is as follows:

Yii::$app->db->close();
Copy the code

View process methods:

Ps - aux | grep psubscribe. PHP a: display all u: predominantly user formats to display the x: display all the procedures, not to distinguish between terminalsCopy the code

To view the JOBS process ID, run the [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>&1Copy the code

Terminating a process running in the background:

Kill -9 Indicates the process IDCopy the code

Empty the nohup.out file:

cat /dev/null > nohup.out
Copy the code

When we use nohup, it is usually used in conjunction with &. However, in practice, many people hang up the program in the background and leave it alone. In fact, this may cause the command to terminate itself when the current account abnormally exits or terminates.

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

1. Press Enter to exit nohUp.

2. Run exit to exit the current account. 3. Then go to the terminal. Make the program background run normally.

Instead of closing the terminal after nohup, we should have used exit every time. This ensures that the command is always running in the background.

I hope the above content can help you. Many PHPer will encounter some problems and bottlenecks when they are advanced, and they have no sense of direction when writing too many business codes. I have sorted out some information, including but not limited to: Distributed architecture, high scalability, high performance, high concurrency, server performance tuning, TP6, Laravel, Redis, Swoft, Kafka, Mysql optimization, shell scripting, Docker, microservices, Nginx, etc. Many knowledge points can be free to share with you