Уже немного поздно для ответа :) но надеюсь, что это будет полезно для других.
Ответ состоит из трех частей:
- Что означает «контекст транзакции, используемый другим сеансом».
- Как воспроизвести ошибку "Контекст транзакции используется другим сеансом."
1. Что означает «контекст транзакции, используемый другим сеансом».
Важное замечание: блокировка контекста транзакции получается непосредственно перед и снимается сразу после взаимодействия между SqlConnection
и SQL Server.
Когда вы выполняете какой-либо SQL-запрос, SqlConnection
"выглядит", есть ли транзакция, обертывающая его. Это может быть SqlTransaction
(«родной» для SqlConnection) или Transaction
из System.Transactions
сборки.
Когда транзакция найдена, SqlConnection
использует ее для связи с SQL Server, и в данный момент они общаются Transaction
, контекст исключительно заблокирован.
Что значит TransactionScope
? Он создает Transaction
и предоставляет информацию о компонентах .NET Framework, так что каждый, включая SqlConnection, может (и по своему замыслу) использовать его.
Итак, объявив TransactionScope
, мы создаем новую транзакцию, которая доступна для всех "транзакционных" объектов, созданных в текущем Thread
.
Описанная ошибка означает следующее:
- Мы создали несколько
SqlConnections
под одним и тем же TransactionContext
(что означает, что они относятся к одной и той же транзакции)
- Мы попросили эти
SqlConnection
обмениваться данными с SQL Server одновременно
- Один из них заблокировал текущий
Transaction
контекст, а следующий выбросил ошибку
2. Как воспроизвести ошибку «Контекст транзакции используется другим сеансом».
Прежде всего, контекст транзакции используется («заблокирован») прямо во время выполнения команды sql. Так что точно воспроизвести такое поведение сложно.
Но мы можем попытаться сделать это, запустив несколько потоков, выполняющих относительно длинные операции SQL в рамках одной транзакции.
Подготовим таблицу [dbo].[Persons]
в [tests]
База данных:
USE [tests]
GO
DROP TABLE [dbo].[Persons]
GO
CREATE TABLE [dbo].[Persons](
[Id] [bigint] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[Name] [nvarchar](1024) NOT NULL,
[Nick] [nvarchar](1024) NOT NULL,
[Email] [nvarchar](1024) NOT NULL)
GO
DECLARE @Counter INT
SET @Counter = 500
WHILE (@Counter > 0) BEGIN
INSERT [dbo].[Persons] ([Name], [Nick], [Email])
VALUES ('Sheev Palpatine', 'DarthSidious', 'spalpatine@galaxyempire.gov')
SET @Counter = @Counter - 1
END
GO
И воспроизвести «Контекст транзакции, используемый другим сеансом». ошибка с кодом C # на примере кода Shrike
using System;
using System.Collections.Generic;
using System.Threading;
using System.Transactions;
using System.Data.SqlClient;
namespace SO.SQL.Transactions
{
public static class TxContextInUseRepro
{
const int Iterations = 100;
const int ThreadCount = 10;
const int MaxThreadSleep = 50;
const string ConnectionString = "Initial Catalog=tests;Data Source=.;" +
"User ID=testUser;PWD=Qwerty12;";
static readonly Random Rnd = new Random();
public static void Main()
{
var txOptions = new TransactionOptions();
txOptions.IsolationLevel = IsolationLevel.ReadCommitted;
using (var ctx = new TransactionScope(
TransactionScopeOption.Required, txOptions))
{
var current = Transaction.Current;
DependentTransaction dtx = current.DependentClone(
DependentCloneOption.BlockCommitUntilComplete);
for (int i = 0; i < Iterations; i++)
{
// make the transaction distributed
using (SqlConnection con1 = new SqlConnection(ConnectionString))
using (SqlConnection con2 = new SqlConnection(ConnectionString))
{
con1.Open();
con2.Open();
}
var threads = new List<Thread>();
for (int j = 0; j < ThreadCount; j++)
{
Thread t1 = new Thread(o => WorkCallback(dtx));
threads.Add(t1);
t1.Start();
}
for (int j = 0; j < ThreadCount; j++)
threads[j].Join();
}
dtx.Complete();
ctx.Complete();
}
}
private static void WorkCallback(DependentTransaction dtx)
{
using (var txScope1 = new TransactionScope(dtx))
{
using (SqlConnection con2 = new SqlConnection(ConnectionString))
{
Thread.Sleep(Rnd.Next(MaxThreadSleep));
con2.Open();
using (var cmd = new SqlCommand("SELECT * FROM [dbo].[Persons]", con2))
using (cmd.ExecuteReader()) { } // simply recieve data
}
txScope1.Complete();
}
}
}
}
И в заключение несколько слов о реализации поддержки транзакций в вашем приложении:
- Избегайте многопоточных операций с данными, если это возможно (независимо от загрузки или сохранения). Например. сохранять
SELECT
/ UPDATE
/ etc ... запросы в одной очереди и обслуживать их с помощью однопотокового рабочего;
- В многопоточных приложениях используются транзакции. Всегда. Везде. Даже для чтения;
- Не делить одну транзакцию между несколькими потоками. Это вызывает странные, неочевидные, трансцендентные и невоспроизводимые сообщения об ошибках:
- «Контекст транзакции используется другим сеансом.»: Несколько одновременных взаимодействий с сервером в рамках одной транзакции;
- «Время ожидания истекло. Период ожидания истек до завершения операции или сервер не отвечает.»: Независящие транзакции были завершены;
- "Сделка под вопросом.";
- ... и я предполагаю много других ...
- Не забудьте установить уровень изоляции для
TransactionScope
. По умолчанию Serializable
, но в большинстве случаев достаточно ReadCommitted
;
- Не забудьте завершить ()
TransactionScope
и DependentTransaction