You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何防止日期时间重叠的日程项被新增/修改并处理并发问题?

ASP.NET Core日程管理系统时间重叠校验与并发冲突落地方案

现有代码基线

  • 控制器层:ScheduleController通过依赖注入获取IScheduleService实例,已实现4个业务接口:
    • GetDraftSchedule:根据工厂编码查询最新草稿状态日程
    • PostSchedule:新建日程主记录
    • PostScheduleItem:为指定日程新增条目
    • PutScheduleItem:更新指定日程条目信息
  • 服务层:已完成AddItemToSchedule(新增日程条目持久化)、ChangeScheduleItem(修改日程条目持久化)两个核心方法的基础业务编码。

一、时间段重叠校验逻辑实现

时间段重叠采用左闭右开区间判定规则,避免相邻时间段(如8:00-8:30与8:30-9:00)被误判为重叠,判定公式为:两个时间段[AStart, AEnd)、[BStart, BEnd)存在重叠的充要条件是 AStart < BEnd && BStart < AEnd。

  1. 先在服务层抽取通用校验私有方法:
    /// <summary>
    /// 校验待写入条目与日程下已有条目是否存在时间冲突
    /// </summary>
    /// <param name="scheduleId">目标日程ID</param>
    /// <param name="newStart">待写入条目开始时间</param>
    /// <param name="newEnd">待写入条目结束时间</param>
    /// <param name="excludeItemId">修改场景下排除当前条目自身,新增场景传null</param>
    private async Task<bool> CheckTimeOverlap(Guid scheduleId, DateTime newStart, DateTime newEnd, Guid? excludeItemId = null)
    {
        var existItems = await _dbContext.ScheduleItems
            .Where(i => i.ScheduleId == scheduleId)
            .Where(excludeItemId.HasValue, i => i.Id != excludeItemId.Value)
            .Select(i => new { i.StartTime, i.EndTime })
            .ToListAsync();
        
        return existItems.Any(item => newStart < item.EndTime && item.StartTime < newEnd);
    }
    
  2. 嵌入现有核心方法:
    • 新增条目场景:在AddItemToSchedule执行持久化前调用校验,若返回true则直接抛出业务异常,提示"当前时间段与已有日程条目冲突,请调整时间"。
    • 修改条目场景:在ChangeScheduleItem执行持久化前调用校验,传入当前修改的条目ID作为排除参数,避免将条目原有时间段误判为冲突。

二、高并发竞态问题解决方案

仅靠应用层查询校验无法抵御并发场景:多个请求同时查询时都可能得到"无时间重叠"的结果,最终写入脏数据。需要结合数据库锁+乐观并发令牌做两层防护。
注意:所有加锁、校验、持久化操作必须包裹在同一个显式数据库事务中,事务隔离级别设置为ReadCommitted,否则更新锁无法实现串行化效果。

1. 并发新增同时间段条目场景处理

采用数据库更新锁(UPDLOCK)+行锁(ROWLOCK)实现同日程下新增操作的串行化:

  • 事务开启后,首先查询并锁定目标日程主记录,同一时间仅能有一个请求拿到该日程的操作锁,其余请求阻塞等待:
    // 锁定目标日程,事务提交/回滚后锁自动释放
    var schedule = await _dbContext.Schedules
        .FromSqlInterpolated($"SELECT * FROM Schedules WITH (UPDLOCK, ROWLOCK) WHERE Id = {scheduleId}")
        .FirstOrDefaultAsync();
    if (schedule == null) throw new KeyNotFoundException("指定日程不存在");
    
  • 拿到锁之后再执行上文的时间重叠校验,校验通过后再写入条目,从根源避免两个请求同时通过校验的问题。

2. 并发修改同一条目场景处理

采用乐观并发控制,保证仅一个修改请求能执行成功:

  • 给ScheduleItem实体添加并发计数字段,添加[ConcurrencyCheck]特性标记为并发令牌:
    public class ScheduleItem
    {
        public Guid Id { get; set; }
        public Guid ScheduleId { get; set; }
        public DateTime StartTime { get; set; }
        public DateTime EndTime { get; set; }
        public string Title { get; set; }
        // 并发更新计数,初始值为0
        [ConcurrencyCheck]
        public int UpdateCount { get; set; } = 0;
    }
    
  • 更新时将查询到的原始UpdateCount作为更新条件,根据受影响行数判断是否更新成功:
    var affected = await _dbContext.ScheduleItems
        .Where(i => i.Id == itemId && i.UpdateCount == originalUpdateCount)
        .ExecuteUpdateAsync(setters => setters
            .SetProperty(i => i.StartTime, newStart)
            .SetProperty(i => i.EndTime, newEnd)
            .SetProperty(i => i.UpdateCount, i => i.UpdateCount + 1)
        );
    // 受影响行数为0说明条目已被其他请求修改,返回409冲突
    if (affected == 0) throw new ConflictException("当前条目已被其他用户更新,请刷新后重试");
    

单元测试验证标准

  • 并发新增场景:用Task.WhenAll模拟多用户同时提交相同时间段的条目请求,断言仅1个请求返回成功,数据库中对应日程下有效条目总数为1,其余请求返回409冲突。
  • 并发修改场景:用Task.WhenAll模拟多用户同时提交同一条目的修改请求,断言仅1个请求返回成功,数据库中该条目UpdateCount字段仅累加1,其余请求返回409冲突。

合法日程条目JSON输入示例

{
  "scheduleId": "a27cf189-4b3d-4d76-9a2e-3c8f1d6a7e2b",
  "title": "一号车间早会",
  "startTime": "2024-06-15T08:00:00",
  "endTime": "2024-06-15T08:30:00",
  "location": "生产区一楼会议室",
  "participants": ["张工", "李主管", "王班长"]
}

内容的提问来源于stack exchange,提问作者Mr.Kadan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 18:03:33