面试题答案
一键面试Entity Framework处理数据一致性和并发操作的机制
- 乐观并发控制
- 原理:EF默认采用乐观并发控制。当更新实体时,EF会检查自实体加载后,数据库中的数据是否发生了变化。它通过比较原始值(从数据库加载时的值)和当前数据库中的值来判断。如果不一致,EF会抛出
DbUpdateConcurrencyException
异常。 - 示例代码:
- 原理:EF默认采用乐观并发控制。当更新实体时,EF会检查自实体加载后,数据库中的数据是否发生了变化。它通过比较原始值(从数据库加载时的值)和当前数据库中的值来判断。如果不一致,EF会抛出
using (var context = new YourDbContext())
{
var entity = context.YourEntities.FirstOrDefault(e => e.Id == someId);
entity.SomeProperty = "new value";
try
{
context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
// 处理并发异常,重新加载数据并重新尝试更新
var entry = ex.Entries.Single();
entry.OriginalValues.SetValues(entry.GetDatabaseValues());
// 可以选择再次尝试保存,或者通知用户数据已变化
}
}
- 悲观并发控制
- 原理:通过在查询时对数据库中的数据加锁,防止其他事务同时修改这些数据。在EF中,可以使用
AsNoTracking
方法配合数据库的锁提示来实现。例如,在SQL Server中,可以使用WITH (UPDLOCK)
提示。 - 示例代码:
- 原理:通过在查询时对数据库中的数据加锁,防止其他事务同时修改这些数据。在EF中,可以使用
using (var context = new YourDbContext())
{
var entity = context.YourEntities
.FromSqlInterpolated($"SELECT * FROM YourEntities WITH (UPDLOCK) WHERE Id = {someId}")
.AsNoTracking()
.FirstOrDefault();
entity.SomeProperty = "new value";
context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();
}
结合第三方工具(分布式事务协调器)保证数据一致性
- 分布式事务协调器(DTC)概述:DTC是Windows操作系统提供的一种服务,用于管理跨多个资源管理器(如数据库、消息队列等)的分布式事务。在.NET中,可以通过
System.Transactions
命名空间来使用DTC。 - 使用DTC实现分布式事务示例:
using (var scope = new TransactionScope())
{
using (var context1 = new YourDbContext1())
{
var entity1 = new YourEntity1 { SomeProperty = "value1" };
context1.YourEntities1.Add(entity1);
context1.SaveChanges();
}
using (var context2 = new YourDbContext2())
{
var entity2 = new YourEntity2 { SomeProperty = "value2" };
context2.YourEntities2.Add(entity2);
context2.SaveChanges();
}
scope.Complete();
}
在上述代码中,TransactionScope
会自动协调多个数据库上下文之间的事务。如果任何一个SaveChanges
操作失败,整个事务将回滚。
高并发读写场景下避免数据冲突的设计方案
- 读写分离
- 原理:将读操作和写操作分离到不同的数据库服务器或数据库实例上。读操作指向只读副本,写操作指向主数据库。这样可以减轻主数据库的压力,减少读写冲突。
- 实现方式:可以使用数据库复制技术(如SQL Server的复制、MySQL的主从复制等)来创建只读副本。在EF中,可以根据操作类型动态切换数据库连接字符串。
- 排队写操作
- 原理:将写操作放入队列(如RabbitMQ、Azure Service Bus等),然后由一个后台服务按顺序从队列中取出写操作并执行。这样可以避免多个写操作同时竞争资源。
- 示例代码(以RabbitMQ为例):
// 生产者代码
using (var connection = new ConnectionFactory { HostName = "localhost" }.CreateConnection())
using (var channel = connection.CreateModel())
{
channel.QueueDeclare(queue: "writeQueue", durable: false, exclusive: false, autoDelete: false, arguments: null);
var message = "Write operation data";
var body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: "", routingKey: "writeQueue", basicProperties: null, body: body);
}
// 消费者代码
using (var connection = new ConnectionFactory { HostName = "localhost" }.CreateConnection())
using (var channel = connection.CreateModel())
{
channel.QueueDeclare(queue: "writeQueue", durable: false, exclusive: false, autoDelete: false, arguments: null);
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
// 执行写操作
using (var context = new YourDbContext())
{
// 根据消息内容执行写操作
context.SaveChanges();
}
channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
};
channel.BasicConsume(queue: "writeQueue", autoAck: false, consumer: consumer);
Console.WriteLine("Press [enter] to exit.");
Console.ReadLine();
}
- 缓存策略
- 原理:使用缓存(如Redis)来存储经常读取的数据。读操作首先从缓存中获取数据,如果缓存中没有,则从数据库读取并将数据放入缓存。写操作在成功后,更新缓存数据。
- 示例代码(以Redis为例):
using StackExchange.Redis;
// 读操作
public YourEntity GetEntityFromCacheOrDb(int id)
{
var redis = ConnectionMultiplexer.Connect("localhost");
var db = redis.GetDatabase();
var cachedValue = db.StringGet($"entity:{id}");
if (!cachedValue.IsNullOrEmpty)
{
return JsonConvert.DeserializeObject<YourEntity>(cachedValue);
}
using (var context = new YourDbContext())
{
var entity = context.YourEntities.FirstOrDefault(e => e.Id == id);
if (entity != null)
{
db.StringSet($"entity:{id}", JsonConvert.SerializeObject(entity));
}
return entity;
}
}
// 写操作
public void UpdateEntity(YourEntity entity)
{
using (var context = new YourDbContext())
{
context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();
}
var redis = ConnectionMultiplexer.Connect("localhost");
var db = redis.GetDatabase();
db.StringSet($"entity:{entity.Id}", JsonConvert.SerializeObject(entity));
}