如何防止日期时间重叠的日程项被新增/修改并处理并发问题?
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。
- 先在服务层抽取通用校验私有方法:
/// <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); } - 嵌入现有核心方法:
- 新增条目场景:在
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
相关产品推荐
相关产品推荐

