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.