1、为什么要使用定时任务
- 当用户下单了,如果长期没付款。将来再去付款的时候,支付宝和微信那边就会提示订单过期了,无法支付。这就是说,订单支付超时后,最好将订单的状态设置为已过期,让用户无法再发起支付。
- 当用户支付了订单,就会自动变成大会员身份。但是大会员身份是有有效期的,在过期后,就需要将这些用户变成普通用户。
2、node-schedule的使用
2.1、 安装 node-schedule
npm install node-schedule
2.2、 定时代码解析
* * * * * *
┬ ┬ ┬ ┬ ┬ ┬
│ │ │ │ │ │
│ │ │ │ │ └ 星期几 (0 - 7) (0 或 7 是星期天)
│ │ │ │ └───── 月份 (1 - 12)
│ │ │ └────────── 日期 (1 - 31)
│ │ └─────────────── 小时 (0 - 23)
│ └──────────────────── 分钟 (0 - 59)
└───────────────────────── 秒 (0 - 59, 可选)
例一:在文档例子里
const job = schedule.scheduleJob('42 * * * *', function(){
console.log('The answer to life, the universe, and everything!');
});
大家先数一数,这里只有 5 位数
,而完整的格式是 6 位数
。这就说明,秒被省略了
。那么这个42就是分钟了。这就是说,每个小时的第 42 分钟
,会执行一次
。
例二,下面还有个例子:
const job = schedule.scheduleJob('0 17 ? * 0,4-6', function(){
console.log('Today is recognized by Rebecca Black!');
});
- 0:表示在第 0 分钟时触发。
- 17:是说在 17 点(也就是下午 5 点)触发。
- ?:表示不指定具体的日期,因为在最后面一位,指定了星期几。
- *:表示每个月都运行。
- 0,4-6:最后面的这个,0 表示:星期天(0)。4-6 表示:星期四(4)、星期五(5)、和星期六(6)都可以。
3.4. 在 Node 项目中使用 node-schedule
// 定时任务
const schedule = require('node-schedule');
const job = schedule.scheduleJob('* * * * * *', function(){
console.log('Hello xw!');
});
3、 项目中封装
及订单的处理
因为一个项目
可能有多个定时任务
,所以我们可以专门建一个文件夹
,来存放定时任务相关的文件
。
3.1、在根目录
中,新建tasks目录
,里面再新建一个check-order.js
文件。
const schedule = require('node-schedule');
const { sequelize, Order } = require('../models');
const { Op } = require('sequelize');
const logger = require('../utils/logger');
const moment = require('moment');
/**
* 定时检查并处理超时未支付订单
* 每天凌晨 4:30 执行一次 测试时可改为0 * * * * * 每分钟的第 0 秒
*/
function scheduleOrderCheck() {
schedule.scheduleJob('0 30 4 * * *', async () => {
const t = await sequelize.transaction();
try {
// 查找超时未支付的订单
const expiredOrders = await Order.findAll({
attributes: ['id'],
where: {
status: 0,
createdAt: {
[Op.lt]: moment().subtract(1, 'day').toDate()
}
},
transaction: t,
lock: true // 使用排它锁,防止并发更新
});
// 已超时订单的 ID 列表
const orderIds = expiredOrders.map(order => order.id);
// 批量更新超时订单状态
await Order.update(
{
status: 2, // 订单状态:已取消(超时)
},
{
where: {
id: orderIds
},
transaction: t
}
);
await t.commit();
} catch (error) {
await t.rollback();
logger.error('定时任务处理超时订单失败:', error);
}
});
}
module.exports = scheduleOrderCheck;
- 先在上面做了一些
引用
。 - 定义一个
检查超时订单
的任务。 - 定时的第一个参数,先设置成
0
,也就是每分钟的第 0 秒执行一次
。 - 下面
开启了事务
。 - 查找状态为
0
,也就是未支付
的,并且在一天之前创建的订单
。 - 找到的订单,都加上
排它锁
。 - 然后使用
map
,遍历一下,将所有订单的ID
,组装成数组
。 - 接着就调用
update
方法,批量更新状态为2,也就是已过期。 - 完成后就提交事务。
- 最底下,如果执行失败,除了回滚外,还加上了日志。
3.2、index.js 注册所有定时任务
const scheduleOrderCheck = require('./check-order');
const logger = require('../utils/logger');
/**
* 初始化所有定时任务
*/
function initScheduleTasks() {
try {
// 启动订单超时检查任务
scheduleOrderCheck();
} catch (error) {
logger.error('定时任务启动失败:', error);
}
}
module.exports = initScheduleTasks;
3.3、修改 app.js
// 启动定时任务
const initScheduleTasks = require('./tasks');
initScheduleTasks();
4、定时器、排他锁结合RabbitMQ实现 订单自动过期功能
4.1、安装RabbitMQ
npm i amqplib
4.2、封装RabbitMQ 生产者及消费者
utils/rabbit-mq.js
const amqp = require('amqplib');
// 日志 可去除
const logger = require('./logger');
const {sequelize, Order} = require("../models");
// 创建全局的 RabbitMQ 连接和通道
let connection;
let channel;
// 封装一个重试连接的函数,增加连接的稳定性
const connectWithRetry = async (url, retries = 5, delay = 5000) => {
let attempt = 0;
while (attempt < retries) {
try {
return await amqp.connect(url);
} catch (error) {
attempt++;
logger.error(`RabbitMQ 连接尝试 ${attempt} 失败:`, error);
if (attempt < retries) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error('无法连接到 RabbitMQ,已达到最大重试次数');
};
/**
* 连接到 RabbitMQ
* @returns {Promise<*>}
*/
const connectToRabbitMQ = async (queueName) => {
if (connection && channel) return;
try {
connection = await connectWithRetry(process.env.RABBITMQ_URL);
channel = await connection.createChannel();
// 监听连接关闭事件,方便处理异常
connection.on('close', () => {
logger.warn('RabbitMQ 连接已关闭,尝试重新连接...');
connection = null;
channel = null;
});
// 监听连接错误事件,增强错误处理能力
connection.on('error', (err) => {
logger.error('RabbitMQ 连接发生错误:', err);
});
await channel.assertQueue(queueName, {durable: true});
} catch (error) {
logger.error('RabbitMQ 连接失败:', error);
throw error;
}
};
/**
* 生产者(发送消息)
*/
const producer = async (queueName, msg) => {
try {
await connectToRabbitMQ(queueName); // 确保已连接
// 消息持久化设置,提高消息可靠性
const options = {persistent: true};
const sent = channel.sendToQueue(queueName, Buffer.from(JSON.stringify(msg)), options);
if (!sent) {
logger.warn('消息未能立即入队,等待下次机会');
}
} catch (error) {
logger.error('邮件队列生产者错误:', error);
throw error;
}
};
/**
* 过期订单队列消费者(接收消息)
* @returns {Promise<void>}
*/
async function consumeExpiredOrders() {
try {
// 链接RabbitMQ
await connectToRabbitMQ('expired_orders_queue');
channel.consume('expired_orders_queue', async (msg) => {
if (msg) {
const orderIds = JSON.parse(msg.content.toString());
const t = await sequelize.transaction();
try {
// 批量更新超时订单状态
await Order.update(
{
status: 2, // 订单状态:已取消(超时)
},
{
where: {
id: orderIds
},
transaction: t
}
);
await t.commit();
channel.ack(msg);
console.log(`成功处理 ${orderIds.length} 个过期订单`);
logger.info('过期订单消费者已开始监听');
} catch (error) {
await t.rollback();
logger.error('处理过期订单时出错:', error);
channel.nack(msg);
}
}
}
)
;
} catch (error) {
console.error('消费过期订单消息时出错:', error);
}
}
module.exports = {
producer,
consumeExpiredOrders
};
utils/order-consumer.js
require('dotenv').config();
const {consumeExpiredOrders} = require('./rabbit-mq');
const logger = require('./logger');
// 封装启动消费者的函数,方便后续扩展和错误处理
const startxpiredOrdersConsumer = async () => {
try {
await consumeExpiredOrders();
logger.info('订单已启动');
} catch (error) {
logger.error('启动订单时出错:', error);
process.exit(1);
}
};
// 启动消费者
startxpiredOrdersConsumer();
// 监听进程信号,优雅关闭消费者
process.on('SIGINT', async () => {
logger.info('收到 SIGINT 信号,正在优雅关闭订单消费者...');
try {
// 这里可以添加关闭连接和通道的逻辑
process.exit(0);
} catch (error) {
logger.error('关闭订单时出错:', error);
process.exit(1);
}
});
在ecosystem.config.js
增加order-consumer脚本
module.exports = {
apps: [
{
name: "express-app",
script: "./bin/www",
watch: process.env.NODE_ENV === 'development', // 根据环境变量决定是否开启监听
interpreter: "node",
env: {
NODE_ENV: "development"
},
env_production: {
NODE_ENV: "production"
}
},
{
name: "order-consumer",
script: "./utils/order-consumer.js",
interpreter: "node",
env: {
NODE_ENV: "development"
},
env_production: {
NODE_ENV: "production"
}
}
]
};
4.3、order定时器封装修改参照 上述第3点修改check-order.js
tasks/check-order.js
const schedule = require('node-schedule');
const {sequelize, Order} = require('../models');
const {Op} = require('sequelize');
const logger = require('../utils/logger');
const moment = require('moment');
const {producer} = require('../utils/rabbit-mq')
/**
* 定时检查并处理超时未支付订单
* 每天凌晨 0:00 执行一次
*/
async function processExpiredOrders() {
// 定义每次查询的订单数量,即分页查询时每页的记录数
const pageSize = 1000;
// 偏移量,用于分页查询,初始为 0 表示从第一条记录开始查询
let offset = 0;
// 标记是否还有更多数据可查询,初始设为 true
let hasMoreData = true;
// 当还有数据可查询时,进入循环进行分页查询和处理
while (hasMoreData) {
// 开启一个数据库事务,确保数据操作的原子性
const t = await sequelize.transaction();
try {
// 查找超时未支付的订单,使用分页查询的方式
const expiredOrders = await Order.findAll({
// 只查询订单的 id 字段,减少数据传输量
attributes: ['id'],
where: {
// 筛选出状态为 0(未支付)的订单
status: 0,
// 筛选出创建时间早于当前时间一天前的订单,即超时未支付订单
createdAt: {
[Op.lt]: moment().subtract(1, 'day').toDate()
}
},
// 每页查询的记录数
limit: pageSize,
// 偏移量,用于分页
offset: offset,
// 将查询操作纳入当前事务
transaction: t,
// 使用排它锁,防止并发更新,保证数据一致性
lock: true
});
// 如果查询到的过期订单数量为 0,说明没有更多符合条件的数据了
if (expiredOrders.length === 0) {
hasMoreData = false;
// 提交事务
await t.commit();
// 跳出循环
break;
}
// 提取已超时订单的 ID 列表,方便后续批量操作
const orderIds = expiredOrders.map(order => order.id);
// 将订单 ID 发送到消息队列,以便后续异步处理
await producer('expired_orders_queue', orderIds)
// 批量更新超时订单的状态为 2(已取消(超时))
await Order.update(
{
status: 2, // 订单状态:已取消(超时)
},
{
where: {
id: orderIds
},
// 将更新操作纳入当前事务
transaction: t
}
);
// 提交事务,确保数据更新操作生效
await t.commit();
// 偏移量增加,用于下一次分页查询
offset += pageSize;
} catch (error) {
// 若出现异常,回滚事务,保证数据的一致性
await t.rollback();
// 记录错误日志,方便后续排查问题
logger.error('定时任务处理超时订单失败:', error);
// 跳出循环,终止当前查询流程
break;
}
}
}
function scheduleOrderCheck() {
// 使用 node - schedule 模块设置定时任务,每天凌晨 0:00 执行 processExpiredOrders 函数
schedule.scheduleJob('0 0 0 * * *', processExpiredOrders);
}
// 导出 scheduleOrderCheck 函数,供其他模块调用以启动定时任务
module.exports = scheduleOrderCheck;
4.4 重启项目
pm2 start ecosystem.config.js --no-daemon