【发布时间】:2015-07-24 16:52:56
【问题描述】:
对于我的数据访问,我在 API 级别使用 TransactionScopes 将整个操作包装在单个事务中,以便我的 SQL 操作可以在某种程度上是可组合的。我有一个托管 API 的 Web 项目和一个单独的服务库,该服务库是 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,以防它被调用,因为两个独立的逻辑连接在单个池连接下错误地处理。但这没有用。
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 缓存某些数据,因为它不会经常更改,所以我最多每分钟获取一次或其他什么。这有点花哨,但我不明白为什么 Lazy 生成器的处理方式与更典型的调用有任何不同,以及为什么这特别会导致 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 吗?
【问题讨论】:
-
为了理智,您是使用多个数据库服务器还是以多种方式引用同一服务器,即(localhost - SERVERNAME - 192.168.2.120)在多个连接字符串中?此外,您是否可以引用同一集群上的两个节点(当您在集群上的节点之间移动时,您无法保证连接最终会在哪个节点上)。
-
好主意,但它是非集群的单实例,只是连接字符串的一个版本。
-
@JasonKleban 你有没有想过这个问题?我发现使用 async/await 时事务升级了,我想知道它是否与此有关。
-
我没有弄清楚为什么。我最终在数据访问操作体周围使用了
TransactionScopeOption.RequiresNew,并且代码注释仍然想知道为什么没有它代码严格无法工作。
标签: c# sql sql-server-2008 async-await msdtc