雷达智富

首页 > 内容 > 程序笔记 > 正文

程序笔记

EntityFramework(EF) 控制并发和事务防止超卖

2025-01-15 4

在使用 Entity Framework (EF) 时,实现加减库存并避免超卖的关键是正确地控制并发和事务。以下是实现方法:

解决超卖的关键点

事务管理:使用事务确保加减库存和库存校验是一个原子操作。 悲观锁:通过数据库层面的锁定(如 FOR UPDATE)确保其他线程无法同时修改相同的记录。 乐观并发控制:使用版本号或时间戳检测是否有其他线程在同一时间修改数据。 避免库存为负值:每次更新库存前检查库存是否充足。

使用悲观锁

通过 EF Core 执行原生 SQL 或存储过程实现悲观锁。

示例代码

using (var context = new YourDbContext())
{
    using (var transaction = await context.Database.BeginTransactionAsync())
    {
        try
        {
            // 使用原生 SQL 加锁 (悲观锁)
            var product = await context.Products
                .FromSqlRaw("SELECT * FROM Products WITH (UPDLOCK, ROWLOCK) WHERE Id = {0}", productId)
                .FirstOrDefaultAsync();

            if (product == null)
            {
                throw new Exception("Product not found");
            }

            if (product.Stock < quantity)
            {
                throw new Exception("Insufficient stock");
            }

            // 减库存
            product.Stock -= quantity;

            // 保存更改
            await context.SaveChangesAsync();

            // 提交事务
            await transaction.CommitAsync();
        }
        catch
        {
            // 回滚事务
            await transaction.RollbackAsync();
            throw;
        }
    }
}

WITH (UPDLOCK, ROWLOCK):确保在查询过程中对目标行加锁,避免其他事务修改。

使用事务 (BeginTransactionAsync) 确保库存校验和更新是一个原子操作。

使用乐观并发控制

通过在表中添加一个 RowVersion 字段,EF Core 自动检测并发冲突。

步骤

在模型中添加一个 RowVersion 字段:

public class Product
{
    public int Id { get; set; }
    public int Stock { get; set; }
    1736903070
    public byte[] RowVersion { get; set; }
}

启用并发检查:

try
{
    var product = await context.Products.FindAsync(productId);

    if (product.Stock < quantity)
    {
        throw new Exception("Insufficient stock");
    }

    product.Stock -= quantity;

    await context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
    // 处理并发冲突
    throw new Exception("Concurrency conflict occurred");
}

Timestamp 属性自动跟踪数据库中的版本号变化。

如果两个线程同时更新同一行,EF 会抛出 DbUpdateConcurrencyException。

数据库层存储过程

另一种方式是在数据库中定义存储过程,确保操作在数据库层面是原子的。

示例存储过程

CREATE PROCEDURE UpdateStock
    @ProductId INT,
    @Quantity INT
AS
BEGIN
    SET XACT_ABORT ON;
    BEGIN TRANSACTION;

    -- 锁定行并检查库存
    SELECT Stock
    FROM Products WITH (UPDLOCK, ROWLOCK)
    WHERE Id = @ProductId;

    IF @@ROWCOUNT = 0
    BEGIN
        ROLLBACK;
        THROW 50000, 'Product not found', 1;
    END

    -- 检查库存是否足够
    IF (SELECT Stock FROM Products WHERE Id = @ProductId) < @Quantity
    BEGIN
        ROLLBACK;
        THROW 50001, 'Insufficient stock', 1;
    END

    -- 更新库存
    UPDATE Products
    SET Stock = Stock - @Quantity
    WHERE Id = @ProductId;

    COMMIT;
END

在 EF 中调用存储过程:

await context.Database.ExecuteSqlRawAsync("EXEC UpdateStock @ProductId = {0}, @Quantity = {1}", productId, quantity);

总结

悲观锁适用于高并发、需要严格保证库存一致性的场景。乐观并发控制适合读取多、写入少的业务。如果业务逻辑复杂,建议将加减库存逻辑放在存储过程中,避免应用层并发问题。

更新于:6小时前
赞一波!

文章评论

评论问答