It's often important to handle scenarios where two or more clients may try to work with the same entities, concurrently. In Chapter 2, Models and Mappings, in section: Handling versioning and concurrency we discussed how to handle that using versioning and optimistic concurrency. A more aggressive approach is to use pessimistic concurrency, where specific rows in the database are explicitly locked for certain operations. Many DBMSes, such as SQL Server, has a good support for this and in this recipe we'll show how NHibernate can use that functionality.
Follow the Getting ready step in the Save entities to the database recipe in this chapter.
Since this recipe requires specific database support, you can't run it using the SQLite database, which is the default in NH4CookbookHelpers
. Reconfigure the recipe runner by setting the RecipeLoader.DefaultConfiguration
property or simply override the Configure method in the Recipe class to provide a database configuration specific to this recipe.
SessionLock
in the project.Recipe
to the folder:using System;
using System.Threading;
using NH4CookbookHelpers.Queries;
using NH4CookbookHelpers.Queries.Model;
using NHibernate;
namespace SessionRecipes.SessionLock
{
public class Recipe : QueryRecipe
{
protected override void Run(ISessionFactory
sessionFactory)
{
ExecuteWithLockMode(sessionFactory,
LockMode.None,
LockMode.None);
ExecuteWithLockMode(sessionFactory,
LockMode.Upgrade,
LockMode.Upgrade);
ExecuteWithLockMode(sessionFactory,
LockMode.UpgradeNoWait,
LockMode.UpgradeNoWait);
}
private void ExecuteWithLockMode(ISessionFactory
sessionFactory, LockMode lockMode1,
LockMode lockMode2)
{
Console.WriteLine("Executing with {0} and {1}",
lockMode1, lockMode2);
Console.WriteLine();
var thread1 = new Thread(() =>
GetAndChangeProductInLock(sessionFactory,
lockMode1, 3000)) {
Name = "Thread1"
};
var thread2 = new Thread(() =>
GetAndChangeProductInLock(sessionFactory,
lockMode2, 0)) {
Name = "Thread2"
};
thread1.Start();
Thread.Sleep(300);
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine();
}
private void GetAndChangeProductInLock(ISessionFactory
sessionFactory, LockMode lockMode, int sleepTime)
{
try
{
using (var session = sessionFactory.OpenSession())
{
using (var tx = session.BeginTransaction())
{
var product = session.Get<Product>(1);
Console.WriteLine("{0} acquiring lock",
Thread.CurrentThread.Name);
session.Lock(product, lockMode);
Console.WriteLine("{0} acquired lock",
Thread.CurrentThread.Name);
product.Description =
string.Format("Updated in LockMode.{0}",
lockMode);
Thread.Sleep(sleepTime);
Console.WriteLine("{0} committing",
Thread.CurrentThread.Name);
tx.Commit();
Console.WriteLine("{0} committed",
Thread.CurrentThread.Name);
}
}
}
catch (Exception ex)
{
Console.WriteLine("Exception in {0}:{1}",
Thread.CurrentThread.Name, ex.Message);
}
}
}
}
SessionLock
recipe. You should be able to see this output:Session.Lock
uses database specific methods to acquire locks on specific rows, in order to prevent concurrent access to the same entities from multiple clients. In our recipe, we try to simulate this scenario, using two separate threads executing almost simultaneously. The first thread will sleep for three seconds before it commits its transaction, and the second thread will have to act accordingly. How it acts depends on the LockMode
used.
This is the default lock mode. No specific lock is required. However, if the database becomes involved (the object was not found in any cache), a read lock may be acquired.
This is a shared lock, which will be acquired implicitly if the transaction's serialization level is RepeatableRead
or serializable.
When this lock mode is used, NHibernate will issue a SELECT WITH (updlock,rowlock)
query. This query will not return anything until any previous locks have been released. This can be seen in the recipe output, where the second thread doesn't acquire its lock until the first thread has committed its transaction:
Thread1 acquiring lock
Thread1 acquired lock
Thread2 acquiring lock
Thread1 committed
Thread2 acquired lock
Thread2 committed
If the transaction owning the lock takes a lot of time to complete, a timeout exception may be thrown in the transaction which tries to acquire the new lock.
This lock mode, which requires specific database support, behaves similarly to Upgrade. However, instead of waiting for a previous lock to be released, it will fail immediately if the lock couldn't be acquired.
Thread1 acquiring lock
Thread1 acquired lock
Thread2 acquiring lock
Exception in Thread2:could not lock:[NH4CookbookHelpers.Queries.Model.Movie#1][SQL: SELECT Id FROM Product with (updlock, rowlock, nowait) WHERE Id = ?] Thread1 committed
3.139.97.202