PHP + Redis实现自动取消订单
简单定时任务解决方案:使用redis的keyspace notifications(键失效后通知事件)
需要注意此功能是在 redis 2.8版本以后推出的,因此你服务器上的 reids 最少要是 2.8 版本以上
业务场景
当一个业务触发以后需要启动一个定时任务,在指定时间内再去执行一个任务(如自动取消订单,自动完成订单等功能)
Redis 的 keyspace notifications 会在 key 失效后发送一个事件,监听此事件的的客户端就可以收到通知
Redis 开启 keyspace notifications
redis 默认不会开启 keyspace notifications,因为开启后会对cpu有消耗
- 更改Redis配置文件(redis.conf)
# 原配置:
notify-keyspace-events ""
# 更改为:
notify-keyspace-events "Ex"
- 重启 Redis,查看 notify-keyspace-events 的值是不是 xE
# redis-cli 命令行
# 查看redis配置信息
config get *
# 监听key过期事件
# keyevent 事件,事件以 __keyevent@<db>__ 为前缀进行发布
# expired 过期事件,当某个键过期并删除时会产生该事件
psubscribe __keyevent@0__:expired
tp5.1 代码实现
- 创建自定义指令
创建一个自定义命令类文件,新建 application/common/command/Timeout.php
<?php
namespace app\command;
use app\common\model\Order;
use app\facade\Cache;
use think\console\Command;
use think\console\Input;
use think\console\Output;
use think\Db;
use think\Exception;
class Timeout extends Command
{
protected function configure()
{
parent::configure(); // TODO: Change the autogenerated stub
$this->setName('timeout')->setDescription('Cancel Timeout Order');
}
protected function execute(Input $input, Output $output)
{
$this->getKey();
$output->writeln("ok");
}
public function getKey() {
// socket流的超时时间 -1禁用超时
ini_set('default_socket_timeout', -1);
// cli模式下,开启数据库断开重连,防止脚本运行一段时间后数据库断开连接
$dbConfig = config('database.');
$dbConfig['break_reconnect'] = true;
Db::init($dbConfig);
// 返回Redis句柄对象
$redis = Cache::store('redis')->handler();
// Redis 订阅监听过期事件
$redis->psubscribe(array('__keyevent@0__:expired'), 'app\command\Timeout::keyCallback'); // 回调必须写绝对路径 要不然会报错
}
// key过期回调
public static function keyCallback($redis, $pattern, $channel, $message) {
// redis key 示例: course_order=6434@@2021071116061353484856
if (strpos($message, 'course_order=') !== false) {
$str = substr($message, strlen('course_order='));
list($orderId, $orderNo) = explode('@@', $str);
// 自动取消课程报名订单(更新订单状态)
try {
$status = Order::where(['id' => $orderId])->value('status');
// 未付款
if ($status == 0) {
$res = Order::where(['id'=> $orderId])->update(['status'=>2]);
$scriptMsg = '';
$logMsg = '';
if ($res) {
$scriptMsg .= "success update order, order_id:$orderId order_number:$orderNo. ";
$logMsg .= "=====success order表更新成功: 订单id:$orderId, 订单编号:$orderNo=====";
} else {
$scriptMsg .= "error update order, order_id:$orderId order_number:$orderNo. ";
$logMsg .= "-----error order表更新失败: 订单id:$orderId, 订单编号:$orderNo-----";
}
self::timeoutOrderWriteLog($logMsg); // 写入日志
echo $scriptMsg; // 脚本消息提示
}
} catch (Exception $e) {
self::timeoutOrderWriteLog('Error: 自动取消订单异常, 异常信息:' . $e->getMessage());
//throw new Exception($e->getMessage());
}
}
}
/**
* 取消订单写入日志
* @param string $data 写入的信息
*/
public static function timeoutOrderWriteLog($data){
//设置路径目录信息
$datefile = date('Ym');
$url = './log/'.$datefile.'/timeout-order.txt'; // 项目根目录,而非public目录
$dir_name=dirname($url);
//目录不存在就创建
if(!file_exists($dir_name))
{
//iconv防止中文名乱码
$res = mkdir(iconv("UTF-8", "GBK", $dir_name),0777,true);
}
$fp = fopen($url,"a");//打开文件资源通道 不存在则自动创建
fwrite($fp,date("Y-m-d H:i:s").var_export($data,true)."\r\n");//写入文件
fclose($fp);//关闭资源通道
}
}
- 配置 application/command.php 文件
return [
'app\command\Timeout' // 超时自动取消订单
];
- 测试-命令帮助-命令行下运行
php think
输出:
- 运行 timeout 命令
php think timeout
# 输出
ok
后台运行脚本
使用 & 命令,后台运行脚本,并使用定时任务每隔一分钟去检查进程是否还存在,没有则重新启动该脚本
- 后台运行脚本
setsid php think timeout > /opt/nginx/tp5.1/log/nohup.txt & # 后台运行脚本
- 安装 crontab
yum install -y vixie-cron
- 项目根目录新建 monitor.sh 脚本文件,并写入以下内容
#!/bin/bash
alive=`ps aux|grep "php think timeout"|grep -v grep|wc -l`
if [ $alive -eq 0 ]
then
cd /opt/nginx/tp5.1
php think timeout > /opt/nginx/tp5.1/log/nohup.txt &
fi
- 添加计划任务(每分钟检测一次)
crontab -e
* * * * * /opt/nginx/tp5.1/monitor.sh > /dev/null &
部署完毕后,对于超时的订单系统将自动取消,并写入日志到 /log/日期文件夹/timeout-order.txt 文件中