在 Web API 中,乐观锁(Optimistic Locking)和悲观锁(Pessimistic Locking)是两种常见的并发控制机制。它们的目的都是在多个用户同时访问和修改相同资源时,确保数据的一致性和完整性。
乐观锁
乐观锁的思想是假设并发访问的操作不会造成冲突,因此在读取和修改资源时不加锁,而是通过版本号或时间戳等机制来检测并发冲突。当两个或多个用户同时修改同一资源时,系统会比对版本号或时间戳,并根据结果判断是否发生了冲突。如果发生了冲突,可以选择回滚事务或采取其他处理方式。
在 Web API 中实现乐观锁通常涉及以下步骤:
- 在数据模型中增加一个版本号字段或时间戳字段。
- 在更新操作中比对客户端提交的版本号或时间戳与数据库中的值是否一致。
- 如果一致,则执行更新操作并递增版本号或更新时间戳。
- 如果不一致,则说明发生了并发冲突,根据业务需求进行相应的处理,例如返回冲突错误信息或重新尝试操作。
csharp
// 定义数据模型,包含版本号字段
public class Order
{
public int Id { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
public int Version { get; set; }
}
csharp
// 定义更新操作接口
[HttpPut("{id}")]
public async Task<IActionResult> UpdateOrder(int id, [FromBody] Order order)
{
// 查询数据库中的订单记录
var existingOrder = await _dbContext.Orders.FindAsync(id);
// 检查版本号是否一致
if (existingOrder.Version != order.Version)
{
return Conflict("The order has been updated by another user. Please refresh and try again.");
}
// 更新订单记录,并递增版本号
existingOrder.ProductName = order.ProductName;
existingOrder.Quantity = order.Quantity;
existingOrder.Version++;
// 提交事务
try
{
await _dbContext.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// 处理并发冲突异常
return Conflict("The order has been updated by another user. Please refresh and try again.");
}
// 返回更新后的订单记录
return Ok(existingOrder);
}
我们通过检查客户端提交的订单记录的版本号与数据库中的版本号是否一致来判断是否发生了并发冲突。如果版本号不一致,则返回冲突错误信息;否则,更新数据库中的订单记录,并递增版本号,最后返回更新后的订单记录。
需要注意的是,如果多个用户同时修改同一条订单记录,只有一个用户能够成功地提交更新操作,其他用户需要重新获取最新版本的订单记录并重新尝试更新。这种机制可以有效地避免并发冲突,确保数据的一致性和完整性。
悲观锁
悲观锁的思想是假设并发访问的操作会造成冲突,因此在读取和修改资源时会加锁,阻止其他用户同时修改。悲观锁通常使用数据库的锁机制实现,如行级锁或表级锁。
在 Web API 中实现悲观锁通常需要对数据源进行加锁操作,以确保资源的独占性。具体实现方式包括:
- 使用关系型数据库提供的事务和锁机制,在读取和修改资源时加锁,并释放锁。
- 使用分布式锁机制,如 Redis 分布式锁,在操作期间将资源锁定,并在操作完成后释放锁。
- 需要注意的是,悲观锁可能会带来性能开销,并且在高并发情况下可能导致资源争用和死锁等问题。因此,在选择使用乐观锁还是悲观锁时,需要根据具体场景和需求进行权衡和选择。
- 无论是乐观锁还是悲观锁,在实际应用中都需要综合考虑数据一致性、并发性能和系统复杂度等因素,选择适合的并发控制策略。
csharp
// 定义数据库上下文
public class ApplicationDbContext : DbContext
{
public DbSet<Order> Orders { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 在数据库中创建唯一索引,用于加锁
modelBuilder.Entity<Order>()
.HasIndex(o => o.Id)
.IsUnique()
.HasFilter(null);
}
}
csharp
// 定义数据模型
public class Order
{
public int Id { get; set; }
public string ProductName { get; set; }
public int Quantity { get; set; }
}
csharp
// 定义更新操作接口
[HttpPut("{id}")]
public async Task<IActionResult> UpdateOrder(int id, [FromBody] Order order)
{
// 加锁
using (var dbContextTransaction = await _dbContext.Database.BeginTransactionAsync())
{
try
{
// 查询数据库中的订单记录并加锁
var existingOrder = await _dbContext.Orders.FirstOrDefaultAsync(o => o.Id == id, cancellationToken: dbContextTransaction.GetDbTransaction().Connection);
if (existingOrder == null)
{
return NotFound();
}
// 更新订单记录
existingOrder.ProductName = order.ProductName;
existingOrder.Quantity = order.Quantity;
// 提交事务
await _dbContext.SaveChangesAsync();
await dbContextTransaction.CommitAsync();
}
catch (Exception)
{
// 处理异常
await dbContextTransaction.RollbackAsync();
throw;
}
}
// 返回更新后的订单记录
return Ok(order);
}
我们通过使用数据库事务和行级锁机制实现悲观锁。在更新操作开始时,我们通过查询数据库并加锁获取订单记录。然后,我们对订单记录进行更新,最后提交事务。如果在更新过程中发生异常,将回滚事务并处理异常。
注意
悲观锁使用了数据库的锁机制,确保了资源的独占性,但也可能带来性能开销和并发性能问题。因此,在使用悲观锁时需要谨慎考虑,并根据具体情况进行权衡和选择。