Concurrency

Transactions offer the functionality to group multiple work units into one big unit of work, but you can further configure Hibernate to achieve better isolation in a concurrent environment. What defines concurrency parameters is transaction isolation levels as well as locking mechanisms. These concepts also require that you learn the capabilities and the behavior of the specific RDBMS that you are using because both of them are implemented by the database systems and are not Hibernate features. For this reason, you need to carefully explore the various options with your specific database system. Here, we will discuss them to show you how to control them through Hibernate.

Lastly, when dealing with concurrency issues, you'll also need to keep in mind the first-level and second-level cache, as they can play an important role in identifying problems. We discussed this topic in Chapter 5, Hibernate Cache.

Isolation levels

Transactions let you isolate your unit of work from someone else's work. The isolation levels allow you to draw a line between the different work units. The higher the level, the more strict the data access. Isolation is one of the characteristics of a database transaction. So, this concept only applies to one transaction while another transaction is in progress. They are listed here from the highest level to the lowest:

  • Serializable: This is the highest isolation level. It means that, when you start a transaction, other transactions are not allowed to access the data you are accessing until the first transaction is committed or rolled back. And, if two threads try to update the same record, the first thread will succeed and the second thread will encounter an error. This provides the most privileged access to the data, but at the same time, it performs very poorly and can cause a lot of performance headaches.
  • Repeatable reads: This means that, if you read (SELECT) data from a table and later within that same transaction you read the same data again, you are guaranteed to get the same data. While this transaction is going on, other transactions can read, but they are prohibited to write until the first transaction is completed. This performs slightly better, but it's still not good for a highly transactional system.
  • Read committed: This is slightly better than the previous two levels. You can read as many times as you want, but it is not guaranteed that you will get the same data. This is because, between your reads, other transactions may change and commit the data. The default isolation level for most database systems is read committed.
  • Read uncommitted: This is also known as dirty read, which lets you read the changes before they are committed. However, you run into the risk of reading data that may be rolled back by another transaction. On the other hand, its performance is the best because it is the least restrictive isolation.

You can set the isolation level on the JDBC connection. Hibernate lets you do this in the configuration file by setting the hibernate.connection.isolation property to the integer value defined by java.sql.Connection, where the value 1 is READ_UNCOMMITTED, value 2 is READ_COMMITTED, value 4 is REPEATABLE_READ, and value 8 is SERIALIZABLE. Let's check the following code line:

<property name="hibernate.connection.isolation">2</property>

If you are using a connection pool outside Hibernate, you can also set the isolation level in your connection pool configuration. Once you set the transaction isolation level on your database connections, in your configuration file, you can still change it for a specific session, but you'll have to do this before starting a transaction. And after the transaction is completed, that is, committed or rolled back, you should always set the isolation back to the default value, since the connection typically belongs to a pool and is reused by other threads (the connection cleanup may do this for you, but it's good practice). In this case, we use the same technique that we used earlier to access the Connection object, as shown here:

Session session = HibernateUtil.getSessionFactory().openSession();
session.doWork(new Work() {
  @Override
  public void execute(Connection connection) throws SQLException {
    connection.setTransactionIsolation(8);
  }
});

Transaction transaction = session.beginTransaction();
try {
  // do work
  
  transaction.commit();
} catch (Exception e) {
  transaction.rollback();
  // log exception
} finally {
  if (session.isOpen()) {
    session.doWork(new Work() {
      @Override
      public void execute(Connection connection) throws SQLException {
      connection.setTransactionIsolation(2);
      }
    });
    session.close();
  }
}

In this case, we set the isolation level to Serializable before we start the transaction. If two threads try to modify the same record, your database driver will throw an exception, which is similar to the following, for the thread that came later:

Caused by: org.postgresql.util.PSQLException: ERROR: could not serialize access due to concurrent update
  at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2161)
  at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:1890)
  at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:255)
  at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:560)
  at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:417)
  at org.postgresql.jdbc2.AbstractJdbc2Statement.executeUpdate(AbstractJdbc2Statement.java:363)
  …
  …

Locking

In reality, when you set the isolation level on a database connection, the database system does a good job managing concurrency. However, you can further instruct the database to do explicit locking using Hibernate to prevent data loss.

There are two types of locking strategies: optimistic and pessimistic:

  • Optimistic lock: This strategy assumes that the data hasn't changed since the transaction began, and when you are ready to commit changes, you can safely assume that you are not overriding someone else's change. This is the behavior of the Read Uncommitted isolation level.
  • Pessimistic lock: This strategy assumes that the data may change while the transaction is in progress. In order to prevent that, you can lock the record before you begin reading, modifying, and finally saving it. In other words, you want exclusive access to the data. The Serializable isolation level acts this way.

Hibernate lets you acquire stricter locks on an entity, which in turn translates to the SELECT FOR UPDATE and SELECT FOR SHARE statements. Most database systems support queries like this. Consider the following code:

Session session = HibernateUtil.getSessionFactory()
  .openSession();
Transaction transaction = session.beginTransaction();
try {
  Person person = (Person) session.get(Person.class, new Long(18));
  
  session.buildLockRequest(
       new LockOptions(LockMode.PESSIMISTIC_WRITE)).lock(person);

  person = randomChange(person);
  session.save(person);
  transaction.commit();
} catch (Exception e) {
  transaction.rollback();
  throw e;
} finally {
  if (session.isOpen())
    session.close();
}

In this case, two threads try to update the entity with the ID 18. Because we are requesting a pessimistic lock, Hibernate issues the following additional select query for each thread:

    select
        id 
    from
        Person 
    where
        id =? 
        and version =? for update

Note that the select statement is appended by for update. This tells the database that we are requesting a lock on that record. The version statement will be explained shortly. When both threads try to set the lock, the second one will get the following exception:

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect) : [com.packt.hibernate.model.Person#18]

If you wish to lock pessimistically, but still allow other threads to read the data while you hold a lock, you can use the PESSIMISTIC_READ lock mode. The PESSIMISTIC_READ lock mode produces the following SQL statement; in this case, the key phrase for share is added:

    select
        id 
    from
        Person 
    where
        id =? 
        and version =? for share

However, two threads may not request the pessimistic read lock at the same time. To test this, let's change the code block to the following:

if (firstThread) {
  session.buildLockRequest(
      new LockOptions(LockMode.PESSIMISTIC_READ)).lock(person);
}
else {
  session.buildLockRequest(
      new LockOptions(LockMode.PESSIMISTIC_READ)).lock(person);
}

person = randomChange(person);
session.save(person);
session.flush();

This will still encounter the same exception shown earlier. (What is not shown here is the orchestration code to ensure that the first thread is really the first thread that acquires the lock.) The reason you encounter the exception is because both threads are still trying to pessimistically lock the record while allowing other threads to read the record without a less restricted lock.

There are other lock modes that Hibernate uses implicitly, such as LockMode.READ and LockMode.WRITE; you should consult the JavaDoc for additional information.

Let's talk about version, which we saw earlier. In order for stricter control to be enforced, you need to add a version attribute to your entity and use the @Version annotation on a column that keeps track of the entity version. This is how Hibernate detects if a row was changed since you read it and are now attempting to update it. You shouldn't let your application code modify the version number. Hibernate lets you create a version column from any data type. This is a JPA annotation. For more information, refer to the JavaDoc in the JPA API.

Explicit locking may cause deadlocks. You can set a timeout on the lock to help alleviate this. You can also ensure that the locking strategy cascades to the associated entities or nonassociated entities that may be required later in the execution path. This is usually when deadlocks occur. One thread has a lock on the root entity and another has a lock on the associated or relevant entity. Thread 1 then needs to acquire the lock on the associated or relevant entity to complete the work, but thread 2 has already acquired the lock on those entities and is waiting for thread 1 to release the lock on the main entity. So they both wait on the other, hence deadlock.

The database server typically detects a deadlock, and when that happens, the driver will throw an exception. So, you just have to ensure that you are catching that exception and handling the error properly.

But there is another scenario that is not quite a deadlock but could impact the performance of your application. This is the case when Thread 1 holds a lock and Thread 2 waits for that lock, but Thread 1 does not release it because it's a long running transaction. Some people refer to this as contention. We have already warned you that you should keep the life of your sessions short and not implement any business logic in your data access layer, such as CPU-intensive computations. If you need access to the database in between your long running logic, consider using multiple sessions and use the pessimistic locking mechanism.

Finally, if you define the association to cascade locks, you'll be able to further avoid a deadlock. Also, if you know in advance that you may need another entity for your data logic, you should acquire the lock ahead of time before moving forward. If you don't know what relevant entity you may need later in your execution path, then you should use a short lock timeout. This is useful when the bottleneck is not caused by a deadlock, but is rather caused by a long running thread (in Java) that is holding the lock. By setting a timeout, you are protecting your thread from other misbehaving threads. The value of the timeout depends on the code block that you are in. Use your best judgment.

User lock

At times, some developers add an extra column to indicate someone else has locked a record. In other words, the application is managing the locking mechanism. This is generally not a good idea. But if done right, it can be effective. Typically, you would write the name of the application user in the lock column and the time at which it was locked. The way a record is locked is by executing an UPDATE statement like this:

UPDATE customer SET lockedby = 'user1', locktime=current_timestamp
WHERE id = 100 AND lockedby is null;

Then, we check the row count affected by this SQL and if it is 1, then this record is now locked by user1.

Here are some reasons why this technique is not recommended:

  • Race condition: This is the first pitfall; two threads execute the SQL statement and they are both able to acquire a lock. Even if you set the connection to auto-commit, it's still possible to encounter the race condition on some database systems. So, you most likely have to check the record again, in a new session, to ensure that you successfully acquired the lock. Some developers do this check when they are ready to release the lock. But, you can also tighten this using a pessimistic lock.
  • Stale locks: You may end up with records that are locked by a user and then never unlocked. So, you either have to have a mechanism to be able to break a lock when obtaining one—for example, allow the users to lock the record if the lock time is more than 30 minutes. Or, if your application has a dashboard, provide admin users with the ability to unlock a record.
  • Multiple applications: If your database is being updated by more than one application, this mechanism doesn't protect you from data loss. While you have your user lock on a record, another application may modify the record without caring if the data is locked or not. This is quite common, especially if you are modernizing a legacy application where both the modernized and the legacy application have to coexist in production, and it is most likely that you will be unable to modify the code in the legacy application. You can also work around this by checking the stored data once more before you write the changes and unlock the record.

As you can see, all this is extra work that you would have to think about carefully, and extra code and logic you would have to implement. Using user locks does bring some advantages to an enterprise application. So, if done right, it can be effective.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.89.2