Dapper 事务,读取时锁定行

Dapper transaction, lock row on read

我需要 dapper 的读锁,因为我有一个特定的情况,我需要从数据库中读取行并锁定它,然后执行一些其他操作。

这就是我所说的小巧玲珑:

    protected async Task<List<T>> LoadData<T, U>(string sql, U parameters)
    {
        var rows = await getConnection().QueryAsync<T>(sql, parameters, commandType: CommandType.StoredProcedure, transaction: getTransaction());

        return rows.ToList();
    }

但是没有行被锁定。 有趣的是,保存数据的交易工作得很好。

    protected async Task<int> SaveData<U>(string sql, CommandType commandType, U parameters)
    {
        return await getConnection().ExecuteAsync(sql, parameters, commandType: commandType, transaction: getTransaction());
    }

在调用 dapper 之前打开连接并开始事务...像这样:

        if (connection.State != ConnectionState.Open)
            connection.Open();

        transaction = connection.BeginTransaction(IsolationLevel.Serializable);

SQL 查询是一个简单的 select 语句,如下所示:

    SELECT * FROM [dbo].[Tab01] WHERE [Id] = @Id

在 SQL 服务器中,SERIALIZABLE 事务不会对 SELECT 查询使用排他锁或限制锁。相反,在与您的 SELECT 查询相对应的事务期间,会获取并持有共享锁。所以如果你 运行

 SELECT * FROM [dbo].[Tab01] WHERE [Id] = @Id

您的事务将在 Tab01 的 Id 索引上接收共享 (S) 键范围锁,覆盖 @Id 的值。这将防止任何其他会话更改该行,或插入具有该 ID 的行。

如果两个 SERIALIZABLE 事务都运行

 SELECT * FROM [dbo].[Tab01] WHERE [Id] = @Id

对于相同的 ID,然后两者都尝试插入具有该值的行,或更新该行,一个将因死锁而失败。所以 SERIALIZABLE 确实 防止会话覆盖彼此的数据,它使用死锁错误来做到这一点,这可能很不方便。

SERIALIZABLE 保证提交的结果与会话已序列化(一次执行一个)相同,但它实际上并不执行该序列化。它乐观地允许并发事务读取相同的数据,但多个会话试图更新已读取的数据,这将导致死锁。

在 SQL 中,服务器要对读取设置限制性锁需要锁提示。通常你使用:

 SELECT * FROM [dbo].[Tab01] with (updlock,serializable) WHERE [Id] = @Id

强制读取使用限制性更强的更新 (U) 锁,并使用 SERIALIZABLE 样式的键范围锁定,以防当前没有具有该 ID 的行。

长时间持有锁是一个非常非常糟糕的主意。因为您无法控制锁定的粒度和长度,所以您最终可能会锁定一半的数据库。

你应该做的是两个选项之一

  • 要么在单个批次中使用事务执行您的“操作”

  • 或者使用其他机制锁定该行。对于第二个选项,您可能有一个 LockedBy 列,或者可能使用 sp_getapplock.


您的代码还有一个问题:

所有迹象表明您正在其他地方创建和缓存连接和事务对象,而不是使用 using 处理。

您的代码应该如下所示:

    protected Task DoStuff()
    {
        using(var connection = new SqlConnection(getConnectionString())
        {
            LoadData(conn, ....)
            
        }
    }

    protected async Task<List<T>> LoadData<T, U>(SqlConnection conn, string sql, U parameters)
    {
        using(var tran = conn.BeginTransaction())
        {
            var rows = await conn.QueryAsync<T>(sql, parameters, commandType: CommandType.StoredProcedure, transaction: tran);
            tran.Commit();
            return rows.ToList();
        }
    }

    protected async Task<int> SaveData<U>(SqlConnection conn, string sql, CommandType commandType, U parameters)
    {
        using(var tran = conn.BeginTransaction())
        {
            var result = await conn.ExecuteAsync(sql, parameters, commandType: commandType, transaction: tran);
            tran.Commit();
            return result;
        }
    }

另见 C# Data Connections Best Practice?