views:

1719

answers:

5

I have the following situation:

If have a MySQL db with a InnoDB table which I use to store unique numbers. I start a transaction, read the value (eg. 1000471), store this value in another table and update the incremented value (100472). Now I want to avoid that somebody else even reads the value while my transaction is running.

If I would use plain MySQL I would do something like this:

Exceute("LOCK tbl1 READ");
Execute("SELECT ... from tbl1");
Execute("INSERT into tbl2");
Execute("UNLOCK TABLES");

but since I am using SubSonic as a DAL and the code should be independent from mysql, I have to use the TransactionScope.

My code:

        TransactionOptions TransOpt = new TransactionOptions();
        TransOpt.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;
        TransOpt.Timeout = new TimeSpan(0, 2, 0);

        using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew, TransOpt))
        {

             // Select Row from tbl1

             // Do something

             ts.Complete();
        }

According to the help of TransactionOptions

system.transactions.isolationlevel

The effect I want to reach could be implemented with IsolationLevel.ReadCommitted, but I can still read the row from outside the transaction (If I try to change it, I get a lock, so the transaction is working)

Does anybody has a suggestion? Is a read lock even possible with TransactionScope

+1  A: 

My first guess was to use SELECT FOR UPDATE and after a quick search I found a page about locking reads in the MySQL 5 reference.

If I understand correctly this is independent from the isolation level used. And take care - the isolation level just tells how the current transaction is affected by changes in other transactions. It does not tell what other transactions can do. However, more restricting locks are required for higher isolation levels.

rudolfson
As I mentioned I use SubSonic for persistance.<br><br>I could run plain SQL against my MySQL DB but that would break the multi database concept of subsonic.thx for the link, but I still haven't found a solution to lock a row for reading with mysqlthis is worth reading, too:http://dev.mysql.com/doc/refman/5.1/en/innodb-lock-modes.htmlI discovered, if I do not set the IsolationLevel I get a"Deadlock found when trying to get lock; try restarting transaction" - MySqlException which seems to be a good thing (see my answer)
SchlaWiener
I read the link you gave and in my opinion, SELECT FOR UPDATE is the correct way to explicitly lock one row. I don't know SubSonic (guess it's an ORM tool / persistence framework) - did you check its documentation if it supports specifying a lock mode when reading entities? I know this possibility from other ORM tools, e.g. Hibernate (Java). If I tell Hibernate to use e.g. LockMode.UPGRADE, this would result in a SELECT FOR UPDATE.
rudolfson
You're right. SELECT FOR UPDATE is the right syntax in plain SQL.The problem seems to be that, using the TransactionScope approach, the TransactionOption IsolationLevel can only control which data can be read (old, uncommited, new) from the current transaction and what happens with writes if data has changed (as you mentioned in your post) but cannot invoke a UPDATE lock. Look what I found meanwile: http://stackoverflow.com/questions/190666/linq-to-sql-and-concurrency-issues/190690
SchlaWiener
A: 

As I didn't find a way to lock a row for reading with InnoDB and TransactionScope (I may be wrong) this should work:

If I run two Transactions simultaniously (without TransactionOptions) and one finishes, the other cannot compleate because of a "Deadlock" Exception.

Instead of avoiding this exception it seems to be best practice, according to the MySQL documentation, to expect a deadlock and restart the transaction.

if you set:

    TransactionOptions TransOpt = new TransactionOptions();
    TransOpt.IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted;

for your transaction you don't get the deadlock exception, but, in my case, this would result in a dublicate unique number, which is worse.

SchlaWiener
A: 

If anyone is interested, this is how TransactionOptions affect MySql:

Lets say I have two methods.

Method1 starts a transaction, selects a row from my table, increments the value and updates the table.

Method2 is the same, but between select and update I added a sleep of 1000ms.

Now imagine I have the following code:

    Private Sub Button1_Click(sender as Object, e as System.EventArgs) Handles Button1.Click

        Dim thread1 As New Threading.Thread(AddressOf Method1)
        Dim thread2 As New Threading.Thread(AddressOf Method2)

        thread2.Start() // I start thread 2 first, because this one sleeps
        thread1.Start()

    End Sub

Without transactions this would happen:
thread2 starts, reads the value 5, then sleeps,
thread1 starts, reads the value 5, updates the value to 6,
thread2 updates the value to 6, too.

Effect: I have the unique number two times.

What I want:
thread2 starts, reads the value 5, then sleeps,
thread1 starts, trys to reads the value, but get a lock and sleeps,
thread2 updates the value to 6,
thread1 continues, reads the value 6, updates the value to 7

That's how to start transaction with the TransactionScope:

        TransactionOptions Opts = new TransactionOptions();
        Opts.IsolationLevel = IsolationLevel.ReadUncommitted;

        // start Transaction
        using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew, Opts))
        {
            // Do your work and call complete
            ts.Complete();
        }

That can even manage distributed transactions. If an Exception is thrown ts.Complete is never called and the Dispose() Part of the Scope rolls back the transaction.

Here's an overview how the different IsolationLevels affect the transaction:

  • IsolationLevel.Chaos
    Throws a NotSupportedException - Chaos isolation level is not supported

  • IsolationLevel.ReadCommited
    The transactions do not interfere each other (two identical reads, bad)

  • IsolationLevel.ReadUncommitted
    The transactions do not interfere each other (two identical reads, bad)

  • IsolationLevel.RepeatableRead
    The transactions do not interfere each other (two identical reads, bad)

  • IsolationLevel.Serializable
    Throws a MySqlException - Deadlock found when trying to get lock; try restarting transaction during Update

  • IsolationLevel.Snapshot
    Throws a MySqlException - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '' at line 1 during Connection.Open()

  • IsolationLevel.Unspecified
    Throws a MySqlException - Deadlock found when trying to get lock; try restarting transaction during Update

  • TransactionOptions not set
    Throws a MySqlException - Deadlock found when trying to get lock; try restarting transaction during Update

SchlaWiener
A: 

Since SubSonic doesn't seem to support SELECT ... FOR UPDATE and using isolation levels would be a misuse in my opinion - what about having a user defined function, which returns a new id? This function would read the current value from tbl1 with a SELECT ... FOR UPDATE, udpate the row and return the value.

In your application code where you insert a new value you would just use:

insert into tbl2 (id, ....) values (next_id(), ...)
rudolfson
Would be a possible solution, but in my case the number is not just a number, but a string value that i programatically combine with a mask-string to determine the new value (e.g. the current number 200905123 and the mask YYYYMM### would result in a new number 200906001 if executed today) and I don't want to pull that logic out of my app.
SchlaWiener
A: 

Ok, I decided to use the "backdoor" and now use a inline query:

        // start transaction
        using (TransactionScope ts = new TransactionScope(TransactionScopeOption.RequiresNew))
        {
            using (SharedDbConnectionScope scope = new SharedDbConnectionScope(DB._provider))
            {

                try
                {

                    Record r = new InlineQuery().ExecuteAsCollection<RecordCollection>(
                        String.Format("SELECT * FROM {0} WHERE {1} = ?param FOR UPDATE",
                                        Record.Schema.TableName,
                                        Record.Columns.Column1), "VALUE")[0];

                    // do something

                    r.Save();
                 }
             }
         }

I started 10 threads simultaniously and it works as expected. Thx to rudolfson for the "SELECT FOR UPDATE" hint.

SchlaWiener
I forgot to mention: that piece of code requires this patch: http://code.google.com/p/subsonicproject/issues/detail?id=96 (or modify it to not use the parameter)
SchlaWiener