How do distributed transactions behave with multiple connections to the same DB in a threaded environment? How do distributed transactions behave with multiple connections to the same DB in a threaded environment? multithreading multithreading

How do distributed transactions behave with multiple connections to the same DB in a threaded environment?


First, you have to separte what you read here and there about SQL Server transactions into 2 distinct cases: local and distributed.

Local SQL transactions:

  • SQL Server allows only one request to execute on each local transaction.
  • By default only one session can enroll in a local transaction. Using sp_getbindtoken and sp_bindsession multiple sessions can be enrolled in a local transaction. The sessions are still restricted to only one executing a request at any time.
  • With Multiple Active Result Sets (MARS) one sessions can execute multiple requests. All requests have to be enrolled in the same local transaction.

Distributed Transactions:

  • Multiple sessions can have their local transaction enrolled in a single distributed transaction.
  • Each session is still enroled in a local transaction, subject to all restrictions mentioned above for local transactions
  • Local transactions enroled in a distributed transaction are subject to two phase commit coordinated by the distributed transaction
  • All local transactions on an instance enrolled in a distributed transaction are still independent local transactions, primarily meaning they have conflicting lock namespaces.

So when a client creates a .Net TransactionScope and under this transaction scope it executes multiple requests on the same server, these requests are all local transactions enrolled in a distributed transaction. A simple example:

class Program    {        static string sqlBatch = @"set nocount on;declare @i int;set @i = 0;while @i < 100000begin    insert into test (a) values (replicate('a',100));    set @i = @i+1;end";        static void Main(string[] args)        {            try            {                TransactionOptions to = new TransactionOptions();                to.IsolationLevel = IsolationLevel.ReadCommitted;                using (TransactionScope scp = new TransactionScope(TransactionScopeOption.Required, to))                {                    using (SqlConnection connA = new SqlConnection(Settings.Default.connString))                    {                        connA.Open();                        using (SqlConnection connB = new SqlConnection(Settings.Default.connString))                        {                            connB.Open();                            SqlCommand cmdA = new SqlCommand(sqlBatch, connA);                            SqlCommand cmdB = new SqlCommand(sqlBatch, connB);                            IAsyncResult arA = cmdA.BeginExecuteNonQuery();                            IAsyncResult arB = cmdB.BeginExecuteNonQuery();                            WaitHandle.WaitAll(new WaitHandle[] { arA.AsyncWaitHandle, arB.AsyncWaitHandle });                            cmdA.EndExecuteNonQuery(arA);                            cmdB.EndExecuteNonQuery(arB);                        }                    }                    scp.Complete();                }            }            catch (Exception e)            {                Console.Error.Write(e);            }        }    }

Create a dummy test table:

create table test (id int not null identity(1,1) primary key, a varchar(100));

and run the code in my sample. You will see that both requests are executing in parallel, each one isnerting 100k rows in the table, then both commit when the transaction scope is complete. So the problems you're seeing are no related to SQL Server nor to TransactionScope, they can easily handle the scenario you describe. More, the code is very simple and straight forward and there isn't any need for dependent transactions to be created, cloning to occur nor transactions to be promotted.

Updated

Using explicit threads and dependent transactions:

 private class ThreadState    {        public DependentTransaction Transaction {get; set;}        public EventWaitHandle Done {get; set;}        public SqlConnection Connection { get; set; }    }    static void Main(string[] args)    {        try        {            TransactionOptions to = new TransactionOptions();            to.IsolationLevel = IsolationLevel.ReadCommitted;            using (TransactionScope scp = new TransactionScope(TransactionScopeOption.Required, to))            {                ThreadState stateA = new ThreadState                 {                    Transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete),                    Done = new AutoResetEvent(false),                    Connection = new SqlConnection(Settings.Default.connString),                };                stateA.Connection.Open();                ThreadState stateB = new ThreadState                {                    Transaction = Transaction.Current.DependentClone(DependentCloneOption.BlockCommitUntilComplete),                    Done = new AutoResetEvent(false),                    Connection = new SqlConnection(Settings.Default.connString),                };                stateB.Connection.Open();                ThreadPool.QueueUserWorkItem(new WaitCallback(Worker), stateA);                ThreadPool.QueueUserWorkItem(new WaitCallback(Worker), stateB);                WaitHandle.WaitAll(new WaitHandle[] { stateA.Done, stateB.Done });                scp.Complete();                //TODO: dispose the open connections            }        }        catch (Exception e)        {            Console.Error.Write(e);        }    }    private static void Worker(object args)    {        Debug.Assert(args is ThreadState);        ThreadState state = (ThreadState) args;        try        {            using (TransactionScope scp = new TransactionScope(state.Transaction))            {                SqlCommand cmd = new SqlCommand(sqlBatch, state.Connection);                cmd.ExecuteNonQuery();                scp.Complete();            }            state.Transaction.Complete();        }        catch (Exception e)        {            Console.Error.WriteLine(e);            state.Transaction.Rollback();        }        finally        {            state.Done.Set();        }    }