在C#项目中使用EFCore操作数据库时,多用户同时修改同一条数据会触发并发更新问题,若没有合理的控制机制,后提交的更新会直接覆盖先提交的更新,造成数据丢失或者业务逻辑错误。RowVersion是数据库层面提供的并发控制方案,能有效解决这类冲突,下面详细介绍其实现方式。

并发更新冲突的产生场景
假设数据库中存在一条用户积分记录,用户A和用户B同时查询到该记录的积分值为100,用户A将其修改为120并提交,用户B随后也将自己查询到的100修改为150并提交。如果没有并发控制,用户B的更新会直接覆盖用户A的修改,最终积分值为150,用户A的20积分修改就丢失了。
EFCore默认不会主动检测这类冲突,需要我们手动配置并发令牌来实现冲突检测。
RowVersion的工作原理
RowVersion是SQL Server等数据库提供的自动递增的时间戳类型字段,每次对记录进行更新时,数据库会自动将该字段的值加1。当EFCore执行更新操作时,会将查询时获取的RowVersion值作为更新条件的一部分,如果当前数据库中的RowVersion值和查询时的值不一致,说明记录已经被其他操作修改过,更新语句会影响0行数据,EFCore就会抛出并发冲突异常。
EFCore中配置RowVersion
1. 实体类定义
首先在实体类中添加RowVersion属性,使用byte[]类型,并且添加Timestamp特性或者Fluent API配置将其标记为并发令牌。
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class UserScore
{
[Key]
public int Id { get; set; }
public int UserId { get; set; }
public int Score { get; set; }
// RowVersion字段,标记为时间戳并发令牌
[Timestamp]
public byte[] RowVersion { get; set; }
}
2. Fluent API配置方式
如果不想使用数据注解,也可以在DbContext的OnModelCreating方法中通过Fluent API配置RowVersion:
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
public DbSet<UserScore> UserScores { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置RowVersion为并发令牌
modelBuilder.Entity<UserScore>()
.Property(u => u.RowVersion)
.IsRowVersion();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// 替换为实际的数据库连接字符串
optionsBuilder.UseSqlServer("Server=127.0.0.1;Database=TestDb;Trusted_Connection=True;");
}
}
3. 数据库迁移
配置完成后,执行EFCore迁移命令会在数据库中生成对应的RowVersion字段,SQL Server中该字段类型为timestamp或者rowversion。
处理并发冲突的完整流程
当并发冲突发生时,EFCore会抛出DbUpdateConcurrencyException异常,我们需要捕获该异常并根据业务需求处理,常见的处理方式有重试更新、提示用户数据已变更等。
基础处理示例
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
public class ScoreService
{
private readonly AppDbContext _dbContext;
public ScoreService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public void UpdateUserScore(int userId, int newScore)
{
try
{
var userScore = _dbContext.UserScores.FirstOrDefault(u => u.UserId == userId);
if (userScore == null)
{
Console.WriteLine("用户积分记录不存在");
return;
}
// 修改积分
userScore.Score = newScore;
_dbContext.SaveChanges();
Console.WriteLine("更新成功");
}
catch (DbUpdateConcurrencyException ex)
{
// 捕获并发冲突异常
Console.WriteLine("数据已被其他用户修改,请重新查询后再操作");
// 可以获取当前数据库中的最新值
var entry = ex.Entries.Single();
var databaseValues = entry.GetDatabaseValues();
if (databaseValues != null)
{
Console.WriteLine($"当前数据库最新积分值为:{databaseValues["Score"]}");
}
}
}
}
进阶重试处理示例
如果业务允许,可以在捕获到并发异常后重新查询最新数据,合并修改后再次尝试更新,最多重试指定次数:
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
public class ScoreService
{
private readonly AppDbContext _dbContext;
private const int MaxRetryCount = 3; // 最大重试次数
public ScoreService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public void UpdateUserScoreWithRetry(int userId, int newScore)
{
int retryCount = 0;
while (retryCount < MaxRetryCount)
{
try
{
var userScore = _dbContext.UserScores.FirstOrDefault(u => u.UserId == userId);
if (userScore == null)
{
Console.WriteLine("用户积分记录不存在");
return;
}
// 保留本地修改的字段,这里示例为直接覆盖为新的积分值
userScore.Score = newScore;
_dbContext.SaveChanges();
Console.WriteLine("更新成功");
return;
}
catch (DbUpdateConcurrencyException)
{
retryCount++;
if (retryCount >= MaxRetryCount)
{
Console.WriteLine("多次重试后仍然发生冲突,请稍后重试");
return;
}
Console.WriteLine($"发生并发冲突,第{retryCount}次重试");
// 重新加载最新数据到当前上下文
_dbContext.ChangeTracker.Entries<UserScore>().First().Reload();
}
}
}
}
进阶注意事项
- RowVersion字段仅对标记了该令牌的实体生效,如果实体没有配置并发令牌,EFCore不会检测并发冲突。
- 如果项目使用的是MySQL数据库,可以使用
IsConcurrencyToken()配置DateTime或者long类型的版本字段,配合触发器或者应用层逻辑实现类似RowVersion的效果。 - 并发冲突处理需要结合具体业务场景,比如涉及金额、库存等核心数据,建议优先提示用户手动确认,避免自动重试导致业务逻辑错误。
- 不要将RowVersion字段暴露给前端,避免前端篡改该值导致并发控制失效。
并发控制的核心是平衡数据一致性和系统性能,RowVersion是轻量级的数据库层面方案,适合大多数单库场景的并发冲突处理,如果是分布式场景,可能需要结合分布式锁等机制实现更复杂的控制。
EFCoreRowVersion并发更新C#时间戳修改时间:2026-06-11 03:45:42