使用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 相关文章推荐
基于mysql的论坛(5)
Oct 09 PHP
PHP 裁剪图片成固定大小代码方法
Sep 09 PHP
php开发环境配置记录
Jan 14 PHP
PHP插入排序实现代码
Apr 04 PHP
本地机apache配置基于域名的虚拟主机详解
Aug 10 PHP
php通过ajax实现双击table修改内容
Apr 28 PHP
php实例分享之二维数组排序
May 15 PHP
PHP通过内置函数memory_get_usage()获取内存使用情况
Nov 20 PHP
Symfony2学习笔记之控制器用法详解
Mar 17 PHP
PHP引用返回用法示例
May 28 PHP
PHP上传图片类显示缩略图功能
Jun 30 PHP
实例讲解PHP表单
Jun 10 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/js获取客户端mac地址的实现代码
2013/07/08 PHP
php实现给图片加灰色半透明效果的方法
2014/10/20 PHP
PHP+Mysql基于事务处理实现转账功能的方法
2015/07/08 PHP
WordPress导航菜单的滚动和淡入淡出效果的实现要点
2015/12/14 PHP
PHP获取当前日期及本周一是几月几号的方法
2017/03/28 PHP
php判断文件上传图片格式的实例详解
2017/09/30 PHP
javascript+css 网页每次加载不同样式的实现方法
2009/12/27 Javascript
javascript工具库代码
2012/03/29 Javascript
js实现键盘上下左右键选择文字并显示在文本框的方法
2015/05/07 Javascript
js简单倒计时实现代码
2016/04/30 Javascript
使用Angular缓存父页面数据的方法
2017/01/03 Javascript
js实现随机数字字母验证码
2017/06/19 Javascript
vue实现a标签点击高亮方法
2018/03/17 Javascript
vue操作下拉选择器获取选择的数据的id方法
2018/08/24 Javascript
在angularJs中进行数据遍历的2种方法
2018/10/08 Javascript
JavaScript设计模式之观察者模式实例详解
2019/01/16 Javascript
jQuery 实现DOM元素拖拽交换位置的实例代码
2020/07/14 jQuery
[01:12]DOTA2次级职业联赛 - Newbee.Y 战队宣传片
2014/12/01 DOTA
Python的time模块中的常用方法整理
2015/06/18 Python
python实现高斯投影正反算方式
2020/01/17 Python
解决Pycharm中恢复被exclude的项目问题(pycharm source root)
2020/02/14 Python
python用WxPython库实现无边框窗体和透明窗体实现方法详解
2020/02/21 Python
python使用for...else跳出双层嵌套循环的方法实例
2020/05/17 Python
深入了解NumPy 高级索引
2020/07/24 Python
详解python3 GUI刷屏器(附源码)
2021/02/18 Python
使用CSS3来代替JS实现交互
2017/08/10 HTML / CSS
大学生个人求职信范文
2013/09/21 职场文书
红领巾心向党广播稿
2014/01/19 职场文书
研究生导师推荐信
2014/09/06 职场文书
党支部书记四风问题整改措施
2014/09/24 职场文书
法学专业大学生实习自我鉴定
2014/10/05 职场文书
党员查摆问题及整改措施
2014/10/10 职场文书
小学生一年级(书信作文)
2019/08/13 职场文书
Python基础之数据结构详解
2021/04/28 Python
Pytorch 中net.train 和 net.eval的使用说明
2021/05/22 Python
【海涛dota解说】DCG联赛第一周 LGD VS DH
2022/04/01 DOTA