让我们假设我有一个执行以下操作的控制器操作:
简单的实现存在多个问题:
解决这些问题的方法似乎是使用SERIALIZABLE事务隔离级别。问题在于,每个人似乎都认为此事务隔离级别非常危险,因为它可能导致死锁。
给出以下简单的解决方案:
public class AController
{
// ...
public async Task Fn(..., CancellationToken cancellationToken)
{
var calendarSlotExists = dbContext.Slots.Where(...).AnyAsync(cancellationToken);
var appointmentsAreOverlapping = dbContext.Appointments.Where(...).AnyAsync(cancellationToken);
if (calendarSlotExists && !appointmentsAreOverlapping)
dbContext.Appointments.Add(...);
dbContext.SaveChangesAsync(cancellationToken);
}
}
什么是始终防止并发问题的最佳方法?我该如何处理最终的死锁?
答案 0 :(得分:4)
数据库完整性检查是您最好的朋友
根据您的描述,您的约会基于空档。这使问题变得更加简单,因为您可以在SlotId
表上为Appointments
定义一个唯一约束。然后,您将需要一个用于Appointments.SlotId
引用Slot.Id
如果在第3步之前删除了1中提取的日历槽,该怎么办?
数据库将引发外键冲突异常
如果在第2步之后但在第3步之前又预定了另一个约会,怎么办?
数据库会抛出重复的键异常
下一步,您需要捕获这两个异常并将用户重定向回预订页面。再次从DB重新加载数据,并检查是否有无效条目,通知用户进行修改,然后重试。
对于死锁部分,它实际上取决于您的表结构。访问数据的方式,对数据进行索引的方式以及数据库的查询计划。没有确定的答案。
答案 1 :(得分:0)
似乎您需要悲观的并发方法来管理任务。不幸的是,Entity Framework Core不支持它。
或者,您可以使用静态的ConcurrentDictionary或实现自己的ConcurrentHashSet来确保免受多个请求的影响,并避免在第2步之后但在第3步之前进行另一个约会。
关于第1步中取出的日历槽在第3步问题之前被删除,我认为在约会和槽之间有一个外键关系来检查SaveChanges上的数据库完整性,或者让ConcurrentDictionary / ConcurrentHashSet Public并从其他操作中检查它(先删除插槽等),是解决该问题的不错选择。
static ConcurrentDictionary<int, object> operations = new ConcurrentDictionary<int, object>();
public async Task<IActionResult> AControllerAction()
{
int? calendarSlotId = 1; //await dbContext.Slots.FirstOrDefaultAsync(..., cancellationToken))?.Id;
try
{
if (calendarSlotId != null && operations.TryAdd(calendarSlotId.Value, null))
{
bool appointmentsAreOverlapping = false; //await dbContext.Slots.Where(...).AnyAsync(cancellationToken);
if (!appointmentsAreOverlapping)
{
//dbContext.Appointments.Add(...);
//await dbContext.SaveChangesAsync(cancellationToken);
return ...; //All done!
}
return ...; //Appointments are overlapping
}
return ...; //There is no slot or slot is being used
}
catch (Exception ex)
{
return ...; //ex exception (DB exceptions, etc)
}
finally
{
if (calendarSlotId != null)
{
operations.TryRemove(calendarSlotId.Value, out object obj);
}
}
}
答案 2 :(得分:0)
有时,在高可用性场景中,建议权衡立即一致性(通过事务获得)以最终一致性(通过工作流/传奇获得)。
在您的示例中,您可以考虑使用一种中间状态存储“挂起”约会的方法,然后再进行一次一致性检查。
public async Task Fn(..., CancellationToken cancellationToken)
{
// suppose "appointment" is our entity, we will store it as "pending" using
// PendingUntil property (which is Nullable<DateTimeOffset>).
// an appointment is in "pending" state if the PendingUntil property is set
// (not null), and its value is >= UtcNow
var utcNow = DateTimeOffset.UtcNow;
appointment.PendingUntil = utcNow.AddSeconds(5);
// we will then use this property to find out if there are other pending appointments
var calendarSlotExists = await dbContext.Slots.Where(...).AnyAsync(cancellationToken);
var appointmentsAreOverlapping = await dbContext.Appointments
.Where(...)
.Where(a => a.PendingUntil == null ||
a.PendingUntil >= now)
.AnyAsync(cancellationToken);
if (calendarSlotExists && !appointmentsAreOverlapping)
dbContext.Appointments.Add(appointment);
else
return BadRequest(); // whatever you what to return
await dbContext.SaveChangesAsync(cancellationToken); // save the pending appointment
// now check if the pending appointment is still valid
var calendarSlotStillExists = await dbContext.Slots.Where(...).AnyAsync(cancellationToken); // same query as before
// a note on the calendar slot existance: you should of course negate any
// slot deletion for (pending or not) appointments.
// we will then check if there is any other appointment in pending state that was
// stored inside the database "before" this one.
// this query is up to you, below you'll find just an example
var overlappingAppointments = await dbContext.Appointments.Where(...)
.Where(a => a.Id != appointment.Id &&
a.PendingUntil == null ||
a.PendingUntil >= now)
.ToListAsync(cancellationToken);
// we are checking if other appointments (pending or not) have been written to the DB
// of course we need to exclude the appointment we just added
if (!calendarSlotStillExists || overlappingAppointments.Any(a => a.PendingUntil == null || a.PendingUntil < appointment.PendingUntil)
{
// concurrency check failed
// this means that another appointment was added after our first check, but before our appointment.
// we have to remove the our appointment
dbContext.Appointments.Remove(appointment);
await dbContext.SaveChangesAsync(cancellationToken); // restore DB
return BadRequest(); // same response as before
}
// ok, we can remove the pending state
appointment.PendingUntil = null;
await dbContext.SaveChangesAsync(cancellationToken); // insert completed
return Ok();
}
当然,这将使数据库的访问次数增加一倍,但是将完全避免事务(具有死锁和锁定延迟)。
您只需要评估哪个方面对您更重要:可伸缩性或即时一致性。