C # и SQL Server: тупик в параллельных транзакциях - PullRequest
0 голосов
/ 20 декабря 2018

Я тестирую пользовательский класс UnitOfWork с System.Data.SqlClient объектами, он мне нужен для управления возможной одновременной транзакцией и подключением к БД для некоторых простых операций ado.net.

У меня тупик между первым или третьим завершенным выполнением, во втором выберите в классе Repo1 или Repo2, один после команды вставки.

Это происходит даже при "Serializable" изоляцииуровень.

Я ожидаю, что даже в отдельной задаче программа может выполнить все транзакции без конфликтов, таким образом, что каждая транзакция ставится в очередь в произвольном порядке, но, возможно, я просто использую неправильный уровень изоляции.

Мое намерение состоит в следующем

  • в той же транзакции: сделать выбор в таблице после вставки, и я ожидаю получить значение, вставленное в предыдущую команду ната же транзакция.

  • в командах вне этой транзакции: я ожидаю, что выборка считывает фактические зафиксированные значения, игнорируя незафиксированные значения

Этомой код

public class AdoNetUnitOfWork : IDisposable
{
    SqlConnection _connection;
    bool _ownsConnection;
    SqlTransaction _transaction;

    public AdoNetUnitOfWork(SqlConnection connection, bool ownsConnection)
    {
        _connection = connection;
        _ownsConnection = ownsConnection;
        _transaction = connection.BeginTransaction(System.Data.IsolationLevel.ReadCommitted);
    }

    public SqlCommand CreateCommand()
    {
        var command = _connection.CreateCommand();
        command.Transaction = _transaction;
        return command;
    }

    public void SaveChanges()
    {
        if (_transaction == null)
            throw new InvalidOperationException("Transaction have already been commited. Check your transaction handling.");

        _transaction.Commit();
        _transaction = null;
    }

    public void Dispose()
    {
        if (_transaction != null)
        {
            _transaction.Rollback();
            _transaction = null;
        }

        if (_connection != null && _ownsConnection)
        {
            _connection.Close();
            _connection = null;
        }
    }
}

public class UnitOfWorkFactory
{
    public static AdoNetUnitOfWork Create()
    {
        var connection = new SqlConnection(
            "Data Source=localhost;Initial Catalog=***;Integrated Security=False;User Id=**;Password=***;MultipleActiveResultSets=True");
        connection.Open();

        return new AdoNetUnitOfWork(connection, true);
    }
}

Класс бизнес-логики:

public class Service
{
    public Service()
    {
    }

    public OutputStr MakeCalls()
    {
        using (var uow = UnitOfWorkFactory.Create())
        {
            var repo1 = new Repo1(uow);
            repo1.ExecuteQuery(out int idStart1, out int idEnd1);

            var repo2 = new Repo2(uow);
            repo2.ExecuteQuery(out int idStart2, out int idEnd2);

            OutputStr str = new OutputStr()
                                { 
                                    IdStart1 = idStart1, IdEnd1 = idEnd1,
                                    IdStart2 = idStart2, IdEnd2 = idEnd2
                                };

            uow.SaveChanges();

            return str;
        }
    }
}

public class OutputStr
{
    public int IdStart1 { get; set; }
    public int IdEnd1 { get; set; }
    public int IdStart2 { get; set; }
    public int IdEnd2 { get; set; }
}

Класс выполнения запроса к таблице 1:

public class Repo1
{
    AdoNetUnitOfWork _uow;

    public Repo1(AdoNetUnitOfWork uow)
    {
        _uow = uow;
    }

    public void ExecuteQuery(out int idStart, out int idEnd)
    {
        SqlCommand cmd = _uow.CreateCommand();

        cmd.CommandText = "SELECT COALESCE(MAX(Id),0) from Table1";
        idStart = int.Parse(cmd.ExecuteScalar().ToString());

        cmd.CommandText = @"
            INSERT INTO Table1
               ([Id])
               SELECT COALESCE(MAX(Id),0)+1 
                from Table1";
        cmd.ExecuteNonQuery();

        cmd.CommandText = "SELECT COALESCE(MAX(Id),0) from Table1";
        idEnd = int.Parse(cmd.ExecuteScalar().ToString());
        //idEnd = 0;
    }
}

Скрипт для создания таблицы1:

CREATE TABLE [dbo].[Table1]
(
    [Id] [INT] NOT NULL
) ON [PRIMARY]
GO

Класс выполнения запроса2

public class Repo2
{
    AdoNetUnitOfWork _uow;

    public Repo2(AdoNetUnitOfWork uow)
    {
        _uow = uow;
    }

    public void ExecuteQuery(out int idStart, out int idEnd)
    {
        SqlCommand cmd = _uow.CreateCommand();

        cmd.CommandText = "SELECT COALESCE(MAX(Id),0) from Table2";
        idStart = int.Parse(cmd.ExecuteScalar().ToString());

        cmd.CommandText = @"INSERT INTO Table2 ([Id])
                                SELECT COALESCE(MAX(Id),0)+1 
                                FROM Table2";
        cmd.ExecuteNonQuery();

        cmd.CommandText = "SELECT COALESCE(MAX(Id), 0) FROM Table2";
        idEnd = int.Parse(cmd.ExecuteScalar().ToString());
        //idEnd = 0;
    }
}

Скрипт для создания таблицы 2

CREATE TABLE [dbo].[Table2]
(
    [Id] [INT] NOT NULL
) ON [PRIMARY]
GO

Консольный класс

class Program
{
    static void Main(string[] args)
    {
        Service svc = new Service();

        for (int i = 0; i < 20; i++)
        {
            Console.WriteLine($"Current call {i}");
            new Task(() => Printer(svc, i)).Start();
        }

        Console.ReadKey();
    }

    private static void Printer(Service svc, int index)
    {
        OutputStr str = svc.MakeCalls();
        Console.WriteLine($@"
            start1:{str.IdStart1}, end1:{str.IdEnd1}, 
            start2:{str.IdStart2}, end2:{str.IdEnd2}");
    }
}

РЕДАКТИРОВАТЬ: решено с помощью этой команды в базе данных

SELECT is_read_committed_snapshot_on, snapshot_isolation_state_desc,snapshot_isolation_state 
FROM sys.databases WHERE name='db'

ALTER DATABASE db SET allow_snapshot_isolation ON
ALTER DATABASE db SET SINGLE_USER WITH ROLLBACK IMMEDIATE
ALTER DATABASE db SET read_committed_snapshot ON
ALTER DATABASE db SET MULTI_USER

SELECT is_read_committed_snapshot_on, snapshot_isolation_state_desc,snapshot_isolation_state 
FROM sys.databases WHERE name='db'

и изменение кода на

_transaction = connection.BeginTransaction(IsolationLevel.Snapshot);

1 Ответ

0 голосов
/ 20 декабря 2018
My intention is the following:
 -in the same transaction: do a select on the table after an insert and I expect to get the value inserted in the prevous command on the same transaction.
 -in commands outside this transaction: i expect that a select reads the actual committed values ignoring, the uncommitted values

Затем вы должны использовать уровень изоляции READ_COMMITTED (по умолчанию) в базе данных с включенной опцией READ_COMMITTED_SNAPSHOT.например,

alter database current set read_committed_snapshot on

См. Изоляция моментального снимка в SQL Server

...