Для доступа к данным я использую 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?
TransactionScopeOption.RequiresNew
вокруг тела операции доступа к данным, а комментарий к коду все еще задавался вопросом, почему код строго не может работать без него. - person Jason Kleban   schedule 15.11.2016