题干与适用场景
设计类似 Google Calendar 的事件冲突处理系统。用户可以创建、修改、删除事件;事件包含参与者、开始结束时间、时区、重复规则、提醒、可见性和会议链接。创建或修改时要判断冲突,并支持拒绝、警告后继续或允许覆盖。假设空闲时间查询远多于写入,常见请求需要几十毫秒级响应;私有日历只能暴露 busy 状态。
面试官考察点
强回答会先定义冲突语义,再谈数据库。核心信号包括:使用半开区间避免首尾相接误报;把规范事件模型与重叠查询索引分开;明确 RRULE、例外和滚动物化边界;说明时区规则如何转成绝对时间;为派生索引选择 outbox 或 CDC;并处理两个写入者同时预订同一资源的竞态。只说“查数据库是否重叠”的回答无法覆盖这些失败路径。
回答前需要澄清的问题
- 首尾相接的两个事件是否冲突?若不冲突,应使用半开区间
[start, end)。 - 重复系列需要展开到多远?无限重复必须采用滚动窗口或按查询展开。
- 多参与者中只检查必需参与者,还是任意 busy 参与者都会阻止创建?这决定查询扇出和策略。
- 双重预订是拒绝、警告还是允许覆盖?会议室等硬资源与个人日历可能不同。
- 查看他人日历时返回完整详情、busy 区间还是完全隐藏?这决定 ACL 和响应模型。
30 秒回答框架
“我会保留规范事件和参与者表,再维护按日历分区的空闲时间索引。冲突使用半开区间:只有 existing.start < new.end 且 new.start < existing.end 才重叠;free 事件不占用时间。重复规则按时区保存 RRULE,在有限时间窗物化并记录例外。写入通过版本检查或资源锁保证一致,事件提交后由 outbox 更新派生索引。查询只返回调用者有权限看到的 busy 信息,并监控索引漂移、重建和通知延迟。”
分步骤深入解答
1. 定义时间与冲突谓词
把每次发生实例表示为 [startutc, endutc),保留原始时区和本地规则用于显示与重复展开。两个实例冲突当且仅当 a.start < b.end 且 b.start < a.end。因此 10:00–11:00 与 11:00–12:00 不冲突;零时长或结束早于开始的输入应拒绝。全天事件先按日历时区转换为边界,再进入同一谓词。
2. 分离规范模型与可用性索引
规范层保存事件、参与者、RRULE、例外、ACL 和版本号,适合编辑与审计。可用性索引只保留 calendar_id、实例起止、占用状态、事件 ID 和可见性标签,按日历与开始时间排序;冲突查询扫描目标时间窗,而不是读取完整事件 JSON。索引可按月或租户分区,热点日历需要缓存或专用分片。
3. 处理重复规则和例外
保存 iCalendar 风格的 RRULE,查询未来窗口时展开实例。滚动物化例如只生成未来 90 天,并在窗口临近时延伸;查询更远范围时按需展开并缓存。this occurrence、this and future 和整系列修改必须产生例外记录或新的系列版本,不能直接改写过去实例。每个实例都要独立参与冲突判定和通知。
4. 写入流程与并发控制
创建或修改先验证时间、ACL 和参与者策略,再在同一事务中检查当前索引版本。个人日历可用乐观版本号,冲突时让客户端重试;严格互斥的会议室或预约资源可按资源加锁或使用数据库排他约束。提交事件后写 outbox 记录,由消费者更新索引并发送通知;索引暂时落后时,读路径可回源规范表或标记结果不确定,不能静默返回无冲突。
5. 隐私、查询和规模
查询响应按 ACL 分层:有权限者看到标题和参与者,其他人只看到 busy 时间段,私有事件显示为不可辨识的占用块。自由/暂定/外出状态按产品策略映射为占用或提示。读多写少时可对常用时间窗缓存;缓存键包含日历版本或失效水位。跨组织 free/busy 查询应设置超时和部分结果策略,避免一个外部日历拖慢全部参与者。
6. 一致性、修复和失败场景
规范事件是事实源,索引是可重建派生物。outbox 或 CDC 保证提交成功后最终一定产生索引更新;消费者按事件版本幂等处理,旧版本消息不能覆盖新版本。定期比较规范实例与索引的哈希或计数,发现漂移后按日历重建并记录影响范围。通知发送失败可重试,但不能回滚已经成功的事件提交。
高质量示范回答
我会先约定 [start, end) 的冲突规则:只有两个区间严格相交才算冲突,首尾相接不冲突,free 事件不占用时间。规范事件表保存原始时区、RRULE、例外、参与者和 ACL;另外维护按日历分区的实例索引,只存起止时间、占用状态和事件 ID,以便在时间窗内快速扫描。RRULE 按事件时区保存,未来 90 天滚动物化,远期按查询展开,例外分别表示单次、后续或整系列修改。
写入时先做权限和策略检查,再用乐观版本或资源锁保护同一日历的并发修改。事件和 outbox 在同一事务提交,消费者按版本幂等更新索引并发送通知;索引是派生物,可从规范表重建。返回冲突时按 ACL 只给可见信息。若是个人日历,我会允许警告后继续;若是会议室这类硬资源,则使用排他约束或锁。监控重点是索引漂移、版本落后、查询延迟和外部 free/busy 超时。
常见错误
- 错误表现:用
start <= other.end判断重叠 → 首尾相接被误报,提醒和可用时间错误 → 明确采用半开区间并拒绝非法零时长事件。 - 错误表现:每次查询都展开无限重复系列 → 复杂度和延迟无上界 → 设置滚动物化窗口,远期按需展开并缓存。
- 错误表现:把完整事件 JSON 直接当冲突索引 → 读路径扫描大字段且泄露隐私 → 建立只含时间和占用状态的派生索引,并按 ACL 脱敏。
- 错误表现:索引更新失败就回滚用户事件 → 事实源与通知、搜索耦合,恢复困难 → 事务写 outbox,异步重试并提供索引重建。
- 错误表现:用全局锁解决所有并发 → 热点日历拖慢全系统 → 只在严格互斥资源按资源锁,普通日历使用版本检查和重试。
追问及应对
如何实现“找出所有参与者都空闲的时间”?
先为每个必需参与者查询目标窗口的 busy 区间,再在统一时间轴上做区间合并与交集求补。参与者越多,查询扇出越大,应并行执行、限制窗口长度,并对无权限或超时的日历返回部分结果和不确定标记,而不是把隐私信息暴露出来。
夏令时切换后,重复会议只在部分日期冲突,如何排查?
检查 RRULE 的时区标识、原始本地时间、展开器版本和例外记录;不要把本地时间先转 UTC 后丢失时区再重复。用跨 DST 边界的固定样例重放,比较每次实例的本地显示时间、UTC 边界和冲突谓词,确认是规则展开还是索引物化错误。
严格禁止会议室双订时,版本检查不够怎么办?
两个请求可能同时读到空闲。对同一资源的时间范围使用数据库排他约束、串行化事务或短时资源锁,并让失败请求明确返回冲突实例。锁只能覆盖提交窗口,不能依赖缓存;高热点资源可按资源分片并限制重试,避免锁队列拖垮其他日历。