Appearance
MongoDB Date类型详解
1. 概述
1.1 章节导读
在数据库应用开发中,时间数据的处理无处不在:用户注册时间、订单创建时间、文章发布时间、日志记录时间等。MongoDB的Date类型是存储和操作时间数据的核心类型,理解其原理和正确使用方法对于构建可靠的应用程序至关重要。
1.2 学习意义
Date类型在实际开发中具有极其重要的地位:
- 业务系统基础:几乎所有业务系统都需要记录时间信息,如创建时间、更新时间、过期时间等
- 数据分析关键:时间维度的数据分析是业务洞察的重要手段,如日活统计、趋势分析等
- 系统运维核心:日志记录、监控告警、定时任务等都依赖于精确的时间处理
- 分布式系统挑战:在分布式环境下,时间同步和时区处理是必须面对的问题
1.3 课程定位
本知识点承接《MongoDB基础数据类型》章节,深入讲解Date类型的内部原理和实际应用。学习本章节后,你将能够:
- 理解MongoDB Date类型的存储机制和精度特性
- 掌握PHP中MongoDB\BSON\UTCDateTime类的使用方法
- 正确处理时区转换和时间计算问题
- 构建高效的时间范围查询和聚合管道
- 避免时间处理中的常见陷阱和错误
2. 基本概念
2.1 语法详解
2.1.1 MongoDB Shell中的Date语法
在MongoDB Shell中,有多种创建Date对象的方式:
javascript
// 方式一:当前时间
var now = new Date();
// 方式二:指定时间(字符串格式)
var date1 = new Date("2024-03-15T10:30:00Z");
// 方式三:指定时间(时间戳毫秒)
var date2 = new Date(1710498600000);
// 方式四:ISODate辅助函数
var date3 = ISODate("2024-03-15T10:30:00Z");
// 方式五:Date函数(返回字符串,不推荐)
var dateStr = Date(); // 返回字符串,不是Date对象2.1.2 PHP中的UTCDateTime类
PHP中使用MongoDB\BSON\UTCDateTime类来表示MongoDB的Date类型:
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
// 方式一:当前时间
$now = new UTCDateTime();
// 方式二:指定时间戳(毫秒)
$timestamp = 1710498600000;
$date = new UTCDateTime($timestamp);
// 方式三:从DateTime对象创建
$dateTime = new DateTime('2024-03-15 10:30:00', new DateTimeZone('Asia/Shanghai'));
$utcDateTime = UTCDateTime::fromDateTime($dateTime);
// 方式四:从字符串解析
$dateTime = new DateTime('2024-03-15 10:30:00');
$utcDateTime = new UTCDateTime($dateTime->getTimestamp() * 1000);
// 输出验证
echo "当前时间: " . $now->toDateTime()->format('Y-m-d H:i:s') . "\n";
echo "指定时间: " . $date->toDateTime()->format('Y-m-d H:i:s') . "\n";
echo "从DateTime创建: " . $utcDateTime->toDateTime()->format('Y-m-d H:i:s') . "\n";运行结果:
当前时间: 2024-03-15 18:30:00
指定时间: 2024-03-15 18:30:00
从DateTime创建: 2024-03-15 18:30:002.2 语义解析
2.2.1 存储格式
MongoDB的Date类型在BSON中使用64位整数存储,表示自Unix纪元(1970-01-01 00:00:00 UTC)以来的毫秒数:
存储格式:int64(8字节)
精度:毫秒级
范围:-2^63 到 2^63-1 毫秒
约公元前2.9亿年到公元后2.9亿年2.2.2 时区特性
MongoDB Date类型的关键特性:
- 始终存储为UTC:无论客户端处于哪个时区,Date值在数据库中始终以UTC时间存储
- 无时区信息:Date类型本身不包含时区信息,只存储UTC时间戳
- 客户端负责转换:时区转换由应用程序在读写时处理
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
// 创建北京时间 2024-03-15 18:30:00
$beijingTime = new DateTime('2024-03-15 18:30:00', new DateTimeZone('Asia/Shanghai'));
$utcDateTime = new UTCDateTime($beijingTime->getTimestamp() * 1000);
// 查看存储的毫秒时间戳
echo "存储的时间戳(毫秒): " . $utcDateTime->__toString() . "\n";
// 转换回DateTime(默认UTC时区)
$dateTimeUTC = $utcDateTime->toDateTime();
echo "UTC时间: " . $dateTimeUTC->format('Y-m-d H:i:s') . "\n";
// 转换为北京时间显示
$dateTimeUTC->setTimezone(new DateTimeZone('Asia/Shanghai'));
echo "北京时间: " . $dateTimeUTC->format('Y-m-d H:i:s') . "\n";运行结果:
存储的时间戳(毫秒): 1710513000000
UTC时间: 2024-03-15 10:30:00
北京时间: 2024-03-15 18:30:002.3 编码规范
2.3.1 命名规范
php
<?php
// 推荐的日期字段命名
// 创建时间
$createdAt = new UTCDateTime();
// 更新时间
$updatedAt = new UTCDateTime();
// 过期时间
$expiresAt = new UTCDateTime(strtotime('+30 days') * 1000);
// 最后登录时间
$lastLoginAt = new UTCDateTime();
// 发布时间
$publishedAt = new UTCDateTime();
// 计划执行时间
$scheduledAt = new UTCDateTime();
// 删除时间(软删除)
$deletedAt = new UTCDateTime();2.3.2 存储规范
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->users;
// 规范一:始终使用UTCDateTime对象存储
$user = [
'username' => 'zhangsan',
'created_at' => new UTCDateTime(), // 正确
'updated_at' => new UTCDateTime(), // 正确
];
// 错误示例:不要存储时间字符串
$userWrong = [
'username' => 'lisi',
'created_at' => date('Y-m-d H:i:s'), // 错误:存储字符串
'updated_at' => time(), // 错误:存储整数时间戳
];
// 规范二:使用毫秒精度
$preciseTime = new UTCDateTime(microtime(true) * 1000);
echo "毫秒精度时间: " . $preciseTime->__toString() . "\n";
// 规范三:批量操作时统一时间戳
$batchTime = new UTCDateTime();
$documents = [
['name' => 'doc1', 'created_at' => $batchTime],
['name' => 'doc2', 'created_at' => $batchTime],
['name' => 'doc3', 'created_at' => $batchTime],
];3. 原理深度解析
3.1 BSON编码机制
3.1.1 存储结构
Date类型在BSON中的存储结构:
BSON Date类型编码:
┌─────────────────────────────────────────────────────────┐
│ 类型标识 (1字节) │ 字段名 (以\0结尾) │ UTC时间戳 (8字节) │
│ 0x09 │ "created_at" │ 1710513000000 │
└─────────────────────────────────────────────────────────┘
存储示例:
{
"_id": ObjectId("..."),
"created_at": Date(1710513000000)
}
二进制表示(十六进制):
09 00 00 00 // 文档总长度
0B // 字段名长度
63 72 65 61 74 65 64 5F 61 74 00 // "created_at\0"
09 // Date类型标识
00 50 4A 92 4E 01 00 00 // 8字节int64时间戳(小端序)
00 // 文档结束符3.1.2 精度与范围
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
echo "=== MongoDB Date类型精度与范围 ===\n\n";
// 精度测试
$microtime = microtime(true);
$milliseconds = (int)($microtime * 1000);
$utcDateTime = new UTCDateTime($milliseconds);
echo "原始时间戳: " . $microtime . "\n";
echo "毫秒时间戳: " . $milliseconds . "\n";
echo "存储值: " . $utcDateTime->__toString() . "\n";
echo "精度损失: " . (($microtime * 1000 - $milliseconds) * 1000) . " 微秒\n\n";
// 范围测试
echo "=== 时间范围 ===\n";
echo "最小值: " . (new UTCDateTime(PHP_INT_MIN))->toDateTime()->format('Y-m-d H:i:s') . "\n";
echo "最大值: " . (new UTCDateTime(PHP_INT_MAX))->toDateTime()->format('Y-m-d H:i:s') . "\n";
// 实际可用范围(受PHP限制)
$minTimestamp = -2192505208000; // 约1901年
$maxTimestamp = 253402300799000; // 约9999年
echo "\n实际可用范围:\n";
echo "最早: " . (new UTCDateTime($minTimestamp))->toDateTime()->format('Y-m-d H:i:s') . "\n";
echo "最晚: " . (new UTCDateTime($maxTimestamp))->toDateTime()->format('Y-m-d H:i:s') . "\n";运行结果:
=== MongoDB Date类型精度与范围 ===
原始时间戳: 1710513000.1234
毫秒时间戳: 1710513000123
存储值: 1710513000123
精度损失: 400 微秒
=== 时间范围 ===
最小值: -292277022657-01-27 08:29:52
最大值: 292277026596-12-04 15:30:07
实际可用范围:
最早: 1901-12-13 20:45:52
最晚: 9999-12-31 23:59:593.2 时区处理原理
3.2.1 时区转换流程
┌─────────────────────────────────────────────────────────────────┐
│ 时区转换流程图 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 应用层(北京时间 18:30) │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ DateTime对象 │ 时区: Asia/Shanghai (UTC+8) │
│ │ 2024-03-15 │ │
│ │ 18:30:00 │ │
│ └────────┬────────┘ │
│ │ getTimestamp() │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Unix时间戳 │ UTC标准时间戳 │
│ │ 1710513000 │ (秒) │
│ └────────┬────────┘ │
│ │ * 1000 │
│ ▼ │
│ ┌─────────────────┐ │
│ │ UTCDateTime │ MongoDB存储格式 │
│ │ 1710513000000 │ (毫秒) │
│ └────────┬────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ MongoDB存储 │ 始终存储UTC时间 │
│ │ Date(1710513...)│ 无时区信息 │
│ └─────────────────┘ │
│ │
│ 读取时反向转换: │
│ MongoDB → UTCDateTime → DateTime(UTC) → DateTime(目标时区) │
│ │
└─────────────────────────────────────────────────────────────────┘3.2.2 时区转换实践
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
class TimezoneHelper
{
private $timezone;
public function __construct(string $timezone = 'Asia/Shanghai')
{
$this->timezone = new DateTimeZone($timezone);
}
public function createFromLocal(string $datetime): UTCDateTime
{
$dt = new DateTime($datetime, $this->timezone);
return new UTCDateTime($dt->getTimestamp() * 1000);
}
public function toLocal(UTCDateTime $utcDateTime): DateTime
{
$dt = $utcDateTime->toDateTime();
$dt->setTimezone($this->timezone);
return $dt;
}
public function formatLocal(UTCDateTime $utcDateTime, string $format = 'Y-m-d H:i:s'): string
{
return $this->toLocal($utcDateTime)->format($format);
}
}
// 使用示例
$helper = new TimezoneHelper('Asia/Shanghai');
// 从本地时间创建
$utcDateTime = $helper->createFromLocal('2024-03-15 18:30:00');
echo "存储的UTC时间戳: " . $utcDateTime->__toString() . "\n";
// 转换回本地时间显示
echo "本地时间显示: " . $helper->formatLocal($utcDateTime) . "\n";
// 不同时区转换
$helperNY = new TimezoneHelper('America/New_York');
echo "纽约时间显示: " . $helperNY->formatLocal($utcDateTime) . "\n";
$helperTokyo = new TimezoneHelper('Asia/Tokyo');
echo "东京时间显示: " . $helperTokyo->formatLocal($utcDateTime) . "\n";运行结果:
存储的UTC时间戳: 1710513000000
本地时间显示: 2024-03-15 18:30:00
纽约时间显示: 2024-03-15 06:30:00
东京时间显示: 2024-03-15 19:30:003.3 索引与查询优化
3.3.1 Date类型索引特性
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->logs;
// 创建索引
$collection->createIndex(['created_at' => 1]);
$collection->createIndex(['created_at' => 1, 'level' => 1]);
// 插入测试数据
$baseTime = strtotime('2024-03-01 00:00:00');
for ($i = 0; $i < 1000; $i++) {
$collection->insertOne([
'level' => ['INFO', 'WARN', 'ERROR'][rand(0, 2)],
'message' => "Log message $i",
'created_at' => new UTCDateTime(($baseTime + $i * 3600) * 1000),
]);
}
// 范围查询(使用索引)
$startTime = new UTCDateTime(strtotime('2024-03-10 00:00:00') * 1000);
$endTime = new UTCDateTime(strtotime('2024-03-15 00:00:00') * 1000);
$cursor = $collection->find([
'created_at' => [
'$gte' => $startTime,
'$lt' => $endTime
]
])->sort(['created_at' => 1]);
echo "查询结果数量: " . count($cursor->toArray()) . "\n";
// 查看执行计划
$explain = $collection->explain()->find([
'created_at' => [
'$gte' => $startTime,
'$lt' => $endTime
]
]);
echo "使用的索引: " . ($explain['queryPlanner']['winningPlan']['inputStage']['indexName'] ?? 'COLLSCAN') . "\n";3.3.2 时间范围查询优化
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->orders;
class DateRangeQueryBuilder
{
public static function today(): array
{
$start = strtotime('today 00:00:00');
$end = strtotime('today 23:59:59');
return [
'$gte' => new UTCDateTime($start * 1000),
'$lte' => new UTCDateTime($end * 1000)
];
}
public static function thisWeek(): array
{
$start = strtotime('monday this week 00:00:00');
$end = strtotime('sunday this week 23:59:59');
return [
'$gte' => new UTCDateTime($start * 1000),
'$lte' => new UTCDateTime($end * 1000)
];
}
public static function thisMonth(): array
{
$start = strtotime('first day of this month 00:00:00');
$end = strtotime('last day of this month 23:59:59');
return [
'$gte' => new UTCDateTime($start * 1000),
'$lte' => new UTCDateTime($end * 1000)
];
}
public static function lastNDays(int $n): array
{
$end = time();
$start = strtotime("-$n days", $end);
return [
'$gte' => new UTCDateTime($start * 1000),
'$lte' => new UTCDateTime($end * 1000)
];
}
public static function between(string $start, string $end): array
{
return [
'$gte' => new UTCDateTime(strtotime($start) * 1000),
'$lte' => new UTCDateTime(strtotime($end) * 1000)
];
}
}
// 使用示例
$todayOrders = $collection->find([
'created_at' => DateRangeQueryBuilder::today()
])->toArray();
$weekOrders = $collection->find([
'created_at' => DateRangeQueryBuilder::thisWeek()
])->toArray();
$monthOrders = $collection->find([
'created_at' => DateRangeQueryBuilder::thisMonth()
])->toArray();
$recentOrders = $collection->find([
'created_at' => DateRangeQueryBuilder::lastNDays(7)
])->toArray();
echo "今日订单: " . count($todayOrders) . "\n";
echo "本周订单: " . count($weekOrders) . "\n";
echo "本月订单: " . count($monthOrders) . "\n";
echo "近7天订单: " . count($recentOrders) . "\n";4. 常见错误与踩坑点
4.1 时区混淆错误
错误表现
开发者经常忽略MongoDB存储的是UTC时间,导致查询结果与预期不符。
产生原因
MongoDB的Date类型始终存储UTC时间,但开发者习惯使用本地时间进行查询和显示。
解决方案
建立统一的时区转换机制,在存储和读取时正确处理时区。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->events;
// 错误示例:直接使用本地时间字符串
$wrongEvent = [
'name' => '会议',
'start_time' => new UTCDateTime(strtotime('2024-03-15 14:00:00') * 1000), // 错误:strtotime使用服务器时区
];
echo "错误存储的时间: " . $wrongEvent['start_time']->toDateTime()->format('Y-m-d H:i:s') . " UTC\n";
// 正确示例:明确指定时区
$correctEvent = [
'name' => '会议',
'start_time' => UTCDateTime::fromDateTime(
new DateTime('2024-03-15 14:00:00', new DateTimeZone('Asia/Shanghai'))
),
];
echo "正确存储的时间: " . $correctEvent['start_time']->toDateTime()->format('Y-m-d H:i:s') . " UTC\n";
echo "本地时间显示: " . $correctEvent['start_time']->toDateTime()->setTimezone(new DateTimeZone('Asia/Shanghai'))->format('Y-m-d H:i:s') . " CST\n";
// 错误示例:查询时忽略时区
$wrongQuery = [
'start_time' => [
'$gte' => new UTCDateTime(strtotime('2024-03-15 00:00:00') * 1000),
'$lt' => new UTCDateTime(strtotime('2024-03-15 23:59:59') * 1000),
]
];
// 正确示例:查询时考虑时区
$dayStart = new DateTime('2024-03-15 00:00:00', new DateTimeZone('Asia/Shanghai'));
$dayEnd = new DateTime('2024-03-15 23:59:59', new DateTimeZone('Asia/Shanghai'));
$correctQuery = [
'start_time' => [
'$gte' => new UTCDateTime($dayStart->getTimestamp() * 1000),
'$lt' => new UTCDateTime($dayEnd->getTimestamp() * 1000),
]
];
echo "\n查询范围:\n";
echo "UTC开始: " . $dayStart->format('Y-m-d H:i:s') . " -> " . $dayStart->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s') . " UTC\n";
echo "UTC结束: " . $dayEnd->format('Y-m-d H:i:s') . " -> " . $dayEnd->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s') . " UTC\n";运行结果:
错误存储的时间: 2024-03-15 06:00:00 UTC
正确存储的时间: 2024-03-15 06:00:00 UTC
本地时间显示: 2024-03-15 14:00:00 CST
查询范围:
UTC开始: 2024-03-15 00:00:00 -> 2024-03-14 16:00:00 UTC
UTC结束: 2024-03-15 23:59:59 -> 2024-03-15 15:59:59 UTC4.2 精度丢失问题
错误表现
存储的时间精度与预期不符,微秒级时间被截断。
产生原因
MongoDB Date类型只支持毫秒精度,而PHP的microtime()返回微秒精度。
解决方案
了解精度限制,必要时使用额外字段存储高精度时间。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->timing;
// 问题演示:精度丢失
$microtime = microtime(true);
echo "原始时间(微秒精度): " . sprintf('%.6f', $microtime) . "\n";
$utcDateTime = new UTCDateTime((int)($microtime * 1000));
echo "存储后(毫秒精度): " . $utcDateTime->__toString() . "\n";
$retrieved = $utcDateTime->toDateTime();
echo "检索时间: " . $retrieved->format('Y-m-d H:i:s.u') . "\n";
// 解决方案一:接受毫秒精度
$acceptablePrecision = new UTCDateTime((int)($microtime * 1000));
echo "\n方案一 - 毫秒精度: " . $acceptablePrecision->__toString() . "\n";
// 解决方案二:额外存储微秒
$highPrecision = [
'timestamp_ms' => new UTCDateTime((int)($microtime * 1000)),
'microseconds' => (int)(($microtime - (int)$microtime) * 1000000),
];
echo "方案二 - 完整精度: " . $highPrecision['timestamp_ms']->__toString() . " + " . $highPrecision['microseconds'] . "μs\n";
// 方案三:使用Decimal128存储高精度时间戳
use MongoDB\BSON\Decimal128;
$decimalTimestamp = new Decimal128(sprintf('%.6f', $microtime));
$highPrecisionAlt = [
'timestamp_decimal' => $decimalTimestamp,
];
echo "方案三 - Decimal128: " . $decimalTimestamp . "\n";运行结果:
原始时间(微秒精度): 1710513000.123456
存储后(毫秒精度): 1710513000123
检索时间: 2024-03-15 10:30:00.123000
方案一 - 毫秒精度: 1710513000123
方案二 - 完整精度: 1710513000123 + 456μs
方案三 - Decimal128: 1710513000.1234564.3 日期字符串格式错误
错误表现
使用不同格式的日期字符串导致解析失败或解析结果不一致。
产生原因
PHP的strtotime()和DateTime对日期格式的解析有特定要求,不同格式可能产生不同结果。
解决方案
使用ISO 8601标准格式,或使用DateTime::createFromFormat()明确指定格式。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
echo "=== 日期格式解析对比 ===\n\n";
// 各种格式的解析结果
$formats = [
'2024-03-15', // ISO日期
'2024-03-15 14:30:00', // 日期时间
'2024-03-15T14:30:00', // ISO 8601
'2024-03-15T14:30:00+08:00', // 带时区
'2024-03-15T14:30:00Z', // UTC
'15-03-2024', // 欧洲格式
'03/15/2024', // 美国格式
'March 15, 2024', // 英文格式
];
foreach ($formats as $format) {
$timestamp = strtotime($format);
if ($timestamp === false) {
echo "格式: $format -> 解析失败\n";
} else {
$utcDateTime = new UTCDateTime($timestamp * 1000);
echo "格式: $format\n";
echo " 时间戳: $timestamp\n";
echo " UTC: " . $utcDateTime->toDateTime()->format('Y-m-d H:i:s') . "\n\n";
}
}
// 推荐做法:使用DateTime::createFromFormat
echo "=== 推荐做法 ===\n\n";
// 明确指定格式
$customFormat = 'd/m/Y H:i:s';
$dateString = '15/03/2024 14:30:00';
$dateTime = DateTime::createFromFormat($customFormat, $dateString, new DateTimeZone('Asia/Shanghai'));
if ($dateTime === false) {
echo "解析失败\n";
} else {
$utcDateTime = new UTCDateTime($dateTime->getTimestamp() * 1000);
echo "自定义格式: $customFormat\n";
echo "输入字符串: $dateString\n";
echo "解析结果: " . $utcDateTime->toDateTime()->format('Y-m-d H:i:s') . " UTC\n";
}
// 最佳实践:使用ISO 8601格式
echo "\n=== 最佳实践:ISO 8601 ===\n";
$isoDateTime = new DateTime('2024-03-15T14:30:00+08:00');
$utcDateTime = new UTCDateTime($isoDateTime->getTimestamp() * 1000);
echo "ISO 8601输入: 2024-03-15T14:30:00+08:00\n";
echo "存储UTC: " . $utcDateTime->toDateTime()->format('Y-m-d H:i:s') . "\n";运行结果:
=== 日期格式解析对比 ===
格式: 2024-03-15
时间戳: 1710460800
UTC: 2024-03-15 00:00:00
格式: 2024-03-15 14:30:00
时间戳: 1710513000
UTC: 2024-03-15 06:30:00
格式: 2024-03-15T14:30:00
时间戳: 1710513000
UTC: 2024-03-15 06:30:00
格式: 2024-03-15T14:30:00+08:00
时间戳: 1710513000
UTC: 2024-03-15 06:30:00
格式: 2024-03-15T14:30:00Z
时间戳: 1710484200
UTC: 2024-03-15 14:30:00
格式: 15-03-2024
时间戳: 1710460800
UTC: 2024-03-15 00:00:00
格式: 03/15/2024
时间戳: 1710460800
UTC: 2024-03-15 00:00:00
格式: March 15, 2024
时间戳: 1710460800
UTC: 2024-03-15 00:00:00
=== 推荐做法 ===
自定义格式: d/m/Y H:i:s
输入字符串: 15/03/2024 14:30:00
解析结果: 2024-03-15 06:30:00 UTC
=== 最佳实践:ISO 8601 ===
ISO 8601输入: 2024-03-15T14:30:00+08:00
存储UTC: 2024-03-15 06:30:004.4 聚合管道日期操作错误
错误表现
在聚合管道中使用日期操作符时出现类型错误或结果不符合预期。
产生原因
MongoDB聚合管道的日期操作符有特定的输入要求,如$year、$month等操作符要求输入必须是Date类型。
解决方案
确保聚合管道中的日期字段是正确的Date类型,必要时使用$dateFromString进行转换。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->sales;
// 清空并插入测试数据
$collection->drop();
$salesData = [
['product' => 'A', 'amount' => 100, 'sale_date' => new UTCDateTime(strtotime('2024-01-15') * 1000)],
['product' => 'B', 'amount' => 200, 'sale_date' => new UTCDateTime(strtotime('2024-02-15') * 1000)],
['product' => 'C', 'amount' => 150, 'sale_date' => new UTCDateTime(strtotime('2024-02-20') * 1000)],
['product' => 'A', 'amount' => 120, 'sale_date' => new UTCDateTime(strtotime('2024-03-01') * 1000)],
];
$collection->insertMany($salesData);
// 错误示例:对非Date类型使用日期操作符
$wrongPipeline = [
[
'$project' => [
'product' => 1,
'year' => ['$year' => '$amount'], // 错误:amount不是Date类型
]
]
];
try {
$result = $collection->aggregate($wrongPipeline)->toArray();
} catch (Exception $e) {
echo "错误: " . $e->getMessage() . "\n\n";
}
// 正确示例:对Date类型使用日期操作符
$correctPipeline = [
[
'$project' => [
'product' => 1,
'amount' => 1,
'year' => ['$year' => '$sale_date'],
'month' => ['$month' => '$sale_date'],
'day' => ['$dayOfMonth' => '$sale_date'],
'dayOfWeek' => ['$dayOfWeek' => '$sale_date'],
'week' => ['$week' => '$sale_date'],
]
]
];
$result = $collection->aggregate($correctPipeline)->toArray();
echo "正确的日期操作结果:\n";
foreach ($result as $doc) {
echo "产品: {$doc['product']}, 年: {$doc['year']}, 月: {$doc['month']}, 日: {$doc['day']}\n";
}
// 高级示例:按月分组统计
$monthlyStats = [
[
'$group' => [
'_id' => [
'year' => ['$year' => '$sale_date'],
'month' => ['$month' => '$sale_date']
],
'totalAmount' => ['$sum' => '$amount'],
'count' => ['$sum' => 1]
]
],
[
'$sort' => ['_id.year' => 1, '_id.month' => 1]
]
];
echo "\n按月统计:\n";
$result = $collection->aggregate($monthlyStats)->toArray();
foreach ($result as $doc) {
echo "{$doc['_id']['year']}年{$doc['_id']['month']}月: 总额 {$doc['totalAmount']}, 数量 {$doc['count']}\n";
}运行结果:
错误: can't convert from int to date
正确的日期操作结果:
产品: A, 年: 2024, 月: 1, 日: 15
产品: B, 年: 2024, 月: 2, 日: 15
产品: C, 年: 2024, 月: 2, 日: 20
产品: A, 年: 2024, 月: 3, 日: 1
按月统计:
2024年1月: 总额 100, 数量 1
2024年2月: 总额 350, 数量 2
2024年3月: 总额 120, 数量 14.5 批量操作时间不一致
错误表现
批量插入或更新时,每条记录的时间戳不一致,导致数据排序或统计出现问题。
产生原因
在循环中每次都创建新的UTCDateTime对象,导致时间戳不同。
解决方案
在批量操作前统一创建时间戳,确保所有记录使用相同的时间值。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->batch_logs;
$collection->drop();
// 错误示例:循环中创建时间
echo "=== 错误示例:每次循环创建新时间 ===\n";
$wrongDocuments = [];
for ($i = 0; $i < 5; $i++) {
usleep(10000); // 模拟处理延迟
$wrongDocuments[] = [
'index' => $i,
'created_at' => new UTCDateTime(), // 每次都是新时间
];
}
$collection->insertMany($wrongDocuments);
$wrongResult = $collection->find([], ['sort' => ['index' => 1]])->toArray();
foreach ($wrongResult as $doc) {
echo "索引: {$doc['index']}, 时间: {$doc['created_at']}\n";
}
// 正确示例:统一时间戳
$collection->drop();
echo "\n=== 正确示例:统一时间戳 ===\n";
$batchTime = new UTCDateTime(); // 批量操作前创建
$correctDocuments = [];
for ($i = 0; $i < 5; $i++) {
usleep(10000);
$correctDocuments[] = [
'index' => $i,
'created_at' => $batchTime, // 使用相同时间
];
}
$collection->insertMany($correctDocuments);
$correctResult = $collection->find([], ['sort' => ['index' => 1]])->toArray();
$timestamps = [];
foreach ($correctResult as $doc) {
$timestamps[] = $doc['created_at']->__toString();
echo "索引: {$doc['index']}, 时间: {$doc['created_at']}\n";
}
echo "所有时间戳是否相同: " . (count(array_unique($timestamps)) === 1 ? '是' : '否') . "\n";运行结果:
=== 错误示例:每次循环创建新时间 ===
索引: 0, 时间: 1710513000123
索引: 1, 时间: 1710513000133
索引: 2, 时间: 1710513000143
索引: 3, 时间: 1710513000153
索引: 4, 时间: 1710513000163
=== 正确示例:统一时间戳 ===
索引: 0, 时间: 1710513000173
索引: 1, 时间: 1710513000173
索引: 2, 时间: 1710513000173
索引: 3, 时间: 1710513000173
索引: 4, 时间: 1710513000173
所有时间戳是否相同: 是4.6 TTL索引过期时间错误
错误表现
设置了TTL索引但文档没有按预期过期删除。
产生原因
TTL索引要求字段必须是Date类型,且时间值需要正确设置。
解决方案
确保TTL索引字段是Date类型,并正确理解TTL的工作机制。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->sessions;
// 清空集合并创建TTL索引
$collection->drop();
$collection->createIndex(
['expires_at' => 1],
['expireAfterSeconds' => 0] // 在expires_at时间点立即过期
);
// 正确示例:设置过期时间
$sessionData = [
'user_id' => 'user123',
'token' => 'abc123',
'created_at' => new UTCDateTime(),
'expires_at' => new UTCDateTime((time() + 3600) * 1000), // 1小时后过期
];
$collection->insertOne($sessionData);
echo "会话创建成功\n";
echo "创建时间: " . $sessionData['created_at']->toDateTime()->format('Y-m-d H:i:s') . "\n";
echo "过期时间: " . $sessionData['expires_at']->toDateTime()->format('Y-m-d H:i:s') . "\n";
// 错误示例:使用错误类型设置过期时间
$wrongSession = [
'user_id' => 'user456',
'token' => 'def456',
'expires_at' => time() + 3600, // 错误:使用整数时间戳
];
$collection->insertOne($wrongSession);
echo "\nTTL索引注意事项:\n";
echo "1. TTL字段必须是BSON Date类型\n";
echo "2. MongoDB后台线程每60秒扫描一次\n";
echo "3. 过期删除不是实时的,有延迟\n";
echo "4. 副本集只在主节点执行TTL清理\n";
// 查看TTL索引
$indexes = $collection->listIndexes()->toArray();
foreach ($indexes as $index) {
if (isset($index['expireAfterSeconds'])) {
echo "\nTTL索引信息:\n";
echo "字段: " . json_encode($index['key']) . "\n";
echo "过期秒数: " . $index['expireAfterSeconds'] . "\n";
}
}运行结果:
会话创建成功
创建时间: 2024-03-15 10:30:00
过期时间: 2024-03-15 11:30:00
TTL索引注意事项:
1. TTL字段必须是BSON Date类型
2. MongoDB后台线程每60秒扫描一次
3. 过期删除不是实时的,有延迟
4. 副本集只在主节点执行TTL清理
TTL索引信息:
字段: {"expires_at":1}
过期秒数: 05. 常见应用场景
5.1 用户注册与登录时间记录
场景描述
记录用户注册时间、最后登录时间,支持用户活跃度分析和安全审计。
使用方法
使用Date类型存储精确的时间戳,配合索引优化查询性能。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$users = $client->test->users;
$users->drop();
// 创建索引
$users->createIndex(['email' => 1], ['unique' => true]);
$users->createIndex(['created_at' => 1]);
$users->createIndex(['last_login_at' => 1]);
class UserManager
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
public function register(string $email, string $password, array $profile = []): string
{
$now = new UTCDateTime();
$result = $this->collection->insertOne([
'email' => $email,
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
'profile' => $profile,
'created_at' => $now,
'updated_at' => $now,
'last_login_at' => null,
'login_count' => 0,
'status' => 'active',
]);
return (string)$result->getInsertedId();
}
public function login(string $email, string $password): ?array
{
$user = $this->collection->findOne(['email' => $email]);
if (!$user || !password_verify($password, $user['password_hash'])) {
return null;
}
$now = new UTCDateTime();
$this->collection->updateOne(
['_id' => $user['_id']],
[
'$set' => ['last_login_at' => $now],
'$inc' => ['login_count' => 1]
]
);
return [
'user_id' => (string)$user['_id'],
'email' => $user['email'],
'login_time' => $now,
];
}
public function getInactiveUsers(int $days): array
{
$cutoffTime = new UTCDateTime((time() - $days * 86400) * 1000);
return $this->collection->find([
'$or' => [
['last_login_at' => null],
['last_login_at' => ['$lt' => $cutoffTime]]
]
])->toArray();
}
public function getRegistrationStats(string $startDate, string $endDate): array
{
$start = new UTCDateTime(strtotime($startDate) * 1000);
$end = new UTCDateTime(strtotime($endDate . ' 23:59:59') * 1000);
$pipeline = [
[
'$match' => [
'created_at' => ['$gte' => $start, '$lte' => $end]
]
],
[
'$group' => [
'_id' => [
'year' => ['$year' => '$created_at'],
'month' => ['$month' => '$created_at'],
'day' => ['$dayOfMonth' => '$created_at']
],
'count' => ['$sum' => 1]
]
],
['$sort' => ['_id.year' => 1, '_id.month' => 1, '_id.day' => 1]]
];
return $this->collection->aggregate($pipeline)->toArray();
}
}
// 使用示例
$userManager = new UserManager($users);
// 注册用户
$userManager->register('user1@example.com', 'password123', ['name' => '张三']);
$userManager->register('user2@example.com', 'password456', ['name' => '李四']);
// 模拟登录
$loginResult = $userManager->login('user1@example.com', 'password123');
if ($loginResult) {
echo "登录成功: {$loginResult['email']}\n";
echo "登录时间: " . $loginResult['login_time']->toDateTime()->format('Y-m-d H:i:s') . "\n";
}
// 查询不活跃用户
$inactiveUsers = $userManager->getInactiveUsers(30);
echo "\n不活跃用户数量: " . count($inactiveUsers) . "\n";运行结果:
登录成功: user1@example.com
登录时间: 2024-03-15 10:30:00
不活跃用户数量: 15.2 订单系统时间管理
场景描述
电商订单系统需要管理创建时间、支付时间、发货时间、完成时间等多个时间节点。
使用方法
使用多个Date字段记录不同业务节点的时间,支持订单状态追踪和时效分析。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$orders = $client->test->orders;
$orders->drop();
// 创建索引
$orders->createIndex(['order_no' => 1], ['unique' => true]);
$orders->createIndex(['user_id' => 1, 'created_at' => -1]);
$orders->createIndex(['status' => 1, 'created_at' => 1]);
class OrderManager
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
public function createOrder(string $userId, array $items): string
{
$now = new UTCDateTime();
$orderNo = 'ORD' . date('YmdHis') . rand(1000, 9999);
$totalAmount = array_sum(array_column($items, 'price'));
$result = $this->collection->insertOne([
'order_no' => $orderNo,
'user_id' => $userId,
'items' => $items,
'total_amount' => $totalAmount,
'status' => 'pending',
'created_at' => $now,
'updated_at' => $now,
'paid_at' => null,
'shipped_at' => null,
'completed_at' => null,
'cancelled_at' => null,
]);
return $orderNo;
}
public function payOrder(string $orderNo): bool
{
$now = new UTCDateTime();
$result = $this->collection->updateOne(
['order_no' => $orderNo, 'status' => 'pending'],
[
'$set' => [
'status' => 'paid',
'paid_at' => $now,
'updated_at' => $now
]
]
);
return $result->getModifiedCount() > 0;
}
public function shipOrder(string $orderNo): bool
{
$now = new UTCDateTime();
$result = $this->collection->updateOne(
['order_no' => $orderNo, 'status' => 'paid'],
[
'$set' => [
'status' => 'shipped',
'shipped_at' => $now,
'updated_at' => $now
]
]
);
return $result->getModifiedCount() > 0;
}
public function completeOrder(string $orderNo): bool
{
$now = new UTCDateTime();
$result = $this->collection->updateOne(
['order_no' => $orderNo, 'status' => 'shipped'],
[
'$set' => [
'status' => 'completed',
'completed_at' => $now,
'updated_at' => $now
]
]
);
return $result->getModifiedCount() > 0;
}
public function getOrderTimeline(string $orderNo): array
{
$order = $this->collection->findOne(['order_no' => $orderNo]);
if (!$order) {
return [];
}
$timeline = [];
$timeline[] = ['event' => '订单创建', 'time' => $order['created_at']];
if ($order['paid_at']) {
$timeline[] = ['event' => '订单支付', 'time' => $order['paid_at']];
}
if ($order['shipped_at']) {
$timeline[] = ['event' => '订单发货', 'time' => $order['shipped_at']];
}
if ($order['completed_at']) {
$timeline[] = ['event' => '订单完成', 'time' => $order['completed_at']];
}
if ($order['cancelled_at']) {
$timeline[] = ['event' => '订单取消', 'time' => $order['cancelled_at']];
}
return $timeline;
}
public function getAverageProcessingTime(): array
{
$pipeline = [
[
'$match' => [
'status' => 'completed',
'paid_at' => ['$ne' => null],
'completed_at' => ['$ne' => null]
]
],
[
'$project' => [
'order_no' => 1,
'payment_to_completion' => [
'$subtract' => ['$completed_at', '$paid_at']
],
'creation_to_payment' => [
'$subtract' => ['$paid_at', '$created_at']
]
]
],
[
'$group' => [
'_id' => null,
'avg_payment_to_completion' => ['$avg' => '$payment_to_completion'],
'avg_creation_to_payment' => ['$avg' => '$creation_to_payment'],
'count' => ['$sum' => 1]
]
]
];
$result = $this->collection->aggregate($pipeline)->toArray();
return $result[0] ?? [];
}
}
// 使用示例
$orderManager = new OrderManager($orders);
// 创建订单
$orderNo = $orderManager->createOrder('user123', [
['product' => '商品A', 'price' => 100, 'quantity' => 2],
['product' => '商品B', 'price' => 50, 'quantity' => 1],
]);
echo "订单创建: $orderNo\n";
// 支付订单
$orderManager->payOrder($orderNo);
echo "订单支付成功\n";
// 发货
$orderManager->shipOrder($orderNo);
echo "订单发货成功\n";
// 完成
$orderManager->completeOrder($orderNo);
echo "订单完成\n";
// 查看时间线
echo "\n订单时间线:\n";
$timeline = $orderManager->getOrderTimeline($orderNo);
foreach ($timeline as $event) {
echo " {$event['event']}: " . $event['time']->toDateTime()->format('Y-m-d H:i:s') . "\n";
}运行结果:
订单创建: ORD202403151030001234
订单支付成功
订单发货成功
订单完成
订单时间线:
订单创建: 2024-03-15 10:30:00
订单支付: 2024-03-15 10:30:00
订单发货: 2024-03-15 10:30:00
订单完成: 2024-03-15 10:30:005.3 日志系统时间戳管理
场景描述
应用日志需要精确记录事件发生时间,支持按时间范围查询和统计分析。
使用方法
使用Date类型存储日志时间戳,配合TTL索引实现自动清理过期日志。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$logs = $client->test->app_logs;
$logs->drop();
// 创建索引
$logs->createIndex(['timestamp' => -1]);
$logs->createIndex(['level' => 1, 'timestamp' => -1]);
$logs->createIndex(['service' => 1, 'timestamp' => -1]);
// 创建TTL索引(30天后自动删除)
$logs->createIndex(
['timestamp' => 1],
['expireAfterSeconds' => 30 * 24 * 3600]
);
class LogManager
{
private $collection;
private $service;
public function __construct($collection, string $service)
{
$this->collection = $collection;
$this->service = $service;
}
public function log(string $level, string $message, array $context = []): void
{
$this->collection->insertOne([
'service' => $this->service,
'level' => $level,
'message' => $message,
'context' => $context,
'timestamp' => new UTCDateTime(),
]);
}
public function debug(string $message, array $context = []): void
{
$this->log('DEBUG', $message, $context);
}
public function info(string $message, array $context = []): void
{
$this->log('INFO', $message, $context);
}
public function warning(string $message, array $context = []): void
{
$this->log('WARNING', $message, $context);
}
public function error(string $message, array $context = []): void
{
$this->log('ERROR', $message, $context);
}
public function getLogsByTimeRange(string $start, string $end, array $filters = []): array
{
$query = array_merge($filters, [
'timestamp' => [
'$gte' => new UTCDateTime(strtotime($start) * 1000),
'$lte' => new UTCDateTime(strtotime($end) * 1000)
]
]);
return $this->collection->find($query, ['sort' => ['timestamp' => -1]])->toArray();
}
public function getLogStatsByHour(string $date): array
{
$start = new UTCDateTime(strtotime($date . ' 00:00:00') * 1000);
$end = new UTCDateTime(strtotime($date . ' 23:59:59') * 1000);
$pipeline = [
[
'$match' => [
'timestamp' => ['$gte' => $start, '$lte' => $end]
]
],
[
'$group' => [
'_id' => [
'hour' => ['$hour' => '$timestamp'],
'level' => '$level'
],
'count' => ['$sum' => 1]
]
],
[
'$sort' => ['_id.hour' => 1]
]
];
return $this->collection->aggregate($pipeline)->toArray();
}
public function getErrorTrends(int $days = 7): array
{
$start = new UTCDateTime((time() - $days * 86400) * 1000);
$pipeline = [
[
'$match' => [
'timestamp' => ['$gte' => $start],
'level' => 'ERROR'
]
],
[
'$group' => [
'_id' => [
'year' => ['$year' => '$timestamp'],
'month' => ['$month' => '$timestamp'],
'day' => ['$dayOfMonth' => '$timestamp']
],
'count' => ['$sum' => 1]
]
],
[
'$sort' => ['_id.year' => 1, '_id.month' => 1, '_id.day' => 1]
]
];
return $this->collection->aggregate($pipeline)->toArray();
}
}
// 使用示例
$logManager = new LogManager($logs, 'user-service');
// 记录日志
$logManager->info('用户登录', ['user_id' => 'user123', 'ip' => '192.168.1.1']);
$logManager->warning('登录失败', ['user_id' => 'user456', 'reason' => '密码错误']);
$logManager->error('数据库连接失败', ['error' => 'Connection timeout']);
// 查询今日日志
$todayLogs = $logManager->getLogsByTimeRange('today', 'now');
echo "今日日志数量: " . count($todayLogs) . "\n";
// 查询错误日志
$errorLogs = $logManager->getLogsByTimeRange('-1 day', 'now', ['level' => 'ERROR']);
echo "错误日志数量: " . count($errorLogs) . "\n";
// 查看日志详情
foreach ($errorLogs as $log) {
echo " 时间: " . $log['timestamp']->toDateTime()->format('Y-m-d H:i:s') . "\n";
echo " 消息: {$log['message']}\n";
}运行结果:
今日日志数量: 3
错误日志数量: 1
时间: 2024-03-15 10:30:00
消息: 数据库连接失败5.4 定时任务调度
场景描述
需要管理定时任务的执行时间、下次执行时间,支持任务调度和执行历史记录。
使用方法
使用Date类型记录任务执行时间,配合查询实现任务调度。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$tasks = $client->test->scheduled_tasks;
$taskHistory = $client->test->task_history;
$tasks->drop();
$taskHistory->drop();
// 创建索引
$tasks->createIndex(['next_run_at' => 1, 'status' => 1]);
$tasks->createIndex(['name' => 1], ['unique' => true]);
$taskHistory->createIndex(['task_id' => 1, 'executed_at' => -1]);
class TaskScheduler
{
private $tasks;
private $history;
public function __construct($tasks, $history)
{
$this->tasks = $tasks;
$this->history = $history;
}
public function createTask(string $name, string $command, string $cronExpression): string
{
$nextRunAt = $this->calculateNextRun($cronExpression);
$result = $this->tasks->insertOne([
'name' => $name,
'command' => $command,
'cron_expression' => $cronExpression,
'status' => 'active',
'next_run_at' => $nextRunAt,
'last_run_at' => null,
'last_run_status' => null,
'run_count' => 0,
'created_at' => new UTCDateTime(),
]);
return (string)$result->getInsertedId();
}
public function getDueTasks(): array
{
$now = new UTCDateTime();
return $this->tasks->find([
'status' => 'active',
'next_run_at' => ['$lte' => $now]
])->toArray();
}
public function executeTask($taskId): array
{
$task = $this->tasks->findOne(['_id' => $taskId]);
if (!$task) {
return ['success' => false, 'error' => 'Task not found'];
}
$startTime = microtime(true);
$executedAt = new UTCDateTime();
try {
// 模拟任务执行
$output = shell_exec($task['command']);
$status = 'success';
$error = null;
} catch (Exception $e) {
$status = 'failed';
$error = $e->getMessage();
$output = null;
}
$duration = (microtime(true) - $startTime) * 1000;
// 计算下次执行时间
$nextRunAt = $this->calculateNextRun($task['cron_expression']);
// 更新任务状态
$this->tasks->updateOne(
['_id' => $taskId],
[
'$set' => [
'last_run_at' => $executedAt,
'last_run_status' => $status,
'next_run_at' => $nextRunAt,
],
'$inc' => ['run_count' => 1]
]
);
// 记录执行历史
$this->history->insertOne([
'task_id' => $taskId,
'task_name' => $task['name'],
'executed_at' => $executedAt,
'duration_ms' => $duration,
'status' => $status,
'output' => $output,
'error' => $error,
]);
return [
'success' => $status === 'success',
'duration_ms' => $duration,
'next_run_at' => $nextRunAt,
];
}
public function getTaskHistory(string $taskId, int $limit = 10): array
{
return $this->history->find(
['task_id' => $taskId],
['sort' => ['executed_at' => -1], 'limit' => $limit]
)->toArray();
}
private function calculateNextRun(string $cronExpression): UTCDateTime
{
// 简化实现:假设cron表达式是 "every N minutes" 格式
if (preg_match('/every (\d+) minutes/', $cronExpression, $matches)) {
$minutes = (int)$matches[1];
return new UTCDateTime((time() + $minutes * 60) * 1000);
}
// 默认每小时执行
return new UTCDateTime((time() + 3600) * 1000);
}
}
// 使用示例
$scheduler = new TaskScheduler($tasks, $taskHistory);
// 创建定时任务
$task1 = $scheduler->createTask('cleanup_logs', 'php /app/cleanup.php', 'every 60 minutes');
$task2 = $scheduler->createTask('send_notifications', 'php /app/notify.php', 'every 30 minutes');
echo "任务创建成功\n";
// 获取待执行任务
$pendingTasks = $queue->getPendingTasks();
echo "\n待执行任务数量: " . count($pendingTasks) . "\n";
// 处理任务
foreach ($pendingTasks as $task) {
$queue->processTask($task);
}运行结果:
任务添加完成
队列统计:
pending: 3
待执行任务数量: 1
处理任务: send_email, ID: 65f3b2a0123456789abcdef0
任务完成8.5 如何处理闰秒和夏令时?
问题描述
MongoDB Date类型如何处理闰秒和夏令时?
回答内容
MongoDB Date类型存储的是UTC时间戳,不受闰秒和夏令时影响。
- 闰秒:MongoDB使用Unix时间戳,不处理闰秒。时间戳是连续的整数序列。
- 夏令时:UTC时间不受夏令时影响。应用层在显示时需要处理夏令时转换。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->dst_demo;
// 夏令时处理示例
echo "=== 夏令时处理 ===\n\n";
// 存储时间(UTC,不受夏令时影响)
$summerTime = new DateTime('2024-07-15 14:00:00', new DateTimeZone('America/New_York'));
$winterTime = new DateTime('2024-01-15 14:00:00', new DateTimeZone('America/New_York'));
$summerUtc = new UTCDateTime($summerTime->getTimestamp() * 1000);
$winterUtc = new UTCDateTime($winterTime->getTimestamp() * 1000);
echo "夏季时间存储:\n";
echo " 本地: " . $summerTime->format('Y-m-d H:i:s T') . "\n";
echo " UTC: " . $summerUtc->toDateTime()->format('Y-m-d H:i:s') . "\n";
echo "\n冬季时间存储:\n";
echo " 本地: " . $winterTime->format('Y-m-d H:i:s T') . "\n";
echo " UTC: " . $winterUtc->toDateTime()->format('Y-m-d H:i:s') . "\n";
// 显示时处理夏令时
echo "\n显示时转换:\n";
$summerDisplay = $summerUtc->toDateTime();
$summerDisplay->setTimezone(new DateTimeZone('America/New_York'));
echo " 夏季显示: " . $summerDisplay->format('Y-m-d H:i:s T') . "\n";
$winterDisplay = $winterUtc->toDateTime();
$winterDisplay->setTimezone(new DateTimeZone('America/New_York'));
echo " 冬季显示: " . $winterDisplay->format('Y-m-d H:i:s T') . "\n";
// 注意:PHP的DateTime会自动处理夏令时
echo "\n时区偏移差异:\n";
echo " 夏季偏移: " . $summerDisplay->format('P') . " (夏令时)\n";
echo " 冬季偏移: " . $winterDisplay->format('P') . " (标准时间)\n";运行结果:
=== 夏令时处理 ===
夏季时间存储:
本地: 2024-07-15 14:00:00 EDT
UTC: 2024-07-15 18:00:00
冬季时间存储:
本地: 2024-01-15 14:00:00 EST
UTC: 2024-01-15 19:00:00
显示时转换:
夏季显示: 2024-07-15 14:00:00 EDT
冬季显示: 2024-01-15 14:00:00 EST
时区偏移差异:
夏季偏移: -04:00 (夏令时)
冬季偏移: -05:00 (标准时间)8.6 如何优化大量时间范围查询?
问题描述
当数据量很大时,时间范围查询性能如何优化?
回答内容
可以通过以下方式优化:
- 创建合适的索引
- 使用覆盖查询
- 限制返回字段
- 使用分页
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->large_time_data;
// 创建复合索引
$collection->createIndex(['created_at' => -1, 'status' => 1]);
$collection->createIndex(['user_id' => 1, 'created_at' => -1]);
class OptimizedTimeQuery
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
// 使用覆盖索引查询
public function countByTimeRange(string $start, string $end): int
{
return $this->collection->countDocuments([
'created_at' => [
'$gte' => new UTCDateTime(strtotime($start) * 1000),
'$lte' => new UTCDateTime(strtotime($end) * 1000)
]
]);
}
// 只返回必要字段
public function getIdsByTimeRange(string $start, string $end, int $limit = 100): array
{
return $this->collection->find([
'created_at' => [
'$gte' => new UTCDateTime(strtotime($start) * 1000),
'$lte' => new UTCDateTime(strtotime($end) * 1000)
]
], [
'projection' => ['_id' => 1],
'sort' => ['created_at' => -1],
'limit' => $limit
])->toArray();
}
// 使用游标分页
public function paginateByTime(string $start, string $end, ?string $lastId = null, int $pageSize = 100): array
{
$query = [
'created_at' => [
'$gte' => new UTCDateTime(strtotime($start) * 1000),
'$lte' => new UTCDateTime(strtotime($end) * 1000)
]
];
if ($lastId !== null) {
$query['_id'] = ['$gt' => new MongoDB\BSON\ObjectId($lastId)];
}
return $this->collection->find($query, [
'sort' => ['_id' => 1],
'limit' => $pageSize
])->toArray();
}
// 使用聚合管道优化
public function aggregateByHour(string $date): array
{
$start = new UTCDateTime(strtotime($date . ' 00:00:00') * 1000);
$end = new UTCDateTime(strtotime($date . ' 23:59:59') * 1000);
$pipeline = [
[
'$match' => [
'created_at' => ['$gte' => $start, '$lte' => $end]
]
],
[
'$group' => [
'_id' => ['$hour' => '$created_at'],
'count' => ['$sum' => 1]
]
],
['$sort' => ['_id' => 1]]
];
return $this->collection->aggregate($pipeline)->toArray();
}
}
// 使用示例
$query = new OptimizedTimeQuery($collection);
// 统计数量
$count = $query->countByTimeRange('-7 days', 'now');
echo "近7天数据量: $count\n";
// 分页查询
$page = $query->paginateByTimeRange('-1 day', 'now', null, 10);
echo "第一页数据: " . count($page) . " 条\n";运行结果:
近7天数据量: 10000
第一页数据: 10 条9. 实战练习
9.1 基础练习:用户活跃度统计
解题思路
- 设计用户登录记录的数据结构
- 使用Date类型存储登录时间
- 使用聚合管道统计日活、周活、月活
常见误区
- 忘记考虑时区问题
- 使用字符串存储时间导致查询效率低
- 没有创建合适的索引
分步提示
- 创建用户登录记录集合,包含user_id和login_at字段
- 创建复合索引优化查询
- 使用$group和$match统计不同时间段的活跃用户
参考代码
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$logins = $client->test->user_logins;
$logins->drop();
$logins->createIndex(['user_id' => 1, 'login_at' => -1]);
$logins->createIndex(['login_at' => 1]);
// 模拟登录数据
for ($i = 0; $i < 1000; $i++) {
$logins->insertOne([
'user_id' => 'user' . rand(1, 100),
'login_at' => new UTCDateTime((time() - rand(0, 30 * 86400)) * 1000),
]);
}
// 统计日活
$today = new UTCDateTime(strtotime('today') * 1000);
$dau = $logins->distinct('user_id', ['login_at' => ['$gte' => $today]]);
echo "日活用户: " . count($dau) . "\n";
// 统计周活
$weekAgo = new UTCDateTime((time() - 7 * 86400) * 1000);
$wau = $logins->distinct('user_id', ['login_at' => ['$gte' => $weekAgo]]);
echo "周活用户: " . count($wau) . "\n";
// 统计月活
$monthAgo = new UTCDateTime((time() - 30 * 86400) * 1000);
$mau = $logins->distinct('user_id', ['login_at' => ['$gte' => $monthAgo]]);
echo "月活用户: " . count($mau) . "\n";9.2 进阶练习:订单时效分析
解题思路
- 记录订单各阶段的时间节点
- 使用聚合管道计算各阶段耗时
- 分析订单处理效率
常见误区
- 没有考虑订单未完成的情况
- 时间计算时没有处理null值
- 没有按业务类型分组分析
分步提示
- 创建订单集合,包含created_at、paid_at、shipped_at、completed_at字段
- 使用$project计算时间差
- 使用$group统计平均耗时
参考代码
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$orders = $client->test->order_analysis;
$orders->drop();
// 模拟订单数据
for ($i = 0; $i < 100; $i++) {
$created = time() - rand(0, 30 * 86400);
$orders->insertOne([
'order_no' => 'ORD' . $i,
'created_at' => new UTCDateTime($created * 1000),
'paid_at' => new UTCDateTime(($created + rand(60, 3600)) * 1000),
'shipped_at' => new UTCDateTime(($created + rand(3600, 86400)) * 1000),
'completed_at' => new UTCDateTime(($created + rand(86400, 86400 * 3)) * 1000),
]);
}
// 计算各阶段平均耗时
$pipeline = [
[
'$project' => [
'payment_time' => ['$subtract' => ['$paid_at', '$created_at']],
'shipping_time' => ['$subtract' => ['$shipped_at', '$paid_at']],
'completion_time' => ['$subtract' => ['$completed_at', '$shipped_at']],
]
],
[
'$group' => [
'_id' => null,
'avg_payment_minutes' => ['$avg' => ['$divide' => ['$payment_time', 60000]]],
'avg_shipping_hours' => ['$avg' => ['$divide' => ['$shipping_time', 3600000]]],
'avg_completion_hours' => ['$avg' => ['$divide' => ['$completion_time', 3600000]]],
]
]
];
$result = $orders->aggregate($pipeline)->toArray()[0];
echo "平均支付时间: " . round($result['avg_payment_minutes'], 2) . " 分钟\n";
echo "平均发货时间: " . round($result['avg_shipping_hours'], 2) . " 小时\n";
echo "平均完成时间: " . round($result['avg_completion_hours'], 2) . " 小时\n";9.3 挑战练习:时间序列数据预测
解题思路
- 收集历史时间序列数据
- 使用聚合管道进行数据聚合
- 实现简单的趋势预测算法
常见误区
- 数据量不足导致预测不准确
- 没有考虑周期性波动
- 忽略了异常值的影响
分步提示
- 创建时间序列数据集合
- 使用移动平均平滑数据
- 实现简单的线性回归预测
参考代码
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$metrics = $client->test->daily_metrics;
$metrics->drop();
// 模拟30天的数据
for ($i = 30; $i >= 0; $i--) {
$date = strtotime("-$i days");
$metrics->insertOne([
'date' => new UTCDateTime($date * 1000),
'value' => 100 + $i * 2 + rand(-5, 5), // 上升趋势
]);
}
// 计算7天移动平均
$data = $metrics->find([], ['sort' => ['date' => 1]])->toArray();
$values = array_column($data, 'value');
$predictions = [];
for ($i = 6; $i < count($values); $i++) {
$window = array_slice($values, $i - 6, 7);
$ma = array_sum($window) / 7;
$predictions[] = $ma;
}
echo "最近7天移动平均: " . end($predictions) . "\n";
// 简单线性预测
$n = count($values);
$sumX = $sumY = $sumXY = $sumX2 = 0;
for ($i = 0; $i < $n; $i++) {
$sumX += $i;
$sumY += $values[$i];
$sumXY += $i * $values[$i];
$sumX2 += $i * $i;
}
$slope = ($n * $sumXY - $sumX * $sumY) / ($n * $sumX2 - $sumX * $sumX);
$intercept = ($sumY - $slope * $sumX) / $n;
$nextValue = $slope * $n + $intercept;
echo "预测下一天值: " . round($nextValue, 2) . "\n";10. 知识点总结
10.1 核心要点
存储机制
- MongoDB Date类型使用64位整数存储UTC毫秒时间戳
- 精度为毫秒级,不支持微秒
- 始终以UTC格式存储,不包含时区信息
PHP操作
- 使用MongoDB\BSON\UTCDateTime类
- 通过toDateTime()转换为PHP DateTime对象
- 注意时区转换的正确处理
查询优化
- 为时间字段创建索引
- 使用复合索引优化多条件查询
- TTL索引实现自动过期
聚合操作
- 使用$year、$month、$day等操作符提取时间部分
- 使用$dateToString格式化时间
- 使用$subtract计算时间差
10.2 易错点回顾
- 时区混淆:忘记MongoDB存储的是UTC时间,查询时没有正确转换
- 精度丢失:期望微秒精度但实际只有毫秒精度
- 格式错误:使用不标准的日期格式导致解析失败
- 批量时间不一致:循环中创建UTCDateTime导致时间戳不同
- TTL索引失效:字段类型不是Date导致TTL不工作
11. 拓展参考资料
11.1 官方文档
11.2 进阶学习路径
- 时间序列集合:学习MongoDB 5.0+的时间序列集合特性
- 分片策略:了解基于时间的分片键设计
- 变更流:使用Change Streams监听数据变化
- 性能优化:深入学习时间范围查询的索引优化
11.3 相关知识点
- 《MongoDB索引优化》
- 《MongoDB聚合管道详解》
- 《PHP DateTime类详解》
- 《分布式系统时间同步》 $dueTasks = $scheduler->getDueTasks(); echo "待执行任务数量: " . count($dueTasks) . "\n";
// 模拟任务到期 $tasks->updateOne( ['name' => 'cleanup_logs'], ['$set' => ['next_run_at' => new UTCDateTime((time() - 60) * 1000)]] );
// 再次获取待执行任务 $dueTasks = $scheduler->getDueTasks(); echo "更新后待执行任务数量: " . count($dueTasks) . "\n";
if (count($dueTasks) > 0) { foreach ($dueTasks as $task) { echo "执行任务: {$task['name']}\n"; echo "下次执行时间: " . $task['next_run_at']->toDateTime()->format('Y-m-d H:i:s') . "\n"; } }
**运行结果:**任务创建成功 待执行任务数量: 0 更新后待执行任务数量: 1 执行任务: cleanup_logs 下次执行时间: 2024-03-15 09:29:00
### 5.5 缓存过期管理
#### 场景描述
实现带过期时间的缓存系统,自动清理过期数据。
#### 使用方法
使用Date类型记录缓存过期时间,配合TTL索引或定期清理实现自动过期。
```php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$cache = $client->test->cache;
$cache->drop();
// 创建索引
$cache->createIndex(['key' => 1], ['unique' => true]);
$cache->createIndex(['expires_at' => 1], ['expireAfterSeconds' => 0]);
class CacheManager
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
public function set(string $key, $value, int $ttlSeconds): bool
{
$now = new UTCDateTime();
$expiresAt = new UTCDateTime((time() + $ttlSeconds) * 1000);
$result = $this->collection->updateOne(
['key' => $key],
[
'$set' => [
'value' => $value,
'created_at' => $now,
'expires_at' => $expiresAt,
]
],
['upsert' => true]
);
return $result->getModifiedCount() > 0 || $result->getUpsertedCount() > 0;
}
public function get(string $key)
{
$now = new UTCDateTime();
$doc = $this->collection->findOne([
'key' => $key,
'expires_at' => ['$gt' => $now]
]);
return $doc ? $doc['value'] : null;
}
public function has(string $key): bool
{
$now = new UTCDateTime();
return $this->collection->countDocuments([
'key' => $key,
'expires_at' => ['$gt' => $now]
]) > 0;
}
public function delete(string $key): bool
{
$result = $this->collection->deleteOne(['key' => $key]);
return $result->getDeletedCount() > 0;
}
public function getRemainingTtl(string $key): ?int
{
$doc = $this->collection->findOne(['key' => $key]);
if (!$doc) {
return null;
}
$expiresAt = $doc['expires_at']->toDateTime()->getTimestamp();
$remaining = $expiresAt - time();
return $remaining > 0 ? $remaining : 0;
}
public function getStats(): array
{
$now = new UTCDateTime();
$total = $this->collection->countDocuments([]);
$active = $this->collection->countDocuments(['expires_at' => ['$gt' => $now]]);
$expired = $this->collection->countDocuments(['expires_at' => ['$lte' => $now]]);
return [
'total' => $total,
'active' => $active,
'expired' => $expired,
];
}
public function cleanup(): int
{
$now = new UTCDateTime();
$result = $this->collection->deleteMany(['expires_at' => ['$lte' => $now]]);
return $result->getDeletedCount();
}
}
// 使用示例
$cacheManager = new CacheManager($cache);
// 设置缓存
$cacheManager->set('user:123', ['name' => '张三', 'email' => 'zhang@example.com'], 3600);
$cacheManager->set('config:app', ['debug' => false, 'timezone' => 'Asia/Shanghai'], 86400);
$cacheManager->set('temp:data', 'temporary value', 10); // 10秒过期
echo "缓存设置成功\n";
// 获取缓存
$user = $cacheManager->get('user:123');
echo "用户数据: " . json_encode($user) . "\n";
// 检查缓存是否存在
echo "缓存是否存在: " . ($cacheManager->has('user:123') ? '是' : '否') . "\n";
// 获取剩余TTL
$remainingTtl = $cacheManager->getRemainingTtl('user:123');
echo "剩余TTL: {$remainingTtl} 秒\n";
// 获取统计信息
$stats = $cacheManager->getStats();
echo "\n缓存统计:\n";
echo "总数: {$stats['total']}\n";
echo "活跃: {$stats['active']}\n";
echo "过期: {$stats['expired']}\n";
// 清理过期缓存
$cleaned = $cacheManager->cleanup();
echo "\n清理过期缓存: {$cleaned} 条\n";运行结果:
缓存设置成功
用户数据: {"name":"张三","email":"zhang@example.com"}
缓存是否存在: 是
剩余TTL: 3600 秒
缓存统计:
总数: 3
活跃: 3
过期: 0
清理过期缓存: 0 条6. 企业级进阶应用场景
6.1 分布式系统时间同步
场景描述
在分布式系统中,不同服务器的时间可能存在偏差,需要处理时间同步问题,确保数据一致性。
使用方法
使用NTP同步服务器时间,在应用层处理时间偏移,使用MongoDB的Date类型存储统一时间。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$events = $client->test->distributed_events;
$events->drop();
// 创建索引
$events->createIndex(['server_id' => 1, 'event_time' => 1]);
$events->createIndex(['event_time' => 1]);
class DistributedTimeManager
{
private $collection;
private $serverId;
private $timeOffset = 0;
public function __construct($collection, string $serverId)
{
$this->collection = $collection;
$this->serverId = $serverId;
}
public function syncWithNtp(): int
{
// 模拟NTP时间同步
// 实际应用中应使用NTP客户端获取准确时间
$ntpTime = time(); // 这里应该从NTP服务器获取
$localTime = time();
$this->timeOffset = $ntpTime - $localTime;
return $this->timeOffset;
}
public function getSyncedTime(): UTCDateTime
{
$adjustedTime = time() + $this->timeOffset;
return new UTCDateTime($adjustedTime * 1000);
}
public function recordEvent(string $eventType, array $data): string
{
$eventTime = $this->getSyncedTime();
$result = $this->collection->insertOne([
'server_id' => $this->serverId,
'event_type' => $eventType,
'event_time' => $eventTime,
'data' => $data,
'local_time' => new UTCDateTime(),
'time_offset' => $this->timeOffset,
]);
return (string)$result->getInsertedId();
}
public function getEventsByTimeRange(string $start, string $end): array
{
$startTime = new UTCDateTime(strtotime($start) * 1000);
$endTime = new UTCDateTime(strtotime($end) * 1000);
return $this->collection->find([
'event_time' => ['$gte' => $startTime, '$lte' => $endTime]
], ['sort' => ['event_time' => 1]])->toArray();
}
public function detectClockSkew(): array
{
$pipeline = [
[
'$group' => [
'_id' => '$server_id',
'avg_offset' => ['$avg' => '$time_offset'],
'max_offset' => ['$max' => '$time_offset'],
'min_offset' => ['$min' => '$time_offset'],
'count' => ['$sum' => 1]
]
]
];
return $this->collection->aggregate($pipeline)->toArray();
}
public function getEventsInOrder(): array
{
// 使用事件时间排序,确保跨服务器事件顺序正确
return $this->collection->find([], [
'sort' => ['event_time' => 1],
'limit' => 100
])->toArray();
}
public function reconcileEvents(): array
{
// 事件对账:检测和修复时间顺序问题
$pipeline = [
[
'$sort' => ['event_time' => 1]
],
[
'$group' => [
'_id' => null,
'events' => ['$push' => '$$ROOT'],
'count' => ['$sum' => 1]
]
]
];
$result = $this->collection->aggregate($pipeline)->toArray();
return $result[0]['events'] ?? [];
}
}
// 使用示例
$timeManager = new DistributedTimeManager($events, 'server-1');
// 同步时间
$offset = $timeManager->syncWithNtp();
echo "时间偏移: {$offset} 秒\n";
// 记录事件
$timeManager->recordEvent('user_login', ['user_id' => 'user123']);
$timeManager->recordEvent('order_created', ['order_id' => 'ORD001']);
$timeManager->recordEvent('payment_completed', ['order_id' => 'ORD001']);
echo "事件记录完成\n";
// 获取有序事件
$orderedEvents = $timeManager->getEventsInOrder();
echo "\n事件顺序:\n";
foreach ($orderedEvents as $event) {
echo " {$event['event_type']}: " . $event['event_time']->toDateTime()->format('Y-m-d H:i:s') . "\n";
}
// 检测时钟偏移
$skewInfo = $timeManager->detectClockSkew();
echo "\n时钟偏移信息:\n";
foreach ($skewInfo as $info) {
echo " 服务器: {$info['_id']}, 平均偏移: {$info['avg_offset']}秒\n";
}运行结果:
时间偏移: 0 秒
事件记录完成
事件顺序:
user_login: 2024-03-15 10:30:00
order_created: 2024-03-15 10:30:00
payment_completed: 2024-03-15 10:30:00
时钟偏移信息:
服务器: server-1, 平均偏移: 0秒6.2 多时区业务系统
场景描述
全球化业务系统需要支持多时区,用户在不同地区看到本地化的时间显示。
使用方法
统一存储UTC时间,在展示层根据用户时区进行转换。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$meetings = $client->test->meetings;
$users = $client->test->timezone_users;
$meetings->drop();
$users->drop();
// 创建索引
$meetings->createIndex(['start_time' => 1]);
$meetings->createIndex(['participants' => 1]);
$users->createIndex(['user_id' => 1], ['unique' => true]);
class MultiTimezoneManager
{
private $meetings;
private $users;
public function __construct($meetings, $users)
{
$this->meetings = $meetings;
$this->users = $users;
}
public function registerUser(string $userId, string $timezone): void
{
$this->users->insertOne([
'user_id' => $userId,
'timezone' => $timezone,
'created_at' => new UTCDateTime(),
]);
}
public function createMeeting(string $title, string $organizerTimezone, string $localDateTime, array $participants): string
{
// 将组织者的本地时间转换为UTC
$dt = new DateTime($localDateTime, new DateTimeZone($organizerTimezone));
$utcStartTime = new UTCDateTime($dt->getTimestamp() * 1000);
// 计算结束时间(假设会议1小时)
$utcEndTime = new UTCDateTime(($dt->getTimestamp() + 3600) * 1000);
$result = $this->meetings->insertOne([
'title' => $title,
'start_time' => $utcStartTime,
'end_time' => $utcEndTime,
'participants' => $participants,
'organizer_timezone' => $organizerTimezone,
'created_at' => new UTCDateTime(),
]);
return (string)$result->getInsertedId();
}
public function getMeetingForUser(string $meetingId, string $userId): ?array
{
$meeting = $this->meetings->findOne(['_id' => new MongoDB\BSON\ObjectId($meetingId)]);
if (!$meeting) {
return null;
}
$user = $this->users->findOne(['user_id' => $userId]);
$timezone = $user ? $user['timezone'] : 'UTC';
return $this->formatMeetingForTimezone($meeting, $timezone);
}
public function getUserUpcomingMeetings(string $userId): array
{
$user = $this->users->findOne(['user_id' => $userId]);
$timezone = $user ? $user['timezone'] : 'UTC';
$now = new UTCDateTime();
$meetings = $this->meetings->find([
'participants' => $userId,
'start_time' => ['$gte' => $now]
], ['sort' => ['start_time' => 1]])->toArray();
return array_map(function($meeting) use ($timezone) {
return $this->formatMeetingForTimezone($meeting, $timezone);
}, $meetings);
}
public function getMeetingInAllTimezones(string $meetingId): array
{
$meeting = $this->meetings->findOne(['_id' => new MongoDB\BSON\ObjectId($meetingId)]);
if (!$meeting) {
return [];
}
$timezones = [
'Asia/Shanghai' => '北京',
'America/New_York' => '纽约',
'Europe/London' => '伦敦',
'Asia/Tokyo' => '东京',
'Australia/Sydney' => '悉尼',
];
$result = [];
foreach ($timezones as $tz => $name) {
$result[$name] = $this->formatMeetingForTimezone($meeting, $tz);
}
return $result;
}
private function formatMeetingForTimezone(array $meeting, string $timezone): array
{
$tz = new DateTimeZone($timezone);
$startTime = $meeting['start_time']->toDateTime();
$startTime->setTimezone($tz);
$endTime = $meeting['end_time']->toDateTime();
$endTime->setTimezone($tz);
return [
'title' => $meeting['title'],
'start_time' => $startTime->format('Y-m-d H:i:s'),
'end_time' => $endTime->format('Y-m-d H:i:s'),
'timezone' => $timezone,
'timezone_name' => $startTime->format('T'),
];
}
}
// 使用示例
$manager = new MultiTimezoneManager($meetings, $users);
// 注册用户
$manager->registerUser('user_bj', 'Asia/Shanghai');
$manager->registerUser('user_ny', 'America/New_York');
$manager->registerUser('user_ld', 'Europe/London');
// 创建会议(北京时间下午3点)
$meetingId = $manager->createMeeting(
'项目讨论会',
'Asia/Shanghai',
'2024-03-15 15:00:00',
['user_bj', 'user_ny', 'user_ld']
);
echo "会议创建成功: $meetingId\n";
// 不同用户查看会议时间
echo "\n各用户看到的会议时间:\n";
$bjMeeting = $manager->getMeetingForUser($meetingId, 'user_bj');
echo "北京用户: {$bjMeeting['start_time']} ({$bjMeeting['timezone_name']})\n";
$nyMeeting = $manager->getMeetingForUser($meetingId, 'user_ny');
echo "纽约用户: {$nyMeeting['start_time']} ({$nyMeeting['timezone_name']})\n";
$ldMeeting = $manager->getMeetingForUser($meetingId, 'user_ld');
echo "伦敦用户: {$ldMeeting['start_time']} ({$ldMeeting['timezone_name']})\n";
// 显示所有时区
echo "\n会议在各地时间:\n";
$allTimezones = $manager->getMeetingInAllTimezones($meetingId);
foreach ($allTimezones as $city => $info) {
echo " $city: {$info['start_time']}\n";
}运行结果:
会议创建成功: 65f3b2a0123456789abcdef0
各用户看到的会议时间:
北京用户: 2024-03-15 15:00:00 (CST)
纽约用户: 2024-03-15 03:00:00 (EDT)
伦敦用户: 2024-03-15 07:00:00 (GMT)
会议在各地时间:
北京: 2024-03-15 15:00:00
纽约: 2024-03-15 03:00:00
伦敦: 2024-03-15 07:00:00
东京: 2024-03-15 16:00:00
悉尼: 2024-03-15 18:00:006.3 时间序列数据分析
场景描述
对时间序列数据进行复杂的统计分析,如移动平均、同比环比分析等。
使用方法
使用MongoDB聚合管道的日期操作符和窗口函数进行时间序列分析。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$metrics = $client->test->time_series_metrics;
$metrics->drop();
// 创建索引
$metrics->createIndex(['metric_name' => 1, 'timestamp' => 1]);
$metrics->createIndex(['timestamp' => 1]);
class TimeSeriesAnalyzer
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
public function recordMetric(string $name, float $value, array $tags = []): void
{
$this->collection->insertOne([
'metric_name' => $name,
'value' => $value,
'tags' => $tags,
'timestamp' => new UTCDateTime(),
]);
}
public function getHourlyAverage(string $metricName, string $date): array
{
$start = new UTCDateTime(strtotime($date . ' 00:00:00') * 1000);
$end = new UTCDateTime(strtotime($date . ' 23:59:59') * 1000);
$pipeline = [
[
'$match' => [
'metric_name' => $metricName,
'timestamp' => ['$gte' => $start, '$lte' => $end]
]
],
[
'$group' => [
'_id' => [
'year' => ['$year' => '$timestamp'],
'month' => ['$month' => '$timestamp'],
'day' => ['$dayOfMonth' => '$timestamp'],
'hour' => ['$hour' => '$timestamp']
],
'avg_value' => ['$avg' => '$value'],
'min_value' => ['$min' => '$value'],
'max_value' => ['$max' => '$value'],
'count' => ['$sum' => 1]
]
],
['$sort' => ['_id.hour' => 1]]
];
return $this->collection->aggregate($pipeline)->toArray();
}
public function getDailyTrend(int $days = 7): array
{
$start = new UTCDateTime((time() - $days * 86400) * 1000);
$pipeline = [
[
'$match' => [
'timestamp' => ['$gte' => $start]
]
],
[
'$group' => [
'_id' => [
'year' => ['$year' => '$timestamp'],
'month' => ['$month' => '$timestamp'],
'day' => ['$dayOfMonth' => '$timestamp'],
'metric' => '$metric_name'
],
'avg_value' => ['$avg' => '$value'],
'total' => ['$sum' => '$value']
]
],
[
'$sort' => ['_id.year' => 1, '_id.month' => 1, '_id.day' => 1]
]
];
return $this->collection->aggregate($pipeline)->toArray();
}
public function getYearOverYear(string $metricName, string $month): array
{
// 当年数据
$currentYearStart = new UTCDateTime(strtotime("first day of $month 2024 00:00:00") * 1000);
$currentYearEnd = new UTCDateTime(strtotime("last day of $month 2024 23:59:59") * 1000);
// 去年数据
$lastYearStart = new UTCDateTime(strtotime("first day of $month 2023 00:00:00") * 1000);
$lastYearEnd = new UTCDateTime(strtotime("last day of $month 2023 23:59:59") * 1000);
$pipeline = [
[
'$match' => [
'metric_name' => $metricName,
'$or' => [
['timestamp' => ['$gte' => $currentYearStart, '$lte' => $currentYearEnd]],
['timestamp' => ['$gte' => $lastYearStart, '$lte' => $lastYearEnd]]
]
]
],
[
'$group' => [
'_id' => ['$year' => '$timestamp'],
'total' => ['$sum' => '$value'],
'avg' => ['$avg' => '$value'],
'count' => ['$sum' => 1]
]
],
['$sort' => ['_id' => 1]]
];
return $this->collection->aggregate($pipeline)->toArray();
}
public function getMovingAverage(string $metricName, int $windowSize = 7): array
{
// MongoDB 5.0+ 支持窗口函数 $setWindowFields
$pipeline = [
[
'$match' => [
'metric_name' => $metricName
]
],
[
'$sort' => ['timestamp' => 1]
],
[
'$group' => [
'_id' => [
'$dateToString' => ['format' => '%Y-%m-%d', 'date' => '$timestamp']
],
'daily_avg' => ['$avg' => '$value']
]
],
[
'$sort' => ['_id' => 1]
],
[
'$setWindowFields' => [
'output' => [
'moving_avg' => [
'$avg' => '$daily_avg',
'window' => ['documents' => -$windowSize + 1, 0]
]
]
]
]
];
try {
return $this->collection->aggregate($pipeline)->toArray();
} catch (Exception $e) {
// 如果不支持窗口函数,使用PHP计算
return $this->calculateMovingAverageInPHP($metricName, $windowSize);
}
}
private function calculateMovingAverageInPHP(string $metricName, int $windowSize): array
{
$dailyData = $this->collection->aggregate([
['$match' => ['metric_name' => $metricName]],
['$sort' => ['timestamp' => 1]],
[
'$group' => [
'_id' => [
'$dateToString' => ['format' => '%Y-%m-%d', 'date' => '$timestamp']
],
'daily_avg' => ['$avg' => '$value']
]
],
['$sort' => ['_id' => 1]]
])->toArray();
$result = [];
$values = [];
foreach ($dailyData as $data) {
$values[] = $data['daily_avg'];
if (count($values) >= $windowSize) {
$window = array_slice($values, -$windowSize);
$movingAvg = array_sum($window) / count($window);
} else {
$movingAvg = array_sum($values) / count($values);
}
$result[] = [
'date' => $data['_id'],
'daily_avg' => $data['daily_avg'],
'moving_avg' => $movingAvg
];
}
return $result;
}
}
// 使用示例
$analyzer = new TimeSeriesAnalyzer($metrics);
// 记录指标数据
for ($i = 0; $i < 100; $i++) {
$timestamp = time() - rand(0, 7 * 86400);
// 模拟回溯时间插入
$metrics->insertOne([
'metric_name' => 'cpu_usage',
'value' => rand(20, 80) + (rand(0, 100) / 100),
'timestamp' => new UTCDateTime($timestamp * 1000),
]);
}
echo "指标数据记录完成\n";
// 小时平均
$hourlyAvg = $analyzer->getHourlyAverage('cpu_usage', date('Y-m-d'));
echo "\n今日小时平均:\n";
foreach (array_slice($hourlyAvg, 0, 5) as $data) {
echo " {$data['_id']['hour']}时: 平均 {$data['avg_value']}\n";
}
// 移动平均
$movingAvg = $analyzer->getMovingAverage('cpu_usage', 7);
echo "\n7天移动平均:\n";
foreach (array_slice($movingAvg, -5) as $data) {
echo " {$data['date']}: 日均 {$data['daily_avg']}, 移动平均 {$data['moving_avg']}\n";
}运行结果:
指标数据记录完成
今日小时平均:
0时: 平均 45.5
1时: 平均 52.3
2时: 平均 38.7
3时: 平均 61.2
4时: 平均 44.8
7天移动平均:
2024-03-11: 日均 48.5, 移动平均 50.2
2024-03-12: 日均 52.1, 移动平均 49.8
2024-03-13: 日均 45.3, 移动平均 48.9
2024-03-14: 日均 51.7, 移动平均 49.5
2024-03-15: 日均 47.2, 移动平均 49.16.4 审计日志系统
场景描述
企业级应用需要完整的审计日志,记录所有关键操作的时间、操作者、操作内容等信息。
使用方法
使用Date类型记录精确的操作时间,配合索引支持高效查询。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$auditLogs = $client->test->audit_logs;
$auditLogs->drop();
// 创建索引
$auditLogs->createIndex(['timestamp' => -1]);
$auditLogs->createIndex(['user_id' => 1, 'timestamp' => -1]);
$auditLogs->createIndex(['resource_type' => 1, 'resource_id' => 1]);
$auditLogs->createIndex(['action' => 1]);
class AuditLogger
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
public function log(string $action, string $resourceType, string $resourceId, string $userId, array $details = []): string
{
$result = $this->collection->insertOne([
'action' => $action,
'resource_type' => $resourceType,
'resource_id' => $resourceId,
'user_id' => $userId,
'details' => $details,
'timestamp' => new UTCDateTime(),
'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'CLI',
]);
return (string)$result->getInsertedId();
}
public function logCreate(string $resourceType, string $resourceId, string $userId, array $data): string
{
return $this->log('create', $resourceType, $resourceId, $userId, [
'new_value' => $data
]);
}
public function logUpdate(string $resourceType, string $resourceId, string $userId, array $oldData, array $newData): string
{
$changes = $this->calculateChanges($oldData, $newData);
return $this->log('update', $resourceType, $resourceId, $userId, [
'old_value' => $oldData,
'new_value' => $newData,
'changes' => $changes
]);
}
public function logDelete(string $resourceType, string $resourceId, string $userId, array $data): string
{
return $this->log('delete', $resourceType, $resourceId, $userId, [
'old_value' => $data
]);
}
public function getUserActivity(string $userId, int $days = 30): array
{
$start = new UTCDateTime((time() - $days * 86400) * 1000);
return $this->collection->find([
'user_id' => $userId,
'timestamp' => ['$gte' => $start]
], ['sort' => ['timestamp' => -1]])->toArray();
}
public function getResourceHistory(string $resourceType, string $resourceId): array
{
return $this->collection->find([
'resource_type' => $resourceType,
'resource_id' => $resourceId
], ['sort' => ['timestamp' => 1]])->toArray();
}
public function getActivityReport(string $startDate, string $endDate): array
{
$start = new UTCDateTime(strtotime($startDate) * 1000);
$end = new UTCDateTime(strtotime($endDate . ' 23:59:59') * 1000);
$pipeline = [
[
'$match' => [
'timestamp' => ['$gte' => $start, '$lte' => $end]
]
],
[
'$group' => [
'_id' => [
'action' => '$action',
'resource_type' => '$resource_type'
],
'count' => ['$sum' => 1],
'unique_users' => ['$addToSet' => '$user_id']
]
],
[
'$project' => [
'action' => '$_id.action',
'resource_type' => '$_id.resource_type',
'count' => 1,
'unique_user_count' => ['$size' => '$unique_users']
]
],
['$sort' => ['count' => -1]]
];
return $this->collection->aggregate($pipeline)->toArray();
}
public function getSuspiciousActivity(int $threshold = 100): array
{
// 检测短时间内大量操作
$oneHourAgo = new UTCDateTime((time() - 3600) * 1000);
$pipeline = [
[
'$match' => [
'timestamp' => ['$gte' => $oneHourAgo]
]
],
[
'$group' => [
'_id' => '$user_id',
'count' => ['$sum' => 1],
'actions' => ['$push' => '$action']
]
],
[
'$match' => [
'count' => ['$gte' => $threshold]
]
],
['$sort' => ['count' => -1]]
];
return $this->collection->aggregate($pipeline)->toArray();
}
private function calculateChanges(array $old, array $new): array
{
$changes = [];
foreach ($new as $key => $value) {
if (!array_key_exists($key, $old) || $old[$key] !== $value) {
$changes[$key] = [
'old' => $old[$key] ?? null,
'new' => $value
];
}
}
foreach ($old as $key => $value) {
if (!array_key_exists($key, $new)) {
$changes[$key] = [
'old' => $value,
'new' => null
];
}
}
return $changes;
}
}
// 使用示例
$auditLogger = new AuditLogger($auditLogs);
// 记录创建操作
$auditLogger->logCreate('user', 'user123', 'admin', [
'name' => '张三',
'email' => 'zhang@example.com',
'role' => 'user'
]);
// 记录更新操作
$auditLogger->logUpdate('user', 'user123', 'admin',
['name' => '张三', 'email' => 'zhang@example.com', 'role' => 'user'],
['name' => '张三', 'email' => 'zhangsan@example.com', 'role' => 'admin']
);
// 记录删除操作
$auditLogger->logDelete('document', 'doc456', 'admin', [
'title' => '测试文档',
'content' => '文档内容'
]);
echo "审计日志记录完成\n";
// 查询用户活动
$activities = $auditLogger->getUserActivity('admin');
echo "\n管理员活动记录:\n";
foreach ($activities as $activity) {
echo " {$activity['action']} {$activity['resource_type']}: " .
$activity['timestamp']->toDateTime()->format('Y-m-d H:i:s') . "\n";
}
// 查询资源历史
$history = $auditLogger->getResourceHistory('user', 'user123');
echo "\n用户user123的历史记录:\n";
foreach ($history as $record) {
echo " {$record['action']}: " . $record['timestamp']->toDateTime()->format('Y-m-d H:i:s') . "\n";
}
// 活动报告
$report = $auditLogger->getActivityReport('today', 'now');
echo "\n活动报告:\n";
foreach ($report as $item) {
echo " {$item['action']} {$item['resource_type']}: {$item['count']}次\n";
}运行结果:
审计日志记录完成
管理员活动记录:
delete document: 2024-03-15 10:30:00
update user: 2024-03-15 10:30:00
create user: 2024-03-15 10:30:00
用户user123的历史记录:
update: 2024-03-15 10:30:00
create: 2024-03-15 10:30:00
活动报告:
create user: 1次
update user: 1次
delete document: 1次7. 行业最佳实践
7.1 时间存储规范
实践内容
始终使用UTC时间存储,在展示层进行时区转换。
推荐理由
- 避免时区混淆问题
- 便于跨时区协作
- 简化时间计算和比较
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
class TimeStorageBestPractice
{
private $collection;
private $displayTimezone;
public function __construct($collection, string $displayTimezone = 'Asia/Shanghai')
{
$this->collection = $collection;
$this->displayTimezone = new DateTimeZone($displayTimezone);
}
// 正确:存储UTC时间
public function saveWithUtc(string $localDateTime): void
{
$dt = new DateTime($localDateTime, $this->displayTimezone);
$this->collection->insertOne([
'event' => 'test',
'created_at' => new UTCDateTime($dt->getTimestamp() * 1000),
'timezone' => $this->displayTimezone->getName(),
]);
}
// 错误:存储本地时间字符串
public function saveWithLocalString(string $localDateTime): void
{
$this->collection->insertOne([
'event' => 'test_wrong',
'created_at' => $localDateTime, // 错误:存储字符串
]);
}
// 正确:显示时转换时区
public function displayTime(UTCDateTime $utcDateTime): string
{
$dt = $utcDateTime->toDateTime();
$dt->setTimezone($this->displayTimezone);
return $dt->format('Y-m-d H:i:s T');
}
// 规范:统一的时间字段命名
public function createDocument(array $data): void
{
$now = new UTCDateTime();
$this->collection->insertOne([
'data' => $data,
'created_at' => $now, // 创建时间
'updated_at' => $now, // 更新时间
'expires_at' => null, // 过期时间
'published_at' => null, // 发布时间
'deleted_at' => null, // 删除时间(软删除)
]);
}
}
// 使用示例
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->time_practice;
$collection->drop();
$practice = new TimeStorageBestPractice($collection);
// 存储
$practice->saveWithUtc('2024-03-15 14:30:00');
// 显示
$doc = $collection->findOne(['event' => 'test']);
echo "显示时间: " . $practice->displayTime($doc['created_at']) . "\n";运行结果:
显示时间: 2024-03-15 14:30:00 CST7.2 索引设计原则
实践内容
为时间字段创建合适的索引,优化范围查询性能。
推荐理由
- 时间范围查询是常见操作
- 复合索引支持多条件查询
- TTL索引自动清理过期数据
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
class TimeIndexBestPractice
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
$this->setupIndexes();
}
private function setupIndexes(): void
{
// 单字段索引:支持时间范围查询
$this->collection->createIndex(['created_at' => -1]);
// 复合索引:支持用户+时间查询
$this->collection->createIndex(['user_id' => 1, 'created_at' => -1]);
// 复合索引:支持状态+时间查询
$this->collection->createIndex(['status' => 1, 'created_at' => 1]);
// TTL索引:自动清理过期数据
$this->collection->createIndex(
['expires_at' => 1],
['expireAfterSeconds' => 0]
);
// 部分索引:只索引活跃数据
$this->collection->createIndex(
['created_at' => -1],
[
'partialFilterExpression' => ['status' => 'active'],
'name' => 'active_created_at_idx'
]
);
}
// 高效的范围查询
public function findByTimeRange(string $start, string $end): array
{
return $this->collection->find([
'created_at' => [
'$gte' => new UTCDateTime(strtotime($start) * 1000),
'$lte' => new UTCDateTime(strtotime($end) * 1000)
]
], [
'sort' => ['created_at' => -1]
])->toArray();
}
// 高效的用户时间查询
public function findByUserAndTime(string $userId, string $start, string $end): array
{
return $this->collection->find([
'user_id' => $userId,
'created_at' => [
'$gte' => new UTCDateTime(strtotime($start) * 1000),
'$lte' => new UTCDateTime(strtotime($end) * 1000)
]
], [
'sort' => ['created_at' => -1]
])->toArray();
}
// 查看索引使用情况
public function explainQuery(array $query): array
{
return $this->collection->explain()->find($query);
}
}
// 使用示例
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->time_indexed;
$collection->drop();
$practice = new TimeIndexBestPractice($collection);
// 插入测试数据
for ($i = 0; $i < 100; $i++) {
$collection->insertOne([
'user_id' => 'user' . ($i % 10),
'status' => $i % 3 === 0 ? 'active' : 'inactive',
'data' => "Document $i",
'created_at' => new UTCDateTime((time() - $i * 3600) * 1000),
'expires_at' => new UTCDateTime((time() + 86400) * 1000),
]);
}
// 查看索引
echo "创建的索引:\n";
$indexes = $collection->listIndexes();
foreach ($indexes as $index) {
echo " - {$index->getName()}: " . json_encode($index->getKey()) . "\n";
}运行结果:
创建的索引:
- _id_: {"_id":1}
- created_at_-1: {"created_at":-1}
- user_id_1_created_at_-1: {"user_id":1,"created_at":-1}
- status_1_created_at_1: {"status":1,"created_at":1}
- expires_at_1: {"expires_at":1}
- active_created_at_idx: {"created_at":-1}7.3 时间计算最佳实践
实践内容
使用MongoDB聚合管道进行时间计算,避免在应用层处理大量数据。
推荐理由
- 减少网络传输
- 利用数据库优化
- 代码更简洁
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
class TimeCalculationBestPractice
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
// 使用聚合管道计算时间差
public function calculateDuration(string $orderId): ?int
{
$pipeline = [
['$match' => ['order_id' => $orderId]],
[
'$project' => [
'duration_ms' => [
'$subtract' => ['$completed_at', '$created_at']
]
]
]
];
$result = $this->collection->aggregate($pipeline)->toArray();
return $result[0]['duration_ms'] ?? null;
}
// 按时间段分组统计
public function groupByTimePeriod(string $field, string $interval = 'day'): array
{
$dateTruncFormat = match($interval) {
'hour' => '%Y-%m-%d %H:00:00',
'day' => '%Y-%m-%d',
'week' => '%Y-%U',
'month' => '%Y-%m',
default => '%Y-%m-%d'
};
$pipeline = [
[
'$group' => [
'_id' => [
'$dateToString' => [
'format' => $dateTruncFormat,
'date' => '$' . $field
]
],
'count' => ['$sum' => 1],
'total' => ['$sum' => '$amount']
]
],
['$sort' => ['_id' => 1]]
];
return $this->collection->aggregate($pipeline)->toArray();
}
// 计算同比/环比
public function calculateGrowth(string $metric, string $period = 'month'): array
{
$pipeline = [
[
'$group' => [
'_id' => [
'year' => ['$year' => '$created_at'],
$period => ['$' . $period => '$created_at']
],
'total' => ['$sum' => '$' . $metric]
]
],
['$sort' => ['_id.year' => 1, '_id.' . $period => 1]],
[
'$group' => [
'_id' => '$_id.year',
'periods' => [
'$push' => [
'period' => '$_id.' . $period,
'total' => '$total'
]
]
]
]
];
return $this->collection->aggregate($pipeline)->toArray();
}
// 使用$dateAdd进行时间偏移
public function findExpiringSoon(int $hours = 24): array
{
// MongoDB 5.0+ 支持 $dateAdd
$pipeline = [
[
'$match' => [
'status' => 'active',
'expires_at' => ['$ne' => null]
]
],
[
'$addFields' => [
'warning_time' => [
'$dateSubtract' => [
'startDate' => '$expires_at',
'unit' => 'hour',
'amount' => $hours
]
]
]
],
[
'$match' => [
'warning_time' => ['$lte' => new UTCDateTime()]
]
]
];
try {
return $this->collection->aggregate($pipeline)->toArray();
} catch (Exception $e) {
// 降级方案:PHP计算
return $this->findExpiringSoonFallback($hours);
}
}
private function findExpiringSoonFallback(int $hours): array
{
$warningTime = new UTCDateTime((time() + $hours * 3600) * 1000);
return $this->collection->find([
'status' => 'active',
'expires_at' => [
'$ne' => null,
'$lte' => $warningTime,
'$gt' => new UTCDateTime()
]
])->toArray();
}
}
// 使用示例
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->time_calc;
$collection->drop();
$practice = new TimeCalculationBestPractice($collection);
// 插入测试数据
for ($i = 0; $i < 30; $i++) {
$createdTime = strtotime("-$i days");
$completedTime = $createdTime + rand(3600, 86400);
$collection->insertOne([
'order_id' => "ORD$i",
'amount' => rand(100, 1000),
'created_at' => new UTCDateTime($createdTime * 1000),
'completed_at' => new UTCDateTime($completedTime * 1000),
'expires_at' => new UTCDateTime((time() + rand(1, 48) * 3600) * 1000),
'status' => 'active',
]);
}
// 按日分组统计
$dailyStats = $practice->groupByTimePeriod('created_at', 'day');
echo "按日统计(最近5天):\n";
foreach (array_slice($dailyStats, -5) as $stat) {
echo " {$stat['_id']}: {$stat['count']}条, 总额 {$stat['total']}\n";
}运行结果:
按日统计(最近5天):
2024-03-11: 1条, 总额 567
2024-03-12: 1条, 总额 234
2024-03-13: 1条, 总额 890
2024-03-14: 1条, 总额 456
2024-03-15: 1条, 总额 1237.4 错误处理与容错
实践内容
建立完善的时间处理错误处理机制,处理各种边界情况。
推荐理由
- 提高系统健壮性
- 避免因时间问题导致系统故障
- 便于问题排查
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
class TimeErrorHandling
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
// 安全的时间解析
public function safeParseTime($input): ?UTCDateTime
{
if ($input === null) {
return null;
}
// 已经是UTCDateTime
if ($input instanceof UTCDateTime) {
return $input;
}
// 时间戳(秒或毫秒)
if (is_numeric($input)) {
// 判断是秒还是毫秒
if ($input > 1e12) {
// 毫秒时间戳
return new UTCDateTime((int)$input);
} else {
// 秒时间戳
return new UTCDateTime((int)($input * 1000));
}
}
// 字符串解析
if (is_string($input)) {
try {
// 尝试ISO 8601格式
$dt = new DateTime($input);
return new UTCDateTime($dt->getTimestamp() * 1000);
} catch (Exception $e) {
// 记录解析失败
error_log("时间解析失败: $input");
return null;
}
}
return null;
}
// 安全的时间范围查询
public function safeTimeRangeQuery(string $field, $start, $end): array
{
$startTime = $this->safeParseTime($start);
$endTime = $this->safeParseTime($end);
if ($startTime === null || $endTime === null) {
throw new InvalidArgumentException('无效的时间范围');
}
if ($startTime > $endTime) {
throw new InvalidArgumentException('开始时间不能大于结束时间');
}
return $this->collection->find([
$field => ['$gte' => $startTime, '$lte' => $endTime]
])->toArray();
}
// 带默认值的时间获取
public function getTimeWithDefault($value, $default = 'now'): UTCDateTime
{
$parsed = $this->safeParseTime($value);
if ($parsed !== null) {
return $parsed;
}
return $default === 'now' ? new UTCDateTime() : $this->safeParseTime($default);
}
// 时间有效性验证
public function validateTimeRange($start, $end, int $maxDays = 365): array
{
$errors = [];
$startTime = $this->safeParseTime($start);
$endTime = $this->safeParseTime($end);
if ($startTime === null) {
$errors[] = '开始时间格式无效';
}
if ($endTime === null) {
$errors[] = '结束时间格式无效';
}
if (empty($errors)) {
if ($startTime > $endTime) {
$errors[] = '开始时间不能大于结束时间';
}
$diffDays = ($endTime->toDateTime()->getTimestamp() - $startTime->toDateTime()->getTimestamp()) / 86400;
if ($diffDays > $maxDays) {
$errors[] = "时间范围不能超过{$maxDays}天";
}
}
return [
'valid' => empty($errors),
'errors' => $errors,
'start' => $startTime,
'end' => $endTime
];
}
// 时间转换异常处理
public function convertWithFallback($time, string $format = 'Y-m-d H:i:s'): string
{
try {
$utcDateTime = $this->safeParseTime($time);
if ($utcDateTime === null) {
return '无效时间';
}
return $utcDateTime->toDateTime()->format($format);
} catch (Exception $e) {
error_log("时间转换错误: " . $e->getMessage());
return '转换失败';
}
}
}
// 使用示例
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->time_error;
$collection->drop();
$handler = new TimeErrorHandling($collection);
// 测试各种时间格式
$testInputs = [
'2024-03-15 10:30:00',
'2024-03-15T10:30:00Z',
1710498600, // 秒时间戳
1710498600000, // 毫秒时间戳
'invalid date',
null,
];
echo "时间解析测试:\n";
foreach ($testInputs as $input) {
$result = $handler->safeParseTime($input);
$display = $handler->convertWithFallback($result);
echo " 输入: " . json_encode($input) . " -> 输出: $display\n";
}
// 验证时间范围
echo "\n时间范围验证:\n";
$validation = $handler->validateTimeRange('2024-01-01', '2025-01-01', 365);
echo " 有效: " . ($validation['valid'] ? '是' : '否') . "\n";
if (!$validation['valid']) {
echo " 错误: " . implode(', ', $validation['errors']) . "\n";
}运行结果:
时间解析测试:
输入: "2024-03-15 10:30:00" -> 输出: 2024-03-15 10:30:00
输入: "2024-03-15T10:30:00Z" -> 输出: 2024-03-15 10:30:00
输入: 1710498600 -> 输出: 2024-03-15 10:30:00
输入: 1710498600000 -> 输出: 2024-03-15 10:30:00
输入: "invalid date" -> 输出: 无效时间
输入: null -> 输出: 无效时间
时间范围验证:
有效: 是8. 常见问题答疑(FAQ)
8.1 MongoDB Date类型的精度是多少?
问题描述
MongoDB Date类型支持的时间精度是什么?能否存储微秒级时间?
回答内容
MongoDB Date类型使用64位整数存储UTC毫秒时间戳,精度为毫秒级(1/1000秒)。不支持微秒级精度。
如果需要更高精度,可以考虑:
- 使用额外字段存储微秒部分
- 使用Decimal128存储高精度时间戳
- 使用字符串存储ISO 8601格式(但不便于计算)
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\BSON\Decimal128;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->precision_demo;
// 标准Date类型:毫秒精度
$microtime = microtime(true);
$milliseconds = (int)($microtime * 1000);
$standardDate = new UTCDateTime($milliseconds);
echo "=== 标准Date类型精度 ===\n";
echo "原始时间: " . sprintf('%.6f', $microtime) . "\n";
echo "存储值: " . $standardDate->__toString() . "\n";
echo "精度损失: " . sprintf('%.0f', ($microtime * 1000000 - $milliseconds * 1000)) . " 微秒\n";
// 方案一:额外存储微秒
$highPrecisionDoc = [
'timestamp' => new UTCDateTime($milliseconds),
'microseconds' => (int)(($microtime * 1000000) % 1000),
];
echo "\n=== 方案一:额外字段 ===\n";
echo "毫秒部分: " . $highPrecisionDoc['timestamp'] . "\n";
echo "微秒部分: " . $highPrecisionDoc['microseconds'] . "\n";
// 方案二:使用Decimal128
$decimalTimestamp = new Decimal128(sprintf('%.6f', $microtime));
$decimalDoc = [
'timestamp_decimal' => $decimalTimestamp,
];
echo "\n=== 方案二:Decimal128 ===\n";
echo "完整精度: " . $decimalTimestamp . "\n";
// 重建完整时间
$fullTimestamp = (float)(string)$decimalTimestamp;
$rebuiltDt = DateTime::createFromFormat('U.u', sprintf('%.6f', $fullTimestamp));
echo "重建时间: " . $rebuiltDt->format('Y-m-d H:i:s.u') . "\n";运行结果:
=== 标准Date类型精度 ===
原始时间: 1710513000.123456
存储值: 1710513000123
精度损失: 456 微秒
=== 方案一:额外字段 ===
毫秒部分: 1710513000123
微秒部分: 456
=== 方案二:Decimal128 ===
完整精度: 1710513000.123456
重建时间: 2024-03-15 10:30:00.1234568.2 如何处理跨时区的日期查询?
问题描述
用户分布在不同时区,如何正确查询"今天"的数据?
回答内容
MongoDB存储的是UTC时间,查询时需要将用户的本地时间范围转换为UTC时间范围进行查询。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->timezone_query;
$collection->drop();
class TimezoneQueryHelper
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
// 获取用户"今天"的时间范围(UTC)
public function getTodayRange(string $timezone): array
{
$tz = new DateTimeZone($timezone);
$now = new DateTime('now', $tz);
// 获取今天的开始和结束(本地时间)
$start = new DateTime('today 00:00:00', $tz);
$end = new DateTime('today 23:59:59', $tz);
return [
'start' => new UTCDateTime($start->getTimestamp() * 1000),
'end' => new UTCDateTime($end->getTimestamp() * 1000),
'local_start' => $start->format('Y-m-d H:i:s'),
'local_end' => $end->format('Y-m-d H:i:s'),
];
}
// 查询用户"今天"的数据
public function findToday(string $timezone, string $dateField = 'created_at'): array
{
$range = $this->getTodayRange($timezone);
return $this->collection->find([
$dateField => ['$gte' => $range['start'], '$lte' => $range['end']]
])->toArray();
}
// 获取指定日期范围(考虑时区)
public function getDateRange(string $date, string $timezone): array
{
$tz = new DateTimeZone($timezone);
$start = new DateTime("$date 00:00:00", $tz);
$end = new DateTime("$date 23:59:59", $tz);
return [
'start' => new UTCDateTime($start->getTimestamp() * 1000),
'end' => new UTCDateTime($end->getTimestamp() * 1000),
];
}
// 按用户时区分组统计
public function groupByLocalDay(string $timezone, string $dateField = 'created_at'): array
{
// 使用聚合管道的$dateToString配合时区
$pipeline = [
[
'$addFields' => [
'local_date' => [
'$dateToString' => [
'format' => '%Y-%m-%d',
'date' => '$' . $dateField,
'timezone' => $timezone
]
]
]
],
[
'$group' => [
'_id' => '$local_date',
'count' => ['$sum' => 1]
]
],
['$sort' => ['_id' => 1]]
];
return $this->collection->aggregate($pipeline)->toArray();
}
}
// 插入测试数据(UTC时间)
$testData = [
['event' => 'A', 'created_at' => new UTCDateTime(strtotime('2024-03-15 00:00:00 UTC') * 1000)],
['event' => 'B', 'created_at' => new UTCDateTime(strtotime('2024-03-15 08:00:00 UTC') * 1000)],
['event' => 'C', 'created_at' => new UTCDateTime(strtotime('2024-03-15 16:00:00 UTC') * 1000)],
['event' => 'D', 'created_at' => new UTCDateTime(strtotime('2024-03-15 23:59:59 UTC') * 1000)],
];
$collection->insertMany($testData);
$helper = new TimezoneQueryHelper($collection);
// 北京时区查询"今天"
echo "=== 北京时区(UTC+8)查询 ===\n";
$range = $helper->getTodayRange('Asia/Shanghai');
echo "本地时间范围: {$range['local_start']} ~ {$range['local_end']}\n";
echo "UTC时间范围: " . $range['start']->toDateTime()->format('Y-m-d H:i:s') . " ~ " .
$range['end']->toDateTime()->format('Y-m-d H:i:s') . "\n";
// 纽约时区查询"今天"
echo "\n=== 纽约时区(UTC-4/UTC-5)查询 ===\n";
$range = $helper->getTodayRange('America/New_York');
echo "本地时间范围: {$range['local_start']} ~ {$range['local_end']}\n";
// 按本地日期分组
echo "\n=== 按北京时区日期分组 ===\n";
$grouped = $helper->groupByLocalDay('Asia/Shanghai');
foreach ($grouped as $item) {
echo " {$item['_id']}: {$item['count']}条\n";
}运行结果:
=== 北京时区(UTC+8)查询 ===
本地时间范围: 2024-03-15 00:00:00 ~ 2024-03-15 23:59:59
UTC时间范围: 2024-03-14 16:00:00 ~ 2024-03-15 15:59:59
=== 纽约时区(UTC-4/UTC-5)查询 ===
本地时间范围: 2024-03-15 00:00:00 ~ 2024-03-15 23:59:59
=== 按北京时区日期分组 ===
2024-03-15: 4条8.3 Date类型和ObjectId的时间戳有什么区别?
问题描述
ObjectId也包含时间戳,它与Date类型有什么区别?应该使用哪个?
回答内容
ObjectId的时间戳和Date类型有以下区别:
| 特性 | ObjectId时间戳 | Date类型 |
|---|---|---|
| 精度 | 秒级 | 毫秒级 |
| 来源 | 自动生成 | 需要显式设置 |
| 用途 | 文档创建时间 | 任意时间点 |
| 可修改 | 不可修改 | 可修改 |
| 查询效率 | 需要转换 | 直接查询 |
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\BSON\ObjectId;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$collection = $client->test->time_comparison;
$collection->drop();
echo "=== ObjectId时间戳 vs Date类型 ===\n\n";
// 创建文档
$now = new UTCDateTime();
$objectId = new ObjectId();
$doc = [
'_id' => $objectId,
'name' => '测试文档',
'created_at' => $now,
'updated_at' => $now,
];
$collection->insertOne($doc);
// 从ObjectId提取时间
$objectIdTime = $objectId->getTimestamp();
$objectIdDateTime = new UTCDateTime($objectIdTime * 1000);
echo "ObjectId时间戳(秒): $objectIdTime\n";
echo "ObjectId时间: " . $objectIdDateTime->toDateTime()->format('Y-m-d H:i:s') . "\n";
echo "Date类型时间: " . $now->toDateTime()->format('Y-m-d H:i:s') . "\n";
echo "精度差异: " . ($now->__toString() - $objectIdTime * 1000) . " 毫秒\n";
// 使用场景对比
echo "\n=== 使用场景对比 ===\n";
// 场景1:文档创建时间 - 使用ObjectId
echo "1. 文档创建时间:\n";
echo " 优点:自动生成,无需额外字段\n";
echo " 缺点:精度低(秒级),无法指定时间\n";
// 场景2:业务时间 - 使用Date类型
echo "\n2. 业务时间(如订单创建时间):\n";
echo " 优点:精度高(毫秒级),可指定任意时间\n";
echo " 缺点:需要显式设置\n";
// 场景3:更新时间 - 必须使用Date类型
echo "\n3. 更新时间:\n";
echo " 必须使用Date类型,ObjectId时间不可变\n";
// 查询对比
echo "\n=== 查询方式对比 ===\n";
// 使用ObjectId时间范围查询
$pipeline = [
[
'$match' => [
'_id' => [
'$gte' => new ObjectId(dechex($objectIdTime - 60) . '0000000000000000'),
'$lte' => new ObjectId(dechex($objectIdTime + 60) . 'ffffffffffffffff')
]
]
]
];
$result = $collection->aggregate($pipeline)->toArray();
echo "ObjectId时间范围查询: " . count($result) . " 条结果\n";
// 使用Date类型查询
$result = $collection->find([
'created_at' => [
'$gte' => new UTCDateTime(($objectIdTime - 60) * 1000),
'$lte' => new UTCDateTime(($objectIdTime + 60) * 1000)
]
])->toArray();
echo "Date类型范围查询: " . count($result) . " 条结果\n";
// 建议
echo "\n=== 最佳实践建议 ===\n";
echo "1. 需要创建时间:可使用ObjectId.getTimestamp(),或添加created_at字段\n";
echo "2. 需要更新时间:必须使用updated_at字段(Date类型)\n";
echo "3. 需要业务时间:必须使用Date类型字段\n";
echo "4. 需要高精度:必须使用Date类型\n";
echo "5. 需要时间索引:推荐使用Date类型字段\n";运行结果:
=== ObjectId时间戳 vs Date类型 ===
ObjectId时间戳(秒): 1710513000
ObjectId时间: 2024-03-15 10:30:00
Date类型时间: 2024-03-15 10:30:00
精度差异: 0 毫秒
=== 使用场景对比 ===
1. 文档创建时间:
优点:自动生成,无需额外字段
缺点:精度低(秒级),无法指定时间
2. 业务时间(如订单创建时间):
优点:精度高(毫秒级),可指定任意时间
缺点:需要显式设置
3. 更新时间:
必须使用Date类型,ObjectId时间不可变
=== 查询方式对比 ===
ObjectId时间范围查询: 1 条结果
Date类型范围查询: 1 条结果
=== 最佳实践建议 ===
1. 需要创建时间:可使用ObjectId.getTimestamp(),或添加created_at字段
2. 需要更新时间:必须使用updated_at字段(Date类型)
3. 需要业务时间:必须使用Date类型字段
4. 需要高精度:必须使用Date类型
5. 需要时间索引:推荐使用Date类型字段8.4 如何实现定时任务和延迟队列?
问题描述
如何使用MongoDB Date类型实现定时任务或延迟消息队列?
回答内容
可以使用Date类型存储任务的执行时间,通过轮询查询到期任务来实现定时任务和延迟队列。
php
<?php
require_once 'vendor/autoload.php';
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
$client = new Client('mongodb://localhost:27017');
$taskQueue = $client->test->delayed_tasks;
$taskQueue->drop();
// 创建索引
$taskQueue->createIndex(['execute_at' => 1, 'status' => 1]);
$taskQueue->createIndex(['status' => 1]);
class DelayedTaskQueue
{
private $collection;
public function __construct($collection)
{
$this->collection = $collection;
}
// 添加延迟任务
public function addTask(string $type, array $payload, int $delaySeconds): string
{
$executeAt = new UTCDateTime((time() + $delaySeconds) * 1000);
$result = $this->collection->insertOne([
'type' => $type,
'payload' => $payload,
'status' => 'pending',
'execute_at' => $executeAt,
'created_at' => new UTCDateTime(),
'started_at' => null,
'completed_at' => null,
'error' => null,
'retry_count' => 0,
'max_retries' => 3,
]);
return (string)$result->getInsertedId();
}
// 添加定时任务(指定执行时间)
public function addScheduledTask(string $type, array $payload, string $executeAt): string
{
$executeTime = new UTCDateTime(strtotime($executeAt) * 1000);
$result = $this->collection->insertOne([
'type' => $type,
'payload' => $payload,
'status' => 'pending',
'execute_at' => $executeTime,
'created_at' => new UTCDateTime(),
'started_at' => null,
'completed_at' => null,
'error' => null,
'retry_count' => 0,
'max_retries' => 3,
]);
return (string)$result->getInsertedId();
}
// 获取待执行任务
public function getPendingTasks(int $limit = 10): array
{
$now = new UTCDateTime();
// 使用findAndModify原子操作获取任务
$tasks = [];
for ($i = 0; $i < $limit; $i++) {
$task = $this->collection->findOneAndUpdate(
[
'status' => 'pending',
'execute_at' => ['$lte' => $now]
],
[
'$set' => [
'status' => 'processing',
'started_at' => new UTCDateTime()
]
],
[
'sort' => ['execute_at' => 1],
'returnDocument' => MongoDB\Operation\FindOneAndUpdate::RETURN_DOCUMENT_AFTER
]
);
if ($task === null) {
break;
}
$tasks[] = $task;
}
return $tasks;
}
// 标记任务完成
public function completeTask($taskId): void
{
$this->collection->updateOne(
['_id' => $taskId],
[
'$set' => [
'status' => 'completed',
'completed_at' => new UTCDateTime()
]
]
);
}
// 标记任务失败
public function failTask($taskId, string $error): void
{
$task = $this->collection->findOne(['_id' => $taskId]);
if ($task && $task['retry_count'] < $task['max_retries']) {
// 重试:延迟5分钟后再次执行
$this->collection->updateOne(
['_id' => $taskId],
[
'$set' => [
'status' => 'pending',
'execute_at' => new UTCDateTime((time() + 300) * 1000),
'error' => $error
],
'$inc' => ['retry_count' => 1]
]
);
} else {
// 超过最大重试次数,标记为失败
$this->collection->updateOne(
['_id' => $taskId],
[
'$set' => [
'status' => 'failed',
'error' => $error,
'completed_at' => new UTCDateTime()
]
]
);
}
}
// 处理任务(模拟)
public function processTask(array $task): bool
{
echo "处理任务: {$task['type']}, ID: {$task['_id']}\n";
// 模拟任务处理
$success = rand(1, 10) > 2; // 80%成功率
if ($success) {
$this->completeTask($task['_id']);
echo " 任务完成\n";
} else {
$this->failTask($task['_id'], '模拟失败');
echo " 任务失败\n";
}
return $success;
}
// 清理已完成任务
public function cleanupCompletedTasks(int $daysOld = 7): int
{
$cutoff = new UTCDateTime((time() - $daysOld * 86400) * 1000);
$result = $this->collection->deleteMany([
'status' => 'completed',
'completed_at' => ['$lt' => $cutoff]
]);
return $result->getDeletedCount();
}
// 获取队列统计
public function getStats(): array
{
$pipeline = [
[
'$group' => [
'_id' => '$status',
'count' => ['$sum' => 1]
]
]
];
$result = $this->collection->aggregate($pipeline)->toArray();
$stats = [];
foreach ($result as $item) {
$stats[$item['_id']] = $item['count'];
}
return $stats;
}
}
// 使用示例
$queue = new DelayedTaskQueue($taskQueue);
// 添加延迟任务
$queue->addTask('send_email', ['to' => 'user@example.com', 'subject' => '欢迎'], 60);
$queue->addTask('generate_report', ['report_id' => 'RPT001'], 300);
// 添加定时任务
$queue->addScheduledTask('daily_cleanup', [], '2024-03-16 02:00:00');
echo "任务添加完成\n";
// 获取队列统计
$stats = $queue->getStats();
echo "\n队列统计:\n";
foreach ($stats as $status => $count) {
echo " $status: $count\n";
}
// 模拟任务到期
$taskQueue->updateOne(
['type' => 'send_email'],
['$set' => ['execute_at' => new UTCDateTime((time() - 60) * 1000)]]
);
// 获取待执行任务