tp5.1 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 文件中

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
好的,我可以为您演示如何使用Spring Boot、MyBatis、MySQL和Redis实现订单在指定时间内没有完成支付自动取消订单的功能。 首先,我们需要定义订单实体类,包含订单号、订单状态、创建时间、过期时间等属性。 ```java public class Order { private Long id; private String orderNo; private Integer status; private Date createTime; private Date expireTime; // 省略getter和setter方法 } ``` 然后我们需要在MySQL数据库中创建订单表,用于存储订单信息。 ```sql CREATE TABLE `order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `status` int(11) NOT NULL, `create_time` datetime NOT NULL, `expire_time` datetime NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ``` 接着,我们需要使用MyBatis来实现订单表的增删改查操作,以及定时任务更新订单状态的操作。这些操作可以通过Mapper接口和Mapper.xml文件来实现。在Mapper.xml文件中,我们可以使用MySQL的定时任务机制,定期执行更新操作,将过期的订单状态改为已取消。 ```xml <!-- OrderMapper.xml --> <mapper namespace="com.example.demo.mapper.OrderMapper"> <insert id="insertOrder" parameterType="Order"> insert into `order` (order_no, status, create_time, expire_time) values (#{orderNo}, #{status}, #{createTime}, #{expireTime}) </insert> <update id="cancelOrder" parameterType="Order"> update `order` set status = #{status} where id = #{id} </update> <select id="getOrderById" parameterType="Long" resultType="Order"> select * from `order` where id = #{id} </select> <update id="updateExpiredOrderStatus"> update `order` set status = #{status} where status = #{oldStatus} and expire_time <= #{now} </update> </mapper> ``` 在定时任务中,我们需要使用MyBatis的update方法来更新过期的订单状态。 ```java @Service public class OrderService { private final OrderMapper orderMapper; public OrderService(OrderMapper orderMapper) { this.orderMapper = orderMapper; } @Scheduled(cron = "0 */1 * * * ?") // 每分执行一次 public void updateExpiredOrderStatus() { Order order = new Order(); order.setStatus(OrderStatusEnum.CANCELED.getValue()); order.setOldStatus(OrderStatusEnum.UNPAID.getValue()); order.setNow(new Date()); orderMapper.updateExpiredOrderStatus(order); } } ``` 然后,我们需要使用Redis实现订单状态的缓存。在订单创建时,将订单信息存储到Redis中,并设置过期时间为订单的过期时间。当用户支付成功后,将Redis中的订单状态改为已支付。如果订单未支付并且过期时间已到,则将Redis中的订单状态改为已取消,并通过MyBatis更新订单状态。 ```java @Configuration public class RedisConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.password}") private String password; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.database}") private int database; @Bean public JedisPool jedisPool() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(100); jedisPoolConfig.setMaxIdle(20); jedisPoolConfig.setMinIdle(10); jedisPoolConfig.setMaxWaitMillis(10000); return new JedisPool(jedisPoolConfig, host, port, timeout, password, database); } } @Service public class OrderService { private final OrderMapper orderMapper; private final JedisPool jedisPool; public OrderService(OrderMapper orderMapper, JedisPool jedisPool) { this.orderMapper = orderMapper; this.jedisPool = jedisPool; } public Order createOrder(Order order) { orderMapper.insertOrder(order); String key = "order:" + order.getId(); try (Jedis jedis = jedisPool.getResource()) { jedis.set(key, String.valueOf(OrderStatusEnum.UNPAID.getValue()), "NX", "EX", order.getExpireTime().getTime() / 1000); } return order; } public void payOrder(Long orderId) { String key = "order:" + orderId; try (Jedis jedis = jedisPool.getResource()) { jedis.set(key, String.valueOf(OrderStatusEnum.PAID.getValue())); } } } ``` 最后,我们需要使用Spring的定时任务来定期检查Redis中的订单状态,并对过期的订单进行处理。具体实现可以通过在Spring Boot应用程序中添加一个Scheduled注解方法来实现。 ```java @Service public class OrderService { private final OrderMapper orderMapper; private final JedisPool jedisPool; public OrderService(OrderMapper orderMapper, JedisPool jedisPool) { this.orderMapper = orderMapper; this.jedisPool = jedisPool; } @Scheduled(cron = "0 */1 * * * ?") // 每分执行一次 public void checkExpiredOrder() { try (Jedis jedis = jedisPool.getResource()) { Set<String> keys = jedis.keys("order:*"); if (keys != null && !keys.isEmpty()) { for (String key : keys) { String value = jedis.get(key); if (value != null) { Long orderId = Long.parseLong(key.split(":")[1]); Order order = orderMapper.getOrderById(orderId); if (order != null && order.getStatus() == OrderStatusEnum.UNPAID.getValue()) { if (new Date().after(order.getExpireTime())) { order.setStatus(OrderStatusEnum.CANCELED.getValue()); orderMapper.cancelOrder(order); jedis.del(key); } } else { jedis.del(key); } } } } } } } ``` 以上就是使用Spring Boot、MyBatis、MySQL和Redis实现订单在指定时间内没有完成支付自动取消订单的详细演示。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值