Вызов MSDTC. Но почему?

Для доступа к данным я использую TransactionScopes на уровне API, чтобы обернуть целые операции в одну транзакцию, чтобы мои операции SQL можно было в некоторой степени компоновать. У меня есть веб-проект, в котором размещен API и отдельная сервисная библиотека, которая представляет собой реализацию и вызовы SQL. В начале операции (точка входа API) я открываю TransactionScope. Всякий раз, когда SqlConnection требуется в рамках обработки операции, запрашивайте AmbientConnection вместо того, чтобы напрямую устанавливать новое соединение. AmbientConnection находит или создает новый SqlConnection для текущей транзакции. Это должно обеспечить хорошую компоновку, но также избежать вызова MSDTC, поскольку он должен продолжать использовать одно и то же соединение для каждой подоперации внутри транзакции. Когда транзакция завершена (с scope.complete()), соединение автоматически закрывается.

Проблема в том, что время от времени MSDTC все еще вызывается, и я не могу понять, почему. Я успешно использовал это раньше, и я считаю, что у меня никогда не вызывался MSDTC. На этот раз две вещи кажутся мне разными: 1) я использую SQL Server 2008 R1 (10.50.4000) — не мой выбор — и я знаю, что поведение MSDTC изменилось, начиная с этой версии, и, возможно, не все перегибы были отработаны до более поздних версий. 2) Использование async-await является новым, и я считаю, что мне придется использовать TransactionScopeAsyncFlowOption.Enabled для размещения этой новой функции на случай, если какая-то часть реализации будет асинхронной. Возможно, необходимы дополнительные меры.

Я попробовал Pooling=false в строке подключения на тот случай, если MSDTC вызывается из-за того, что два независимых логических соединения ошибочно обрабатываются в рамках одного объединенного соединения. Но это не сработало.

Операция API

// Exposed API composing multiple low-level operations within a single TransactionScope
// independent of any database platform specifics.
[HttpPost]
public async Task<IHttpActionResult> GetMeTheTwoThings()
{
    using (var scope = new TransactionScope(TransactionScopeOption.Required, TransactionScopeAsyncFlowOption.Enabled))
    {
        var result = new TwoThings(
            await serviceLayer.GetThingOne(),
            await serviceLayer.GetThingTwo());

        scope.Complete();

        return Ok(result);
    }
}

Реализация сервисного уровня

public async Task<ThingOne> GetThingOne()
{
    using (var cmd = connManagement.AmbientConnection.CreateCommand())
    {
        cmd.CommandType = System.Data.CommandType.StoredProcedure;
        cmd.CommandText = "dbo.GetThingOne";

        return (ThingOne)(await cmd.ExecuteScalarAsync());
    }
}

public async Task<ThingTwo> GetThingTwo()
{
    using (var cmd = connManagement.AmbientConnection.CreateCommand())
    {
        cmd.CommandType = System.Data.CommandType.StoredProcedure;
        cmd.CommandText = "dbo.GetThingTwo";

        return (ThingTwo)(await cmd.ExecuteScalarAsync());
    }
}

Реализация AmbientConnection

internal class SQLConnManagement
{
    readonly string connStr;
    readonly ConcurrentDictionary<Transaction, SqlConnection> txConnections = new ConcurrentDictionary<Transaction, SqlConnection>();

    private SqlConnection CreateConnection(Transaction tx)
    {
        var conn = new SqlConnection(this.connStr);

        // When the transaction completes, close the connection as well
        tx.TransactionCompleted += (s, e) =>
        {
            SqlConnection closing_conn;
            if (txConnections.TryRemove(e.Transaction, out closing_conn))
            {
                closing_conn.Dispose(); // closing_conn == conn
            }
        };

        conn.Open();

        return conn;
    }

    internal SqlConnection AmbientConnection
    {
        get
        {
            var txCurrent = Transaction.Current;

            if (txCurrent == null) throw new InvalidOperationException("An ambient transaction is required.");

            return txConnections.GetOrAdd(txCurrent, CreateConnection);
        }
    }

    public SQLConnManagement(string connStr)
    {
        this.connStr = connStr;
    }
}

Не хочу усложнять пост, но это может иметь значение, потому что мне кажется, что каждый раз, когда MSDTC вызывается, зарегистрированная трассировка стека показывает, что был задействован этот следующий механизм. Определенные данные я кэширую с помощью встроенного ObjetCache, потому что они не меняются часто, и поэтому я просто получаю их не чаще одного раза в минуту или что-то еще. Это немного причудливо, но я не понимаю, почему ленивый генератор будет обрабатываться иначе, чем более типичный вызов, и почему это может привести к тому, что иногда будет вызываться MSSDTC. Я тоже пробовал LazyThreadSafetyMode.ExecutionAndPublication на всякий случай, но это все равно не помогает (и тогда исключение просто продолжает доставляться как кешированный результат для последующих запросов до истечения срока действия, конечно, и это нежелательно).

/// <summary>
/// Cache element that gets the item by key, or if it is missing, creates, caches, and returns the item
/// </summary>
static T CacheGetWithGenerate<T>(ObjectCache cache, string key, Func<T> generator, DateTimeOffset offset) where T : class
{
    var generatorWrapped = new Lazy<T>(generator, System.Threading.LazyThreadSafetyMode.PublicationOnly);

    return ((Lazy<T>)cache.AddOrGetExisting(
        key,
        generatorWrapped,
        offset))?.Value ?? generatorWrapped.Value;
}

public ThingTwo CachedThingTwo
{
    get
    {
        return CacheGetWithGenerate(
            MemoryCache.Default,
            "Services.ThingTwoData",
            () => GetThingTwo(), // ok, GetThingTwo isn't async this time, fudged example
            DateTime.Now.Add(TimeSpan.FromMinutes(1)));
    }
}

Знаете ли вы, почему вызывается MSDTC?


person Jason Kleban    schedule 24.07.2015    source источник
comment
Просто для здравомыслия, используете ли вы более одного сервера базы данных или ссылаетесь на один и тот же сервер более чем одним способом, то есть (localhost - SERVERNAME - 192.168.2.120) более чем в одной строке подключения? Кроме того, не могли бы вы ссылаться на два узла в одном и том же кластере (когда вы переходите между узлами в кластере, у вас нет гарантии, на каком узле закончится соединение).   -  person Ross Bush    schedule 24.07.2015
comment
Хорошая мысль, но это некластеризованный один экземпляр и всего одна версия строки подключения.   -  person Jason Kleban    schedule 27.07.2015
comment
@JasonKleban Ты когда-нибудь это понимал? Я обнаружил, что транзакция эскалируется при использовании async/await, и мне интересно, связано ли это вообще с этим.   -  person ajbeaven    schedule 12.11.2016
comment
Я не понял, почему. В итоге я использовал TransactionScopeOption.RequiresNew вокруг тела операции доступа к данным, а комментарий к коду все еще задавался вопросом, почему код строго не может работать без него.   -  person Jason Kleban    schedule 15.11.2016


Ответы (1)


PublicationOnly означает, что можно создать два соединения и одно удалить. Я удивлен, что вы сделали эту ошибку, потому что вы явно указали PublicationOnly (в отличие от безопасного режима по умолчанию, который является безопасным). Вы явно разрешили эту ошибку.

Я почему-то не увидел, что вы уже пробовали ExecutionAndPublication. Поскольку неиспользование является ошибкой, исправьте код в вопросе.

CreateConnection также не работает в том смысле, что в случае исключения при открытии объект подключения не удаляется. Возможно, безобидно, но как знать.

Кроме того, проверьте этот код на прерывание потока, которое может произойти, когда ASP.NET истечет время ожидания запроса. Вы делаете здесь очень опасные и хрупкие вещи.

Шаблон, который я использую, заключается в использовании контейнера IOC для внедрения соединения, которое является общим для всего запроса. Первый клиент для этого соединения открывает его. Событие завершения запроса закрывает его. Просто и избавляет от всех этих неприятных общих, изменяемых, многопоточных состояний.

Почему вы используете кеш для данных, которые не хотите потерять? Вероятно, это ошибка. Не делай этого.

О чем ?.Value ?? generatorWrapped.Value? Словарь никогда не может возвращать значение null. Удалите этот код. Если он может вернуть null, то принудительное отложенное значение создаст второе соединение, так что это тоже логическая ошибка.

person usr    schedule 24.07.2015
comment
Относительно (...)?.Value ?? generatorWrapped.Value: обратите внимание, что cache не является словарем. Если в кэше нет записи кэша, ключ которой соответствует параметру ключа, создается новая запись кэша, и перегрузка метода MemoryCache.AddOrGetExisting возвращает значение null. Если соответствующая запись кэша существует, возвращается существующая запись.. Эта строка берет существующую запись, объединенную с вновь созданной ленивой записью, и оценивает ее. Lazy может создаваться несколько раз, но оцениваться будет только успешно закешированное создание. - person Jason Kleban; 27.07.2015
comment
Этот ленивый конструктор дешев и не может не дать сбой. Затем PublicationOnly означает, что почти одновременные начальные вызовы кэшированного отложенного выполнения могут выполнять GetThingTwo() больше, чем необходимо, но только с один успешный результат возвращается как значение. Я считаю, что любой избыточный вызов выполняется в контексте однорангового вызывающего абонента, сохраняющего этот Transaction.Current, как обычно, и не будет особенно вызывать ситуацию MSDTC. И множественные оценки здесь лучше, чем кешированные исключения (пока я не избавлюсь от несогласованных исключений, подобных этому MSDTC). - person Jason Kleban; 27.07.2015
comment
Но спасибо за подробный анализ, есть над чем задуматься. Я рассмотрю ваш шаблон IoC, хотя для улучшений и хороших замечаний о conn.Open возможном сбое и возможных условиях ThreadAbort, хотя я пока не вижу тех, которые вызывают MSDTC. - person Jason Kleban; 27.07.2015
comment
PublicationOnly означает, что можно открыть два соединения. Ты согласен? Это немедленно вызывает участие MSDTC. - person usr; 27.07.2015
comment
Также обратите внимание на то, что MemoryCache разрешено выбрасывать ваши предметы (особенно при тайм-ауте в 1 минуту, который я только что заметил!). Это также означает, что можно создать два соединения. Ты согласен? - person usr; 27.07.2015
comment
(...)?.Ценность ?? генераторWrapped.Value: Хорошо, если это поведение MemoryCache, то ?.Value действительно может быть нулевым, и вы правы. Что за сумасшедший дизайн API?! В любом случае, вы не можете использовать MemoryCache, потому что он выбрасывает ваши данные. - person usr; 27.07.2015
comment
Только если две попытки выполнены с одной и той же транзакцией. Но здесь каждая попытка, хотя и почти одновременная, будет осуществляться в пределах их собственных независимых транзакций (каждой инкапсулирующей асинхронной транзакцией сохраняется TransactionScope) и, таким образом, не будет причиной для эскалации любой транзакции. Если это предположение неверно, я хочу понять его полностью! - person Jason Kleban; 27.07.2015
comment
Хорошо, если нет параллелизма для каждого ключа, то это нормально, но вам вообще не нужен ленивый. - person usr; 27.07.2015
comment
Кэшированные данные доступны только для чтения и редко изменяются. Так что это нормально, что такие данные выбрасываются — они будут заменены более свежими данными, которые в данном случае приемлемы. Например, параметры выпадающего меню, которые меняются, может быть, раз в квартал (но я не знаю, когда), но не оптимизированы для чтения по какой-либо причине в наборе данных. - person Jason Kleban; 27.07.2015
comment
Хорошо, я думаю, что немного смущен тем, что вы храните в этом кеше. - person usr; 27.07.2015
comment
Ленивый требуется. AddOrGetExisting принимает значение, а не генератор. Предоставленное значение, если не это ленивое, будет новым открытым соединением каждый раз, которое, возможно, будет отброшено. - person Jason Kleban; 27.07.2015
comment
Хорошо, теперь я перестану говорить чепуху. Я надеюсь, что это обсуждение помогло вам в чем-то. Просто игнорируйте запутанные части :) - person usr; 27.07.2015
comment
Я очень ценю обсуждение. Помогает исключить вещи по крайней мере! - person Jason Kleban; 27.07.2015