EF Core并发令牌:乐观并发控制的实现原理
引言
在当今高并发的应用场景中,数据一致性是每个开发者必须面对的挑战。你是否曾经遇到过这样的问题:多个用户同时修改同一条数据,后提交的操作覆盖了前一个用户的更改,导致数据丢失?EF Core的并发令牌(Concurrency Token)机制正是为了解决这类问题而设计的。
读完本文,你将掌握:
- 乐观并发控制的基本原理
- EF Core并发令牌的工作机制
- 多种并发令牌的实现方式
- 并发冲突的处理策略
- 实际应用中的最佳实践
乐观并发控制基础
什么是乐观并发控制?
乐观并发控制(Optimistic Concurrency Control)是一种处理并发访问的策略,它假设多个事务同时操作同一数据的概率较低,因此允许多个事务同时进行,只在提交时检查是否存在冲突。
悲观并发 vs 乐观并发
| 特性 | 悲观并发控制 | 乐观并发控制 |
|---|---|---|
| 策略 | 先加锁再操作 | 先操作后检查 |
| 性能 | 可能阻塞其他操作 | 无锁,性能更好 |
| 适用场景 | 高冲突环境 | 低冲突环境 |
| 实现复杂度 | 相对简单 | 需要冲突检测机制 |
EF Core并发令牌机制
并发令牌的核心概念
在EF Core中,并发令牌是一个特殊的属性,用于检测数据在读取和保存之间是否被其他操作修改。当执行更新或删除操作时,EF Core会在WHERE子句中包含并发令牌的检查。
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
// 并发令牌属性
[ConcurrencyCheck]
public byte[] Version { get; set; }
}
并发令牌的工作原理
EF Core的并发控制流程如下:
SQL生成机制
当使用并发令牌时,EF Core生成的UPDATE语句会在WHERE子句中包含并发令牌的检查:
UPDATE Products
SET Name = @p0, Price = @p1, Version = @p2
WHERE Id = @p3 AND Version = @p4
如果Version的值在读取后已被修改,WHERE条件不匹配,影响行数为0,EF Core就会抛出DbUpdateConcurrencyException。
并发令牌的类型与配置
1. 时间戳/行版本令牌
这是最常用的并发令牌类型,数据库会自动维护版本号:
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
// 或者使用Fluent API
modelBuilder.Entity<Product>()
.Property(p => p.RowVersion)
.IsRowVersion();
2. 自定义属性令牌
任何属性都可以配置为并发令牌:
public class Order
{
public int Id { get; set; }
public DateTime OrderDate { get; set; }
[ConcurrencyCheck]
public string ConcurrencyToken { get; set; }
}
// Fluent API配置
modelBuilder.Entity<Order>()
.Property(o => o.ConcurrencyToken)
.IsConcurrencyToken();
3. 组合并发令牌
多个属性可以组合作为并发令牌:
public class User
{
public int Id { get; set; }
public string Username { get; set; }
[ConcurrencyCheck]
public DateTime LastModified { get; set; }
[ConcurrencyCheck]
public string ModifiedBy { get; set; }
}
并发冲突处理策略
1. 自动重试策略
public async Task<bool> UpdateProductAsync(Product product)
{
for (int attempt = 0; attempt < 3; attempt++)
{
try
{
_context.Products.Update(product);
await _context.SaveChangesAsync();
return true;
}
catch (DbUpdateConcurrencyException ex)
{
foreach (var entry in ex.Entries)
{
if (entry.Entity is Product)
{
var databaseValues = await entry.GetDatabaseValuesAsync();
if (databaseValues == null)
{
// 记录已被删除
return false;
}
var databaseProduct = (Product)databaseValues.ToObject();
// 解决冲突策略
if (ShouldOverrideLocalChanges(product, databaseProduct))
{
entry.OriginalValues.SetValues(databaseValues);
}
else
{
// 使用当前值继续尝试
entry.OriginalValues.SetValues(entry.CurrentValues);
}
}
}
}
}
return false;
}
2. 客户端获胜策略
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
// 强制使用客户端值
entry.OriginalValues.SetValues(databaseValues);
await _context.SaveChangesAsync();
}
3. 数据库获胜策略
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
// 使用数据库值,放弃客户端修改
entry.CurrentValues.SetValues(databaseValues);
entry.OriginalValues.SetValues(databaseValues);
// 可以重新抛出异常或返回特定结果
throw new ConcurrencyConflictException("数据已被其他用户修改");
}
高级应用场景
分布式系统并发控制
在分布式系统中,并发控制更加复杂:
public class DistributedConcurrencyHandler
{
private readonly IDistributedCache _cache;
public async Task<string> AcquireLockAsync(string resourceId, TimeSpan timeout)
{
var lockKey = $"lock:{resourceId}";
var token = Guid.NewGuid().ToString();
var acquired = await _cache.StringSetAsync(lockKey, token, timeout, When.NotExists);
return acquired ? token : null;
}
public async Task ReleaseLockAsync(string resourceId, string token)
{
var lockKey = $"lock:{resourceId}";
var currentToken = await _cache.StringGetAsync(lockKey);
if (currentToken == token)
{
await _cache.KeyDeleteAsync(lockKey);
}
}
}
自定义并发令牌提供程序
public class CustomConcurrencyTokenGenerator : IConcurrencyTokenGenerator
{
public object GenerateToken(object entity)
{
return entity switch
{
Product p => $"{p.Id}:{DateTime.UtcNow.Ticks}",
Order o => Interlocked.Increment(ref o.Version),
_ => Guid.NewGuid().ToString()
};
}
}
性能优化建议
1. 选择合适的并发令牌类型
| 令牌类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 行版本 | 数据库自动维护,性能好 | 需要数据库支持 | 大多数场景 |
| 时间戳 | 简单易用 | 精度可能不够 | 低并发环境 |
| 自定义 | 灵活可控 | 需要手动维护 | 特定业务需求 |
2. 避免过度使用并发令牌
只在真正需要并发控制的实体上使用并发令牌,不必要的并发检查会增加数据库负担。
3. 批量操作优化
对于批量更新操作,考虑使用存储过程或原生SQL来避免逐行检查:
await _context.Database.ExecuteSqlRawAsync(
"UPDATE Products SET Price = Price * 1.1 WHERE CategoryId = {0}",
categoryId);
常见问题与解决方案
1. 虚假冲突检测
当并发令牌包含可变数据时可能发生:
// 错误示例:使用可变的DateTime.Now
modelBuilder.Entity<Order>()
.Property(o => o.LastModified)
.IsConcurrencyToken()
.HasDefaultValueSql("GETUTCDATE()"); // 使用数据库函数
// 正确做法:在保存时设置值
order.LastModified = DateTime.UtcNow;
2. 并发令牌更新策略
确保并发令牌在每次修改时都更新:
public override int SaveChanges()
{
var entries = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified || e.State == EntityState.Added);
foreach (var entry in entries)
{
var concurrencyProperties = entry.Properties
.Where(p => p.Metadata.IsConcurrencyToken);
foreach (var property in concurrencyProperties)
{
property.CurrentValue = GenerateTokenValue();
}
}
return base.SaveChanges();
}
测试策略
单元测试并发行为
[Test]
public async Task Should_Throw_ConcurrencyException_When_Version_Changed()
{
// Arrange
var product = new Product { Id = 1, Name = "Test", Version = new byte[8] };
_context.Products.Add(product);
await _context.SaveChangesAsync();
// 模拟并发修改
using var context2 = new AppDbContext();
var product2 = await context2.Products.FindAsync(1);
product2.Name = "Modified";
await context2.SaveChangesAsync();
// Act & Assert
product.Name = "Another Modification";
_context.Products.Update(product);
await Assert.ThrowsAsync<DbUpdateConcurrencyException>(
async () => await _context.SaveChangesAsync());
}
集成测试
[Test]
public async Task Should_Handle_Concurrency_Conflict_Gracefully()
{
// 模拟多个并发请求
var tasks = Enumerable.Range(0, 5)
.Select(i => Task.Run(async () =>
{
using var context = new AppDbContext();
var product = await context.Products.FindAsync(1);
product.Price += 1;
await context.SaveChangesAsync();
}));
// 应该只有部分成功,其他抛出异常
var results = await Task.WhenAll(tasks);
Assert.That(results.Count(r => r.IsFaulted), Is.GreaterThan(0));
}
总结
EF Core的并发令牌机制为处理数据并发访问提供了强大而灵活的解决方案。通过理解其工作原理和不同的实现方式,开发者可以根据具体业务需求选择合适的并发控制策略。
关键要点:
- 选择合适的令牌类型:根据业务场景选择行版本、时间戳或自定义令牌
- 合理处理冲突:实现适当的冲突解决策略,避免数据丢失
- 性能考虑:在并发控制和性能之间找到平衡点
- 测试覆盖:确保并发场景得到充分测试
乐观并发控制是现代应用程序中不可或缺的特性,掌握EF Core的并发令牌机制将帮助您构建更加健壮和可靠的系统。
下一步学习建议:
- 深入了解EF Core的变更跟踪机制
- 学习分布式事务处理模式
- 探索更高级的并发控制算法
记得在实际项目中实践这些概念,并根据具体需求调整实现策略。并发控制是一个需要不断学习和优化的领域,保持对新技术和最佳实践的关注将帮助您构建更好的应用程序。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



