En el desarrollo de aplicaciones distribuidas y de alta demanda, la integridad de los datos no es negociable. Cuando dos hilos o procesos intentan modificar el mismo registro simultáneamente, entramos en el terreno de las Race Conditions. Para resolver esto en .NET (EF Core / SQL), tenemos dos filosofías opuestas.
En este tutorial, vamos a ensuciarnos las manos con el código para entender cómo operan bajo el capó.
Pessimistic Locking: «Nadie toca esto hasta que yo termine»
El bloqueo pesimista asume que el conflicto es inminente. Bloquea el recurso en la base de datos desde el momento en que se lee hasta que la transacción se completa.
Implementación con EF Core y Transacciones
Para aplicar un bloqueo pesimista real en SQL Server a través de EF Core, solemos recurrir a Raw SQL o interceptores para inyectar «Hints» de bloqueo como UPDLOCK.
using var transaction = await _context.Database.BeginTransactionAsync();
try
{
// Usamos UPDLOCK para asegurar que otros procesos esperen
// hasta que esta transacción haga COMMIT o ROLLBACK
var item = await _context.WorkItems
.FromSqlRaw("SELECT * FROM WorkItems WITH (UPDLOCK) WHERE Id = {0}", itemId)
.FirstOrDefaultAsync();
if (item == null) return;
item.Status = "In Progress";
item.LastUpdated = DateTime.UtcNow;
await _context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch (Exception)
{
await transaction.RollbackAsync();
throw;
}
¿Por qué usarlo?
- Consistencia Total: Ideal para sistemas financieros o de inventario crítico donde el costo de un error es mayor al costo de esperar.
- Escenario de Falla: Si no usas esto en un sistema de reserva de asientos, podrías terminar con un «Overbooking» porque dos procesos leyeron que el asiento estaba libre al mismo tiempo.
Optimistic Locking: «Validar antes de Confirmar»
Aquí no bloqueamos nada al leer. Simplemente guardamos una «versión» del registro. Al intentar guardar, comparamos si la versión en la base de datos sigue siendo la misma que leímos.
Implementación con RowVersion (Timestamp)
EF Core maneja esto de forma elegante usando una propiedad decorada con [Timestamp].
public class WorkItem
{
public int Id { get; set; }
public string Title { get; set; }
// El motor de DB (SQL Server) actualiza esto automáticamente
[Timestamp]
public byte[] RowVersion { get; set; }
}
El Flujo de Trabajo (The "Check-and-Save")
Cuando ejecutas SaveChangesAsync(), EF Core genera un SQL similar a:
UPDATE WorkItems SET Title = @p1 WHERE Id = @id AND RowVersion = @oldVersion
C#
try
{
var item = await _context.WorkItems.FindAsync(itemId);
item.Title = "Updated Title";
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// ¡Conflicto detectado!
// Alguien cambió el registro entre nuestra lectura y el guardado
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
// Lógica de resolución: ¿Ganamos nosotros o el que ya guardó?
throw new Exception("El registro fue modificado por otro usuario.");
}
El Flujo de Trabajo (The «Check-and-Save»)
Cuando ejecutas SaveChangesAsync(), EF Core genera un SQL similar a: UPDATE WorkItems SET Title = @p1 WHERE Id = @id AND RowVersion = @oldVersion
try
{
var item = await _context.WorkItems.FindAsync(itemId);
item.Title = "Updated Title";
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
// ¡Conflicto detectado!
// Alguien cambió el registro entre nuestra lectura y el guardado
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
// Lógica de resolución: ¿Ganamos nosotros o el que ya guardó?
throw new Exception("El registro fue modificado por otro usuario.");
}
Comparativa de Rendimiento (2025-2026)
| Característica | Pessimistic Locking | Optimistic Locking |
| Escalabilidad | Baja (Mantiene conexiones abiertas y bloqueos) | Alta (No bloquea recursos de DB) |
| Experiencia de Usuario | El usuario espera (Latencia) | El usuario puede recibir un error al final |
| Manejo de Errores | Prevención de Deadlocks manual | Catch de DbUpdateConcurrencyException |
| Uso Ideal | Alta contención (Muchos escriben al mismo tiempo) | Baja contención (Lecturas frecuentes, pocas colisiones) |
Notas
- Analiza la Contención: Si el 99% de las veces los usuarios editan registros diferentes, usa Optimistic Locking. Ahorrarás recursos de servidor y mejorarás el throughput.
- Cuidado con los Deadlocks: En bloqueos pesimistas, siempre adquiere los bloqueos en el mismo orden jerárquico para evitar que dos procesos se bloqueen mutuamente para siempre.
- No confíes solo en el ID: Si usas locking optimista, asegúrate de que tu RowVersion sea parte de la lógica de negocio. En APIs REST, usa el header If-Match con el valor de la versión para implementar concurrencia de extremo a extremo.
- UX Consciente: Si eliges el modelo optimista, el frontend debe ser capaz de manejar el error 409 (Conflict) y permitir al usuario «fusionar» sus cambios o recargar la versión más reciente.
Conslusiones
En arquitecturas de microservicios, el bloqueo pesimista es casi un antipatrón debido a la latencia de red. Prioriza siempre el bloqueo optimista con estrategias de reintento (Retry Policies) usando librerías como Polly.
