Skip to content

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:00

2.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:00

2.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:59

3.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:00

3.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 UTC

4.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.123456

4.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:00

4.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, 数量 1

4.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}
过期秒数: 0

5. 常见应用场景

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

不活跃用户数量: 1

5.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:00

5.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 如何优化大量时间范围查询?

问题描述

当数据量很大时,时间范围查询性能如何优化?

回答内容

可以通过以下方式优化:

  1. 创建合适的索引
  2. 使用覆盖查询
  3. 限制返回字段
  4. 使用分页
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 基础练习:用户活跃度统计

解题思路

  1. 设计用户登录记录的数据结构
  2. 使用Date类型存储登录时间
  3. 使用聚合管道统计日活、周活、月活

常见误区

  • 忘记考虑时区问题
  • 使用字符串存储时间导致查询效率低
  • 没有创建合适的索引

分步提示

  1. 创建用户登录记录集合,包含user_id和login_at字段
  2. 创建复合索引优化查询
  3. 使用$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 进阶练习:订单时效分析

解题思路

  1. 记录订单各阶段的时间节点
  2. 使用聚合管道计算各阶段耗时
  3. 分析订单处理效率

常见误区

  • 没有考虑订单未完成的情况
  • 时间计算时没有处理null值
  • 没有按业务类型分组分析

分步提示

  1. 创建订单集合,包含created_at、paid_at、shipped_at、completed_at字段
  2. 使用$project计算时间差
  3. 使用$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 挑战练习:时间序列数据预测

解题思路

  1. 收集历史时间序列数据
  2. 使用聚合管道进行数据聚合
  3. 实现简单的趋势预测算法

常见误区

  • 数据量不足导致预测不准确
  • 没有考虑周期性波动
  • 忽略了异常值的影响

分步提示

  1. 创建时间序列数据集合
  2. 使用移动平均平滑数据
  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 核心要点

  1. 存储机制

    • MongoDB Date类型使用64位整数存储UTC毫秒时间戳
    • 精度为毫秒级,不支持微秒
    • 始终以UTC格式存储,不包含时区信息
  2. PHP操作

    • 使用MongoDB\BSON\UTCDateTime类
    • 通过toDateTime()转换为PHP DateTime对象
    • 注意时区转换的正确处理
  3. 查询优化

    • 为时间字段创建索引
    • 使用复合索引优化多条件查询
    • TTL索引实现自动过期
  4. 聚合操作

    • 使用$year、$month、$day等操作符提取时间部分
    • 使用$dateToString格式化时间
    • 使用$subtract计算时间差

10.2 易错点回顾

  1. 时区混淆:忘记MongoDB存储的是UTC时间,查询时没有正确转换
  2. 精度丢失:期望微秒精度但实际只有毫秒精度
  3. 格式错误:使用不标准的日期格式导致解析失败
  4. 批量时间不一致:循环中创建UTCDateTime导致时间戳不同
  5. TTL索引失效:字段类型不是Date导致TTL不工作

11. 拓展参考资料

11.1 官方文档

11.2 进阶学习路径

  1. 时间序列集合:学习MongoDB 5.0+的时间序列集合特性
  2. 分片策略:了解基于时间的分片键设计
  3. 变更流:使用Change Streams监听数据变化
  4. 性能优化:深入学习时间范围查询的索引优化

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:00

6.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.1

6.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 CST

7.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条, 总额 123

7.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秒)。不支持微秒级精度。

如果需要更高精度,可以考虑:

  1. 使用额外字段存储微秒部分
  2. 使用Decimal128存储高精度时间戳
  3. 使用字符串存储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.123456

8.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)]]
);

// 获取待执行任务