使用PHP+Redis实现延迟任务,实现自动取消订单功能


Posted in PHP onNovember 21, 2019

简单定时任务解决方案:使用redis的keyspace notifications(键失效后通知事件) 需要注意此功能是在redis 2.8版本以后推出的,因此你服务器上的reids最少要是2.8版本以上;

(A)业务场景:

1、当一个业务触发以后需要启动一个定时任务,在指定时间内再去执行一个任务(如自动取消订单,自动完成订单等功能)

2、redis的keyspace notifications 会在key失效后发送一个事件,监听此事件的的客户端就可以收到通知

(B)服务准备:

1、修改reids配置文件(redis.conf)【window系统配置文件为:redis.windows.conf 】

redis默认不会开启keyspace notifications,因为开启后会对cpu有消耗

备注:E:keyevent事件,事件以__keyevent@<db>__为前缀进行发布;

x:过期事件,当某个键过期并删除时会产生该事件;

原配置为:

notify-keyspace-events ""

更改 配置如下:

notify-keyspace-events "Ex"

保存配置后,重启Redis服务,使配置生效

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

window系统重启redis ,先切换到redis文件目录,然后关闭redis服务(redis-server --service-stop),再开启(redis-server --service-start)

使用PHP+Redis实现延迟任务,实现自动取消订单功能

C)文件代码:

phpredis实现订阅Keyspace notification,可实现自动取消订单,自动完成订单。以下为测试例子

创建4个文件,然后自行修改数据库和redis配置参数

db.class.php

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

 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 条件
 * @return mixed 查询结果数目
 */
 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 获取全部结果
 */
 public function fetchAll()
 {
 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('127.0.0.1','6379','','15');
$order_sn = 'SN'.time().'T'.rand(10000000,99999999);
$use_mysql = 1; //是否使用数据库,1使用,2不使用
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延迟任务'); //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('127.0.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[0]['status']) && $finds[0]['status']==0){
 $data = array('status' => 3);
 $where = " id = ".$finds[0]['id'];
 $mysql->update('order',$data,$where);
 }
 }
}
//或者
/*$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); //连接Redis
 $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);
 }
}

window系统测试方法:先在cmd命令界面运行psubscribe.php,然后网页打开index.php。

使监听后台始终运行(订阅)

有个问题 做到这一步,利用 phpredis 扩展,成功在代码里实现对过期 Key 的监听,并在 psCallback()里进行回调处理。开头提出的两个需求已经实现。可是这里有个问题:redis 在执行完订阅操作后,终端进入阻塞状态,需要一直挂在那。且此订阅脚本需要人为在命令行执行,不符合实际需求。

实际上,我们对过期监听回调的需求,是希望它像守护进程一样,在后台运行,当有过期事件的消息时,触发回调函数。使监听后台始终运行 希望像守护进程一样在后台一样,

我是这样实现的。

Linux中有一个nohup命令。功能就是不挂断地运行命令。同时nohup把脚本程序的所有输出,都放到当前目录的nohup.out文件中,如果文件不可写,则放到<用户主目录>/nohup.out 文件中。那么有了这个命令以后,不管我们终端窗口是否关闭,都能够让我们的php脚本一直运行。

编写psubscribe.php文件:

<?php
#! /usr/bin/env php
ini_set('default_socket_timeout', -1); //不超时
require_once 'Redis2.class.php';
$redis_db = '15';
$redis = new \Redis2('127.0.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[0]['status']) && $finds[0]['status']==0){
 $data = array('status' => 3);
 $where = " id = ".$finds[0]['id'];
 $mysql->update('order',$data,$where);
 }
 }
}
//或者
/*$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";
 //................
});*/

注意:我们在开头,申明 php 编译器的路径:

#! /usr/bin/env php

这是执行 php 脚本所必须的。

然后,nohup 不挂起执行 psubscribe.php,注意 末尾的 &

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

说明:脚本确实已经在 4456 号进程上跑起来。

查看下nohup.out cat 一下 nohuo.out,看下是否有过期输出:

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

运行index.php ,3秒后效果如上即成功

遇到问题:使用命令行模式开启监控脚本 ,一段时间后报错 :Error while sending QUERY packet. PID=xxx

解决方法:由于等待消息队列是一个长连接,而等待回调前有个数据库连接,数据库的wait_timeout=28800,所以只要下一条消息离上一条消息超过8小时,就会出现这个错误,把wait_timeout设置成10,并且捕获异常,发现真实的报错是 MySQL server has gone away ,
所以只要处理完所有业务逻辑后主动关闭数据库连接,即数据库连接主动close掉就可以解决问题

yii解决方法如下:

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

查看进程方法:

ps -aux|grep psubscribe.php

a:显示所有程序
u:以用户为主的格式来显示
x:显示所有程序,不以终端机来区分

查看jobs进程ID:[ jobs -l ]命令

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

终止后台运行的进程方法:

kill -9 进程号

清空 nohup.out文件方法:

cat /dev/null > nohup.out

我们在使用nohup的时候,一般都和&配合使用,但是在实际使用过程中,很多人后台挂上程序就这样不管了,其实这样有可能在当前账户非正常退出或者结束的时候,命令还是自己结束了。

所以在使用nohup命令后台运行命令之后,我们需要做以下操作:

1.先回车,退出nohup的提示。

2.然后执行exit正常退出当前账户。
3.然后再去链接终端。使得程序后台正常运行。

我们应该每次都使用exit退出,而不应该每次在nohup执行成功后直接关闭终端。这样才能保证命令一直在后台运行。

总结

以上所述是小编给大家介绍的使用PHP+Redis实现延迟任务,实现自动取消订单功能,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

PHP 相关文章推荐
使用sockets:从新闻组中获取文章(三)
Oct 09 PHP
php项目打包方法
Feb 18 PHP
url decode problem 解决方法
Dec 26 PHP
PHP命名空间(Namespace)的使用详解
May 04 PHP
php魔术方法与魔术变量、内置方法与内置变量的深入分析
Jun 03 PHP
解析PHP中的正则表达式以及模式匹配
Jun 19 PHP
PHPThumb图片处理实例
May 03 PHP
PHP面向对象教程之自定义类
Jun 10 PHP
Laravel使用PHPQRCODE实现生成带有LOGO的二维码图片功能示例
Jul 07 PHP
php中html_entity_decode实现HTML实体转义
Jun 13 PHP
php实现文章评论系统
Feb 18 PHP
Laravel框架实现简单的学生信息管理平台案例
May 07 PHP
PHP框架实现WebSocket在线聊天通讯系统
Nov 21 #PHP
PHP读取Excel内的图片(phpspreadsheet和PHPExcel扩展库)
Nov 19 #PHP
使用PHP开发留言板功能
Nov 19 #PHP
关于Laravel参数验证的一些疑与惑
Nov 19 #PHP
php传值和传引用的区别点总结
Nov 19 #PHP
php 使用 __call实现重载功能示例
Nov 18 #PHP
PHP中通过getopt解析GNU C风格命令行选项
Nov 18 #PHP
You might like
PHP var_dump遍历对象属性的函数与应用代码
2010/06/04 PHP
php封装好的人民币数值转中文大写类
2015/12/20 PHP
PHP 加密 Password Hashing API基础知识点
2020/03/02 PHP
javascript实现的距离现在多长时间后的一个格式化的日期
2009/10/29 Javascript
jQuery.ajax 用户登录验证代码
2010/10/29 Javascript
获取css样式表内样式的js函数currentStyle(IE),defaultView(FF)
2011/02/14 Javascript
30个让人兴奋的视差滚动(Parallax Scrolling)效果网站
2012/03/04 Javascript
javascript打印输出json实例
2013/11/11 Javascript
jQuery 1.9使用$.support替代$.browser的使用方法
2014/05/27 Javascript
学习javascript的闭包,原型,和匿名函数之旅
2015/10/18 Javascript
jQuery中的siblings用法实例分析
2015/12/24 Javascript
Bootstrap基本样式学习笔记之图片(6)
2016/12/07 Javascript
Bootstrap基本插件学习笔记之轮播幻灯片(23)
2016/12/08 Javascript
浅析JavaScript动画模拟拖拽原理
2016/12/09 Javascript
JavaScript 函数的定义-调用、注意事项
2017/04/16 Javascript
JS基于正则实现数字千分位用逗号分隔的方法
2017/06/16 Javascript
JavaScript实现打印星型金字塔功能实例分析
2017/09/27 Javascript
基于vue,vue-router, vuex及addRoutes进行权限控制问题
2018/05/02 Javascript
Bootbox将后台JSON数据填充Form表单的实例代码
2018/09/10 Javascript
Vue3 的响应式和以前有什么区别,Proxy 无敌?
2020/05/20 Javascript
js实现弹窗效果
2020/08/09 Javascript
详解Python网络框架Django和Scrapy安装指南
2019/04/01 Python
Python中利用LSTM模型进行时间序列预测分析的实现
2019/07/26 Python
python str字符串转uuid实例
2020/03/03 Python
Habitat家居英国官方网站:沙发、家具、照明、厨房和户外
2019/12/12 全球购物
C语言编程题
2015/03/09 面试题
某/etc/fstab文件中的某行如下: /dev/had5 /mnt/dosdata msdos defaults,usrquota 1 2 请解释其含义
2013/09/18 面试题
大学生应聘推荐信范文
2013/11/19 职场文书
大门门卫岗位职责
2013/11/30 职场文书
大学四年学习的自我评价分享
2013/12/09 职场文书
工程管理英文求职信
2014/03/18 职场文书
迎国庆横幅标语
2014/10/08 职场文书
毕业典礼致辞
2015/07/29 职场文书
小学三年级数学教学反思
2016/02/16 职场文书
手把手教你怎么用Python实现zip文件密码的破解
2021/05/27 Python
Python max函数中key的用法及原理解析
2021/06/26 Python