Close

Spring Data JPA - Applying Pessimistic Locking with @Lock Annotation

[Last Updated: Nov 16, 2018]

In Spring Data, Optimistic Locking (last tutorial) is enabled by default given that @Version annotation is used in entities. To use other locking mechanism specified by JPA, Spring Data provides Lock annotation:

package org.springframework.data.jpa.repository;
 ...
import javax.persistence.LockModeType;

@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lock {
      LockModeType value();
}

This annotation is used on the repository methods with a desired LockModeType.

In following example we are going to use @Lock annotation to enable Pessimistic locking.

Example

Entity

@Entity
public class Article {
  @Id
  @GeneratedValue
  private Long id;
  private String content;
    .............
}
package com.logicbig.example;

import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;
import javax.persistence.LockModeType;

public interface ArticleRepository extends CrudRepository<Article, Long> {

  @Lock(LockModeType.PESSIMISTIC_WRITE)
  @Query("select a from Article a where a.id = :id")
  Article findArticleForWrite(@Param("id") Long id);

  @Lock(LockModeType.PESSIMISTIC_READ)
  @Query("select a from Article a where a.id = :id")
  Article findArticleForRead(@Param("id") Long id);
}

In above example we are using @Lock annotation to our query methods. We are also allowed to override methods from CrudRepository to apply this annotation.

Setting lock timeout

We are going to set javax.persistence.lock.timeout (milliseconds) in persistence.xml (we can also do that via spring config). If we don't set it then database specific default value of lock timeout will be used (in case of H2 database it is 1000 milliseconds).

The purpose of this parameter is to specify how long a persistence provider should wait to obtain a requested lock. If the time it takes to obtain a lock exceeds the value of this property, PessimisticLockException will be thrown.

src/main/resources/META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
             xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">

    <persistence-unit name="example-unit" transaction-type="RESOURCE_LOCAL">
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <properties>
            <property name="javax.persistence.lock.timeout" value="5000"/>
            <property name="javax.persistence.schema-generation.database.action" value="create"/>
            <property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=5000"/>
        </properties>
    </persistence-unit>

</persistence>

In case of H2 database, javax.persistence.lock.timeout does not seem to have effect, we also needed to set H2 specific property, LOCK_TIMEOUT.

Example client

In following example, each of the two threads act as two users. One thread obtains PESSIMISTIC_READ lock and other obtains PESSIMISTIC_WRITE lock. The read thread delays for a long time, so PessimisticLockException might be thrown in the write thread:

@Component
public class ExampleClient {
    @Autowired
    private ArticleRepository repo;
    @Autowired
    private Tasks tasks;

    public ExecutorService run() {
        //creating and persisting an Article
        Article article = new Article("test article");
        repo.save(article);

        ExecutorService es = Executors.newFixedThreadPool(2);

        //user 1, reader
        es.execute(tasks::runUser1Transaction);

        //user 2, writer
        es.execute(tasks::runUser2Transaction);

        return es;
    }

    @Service
    @Transactional
    public class Tasks {
        public void runUser1Transaction() {
            System.out.println(" -- user 1 reading Article entity --");
            long start = System.currentTimeMillis();
            Article article1 = null;
            try {
                article1 = repo.findArticleForRead(1L);
            } catch (Exception e) {
                System.err.println("User 1 got exception while acquiring the database lock:\n " + e);
                return;
            }
            System.out.println("user 1 got the lock, block time was: " + (System.currentTimeMillis() - start));
            //delay for 2 secs
            ThreadSleep(3000);
            System.out.println("User 1 read article: " + article1);
        }

        public void runUser2Transaction() {
            ThreadSleep(500);//let user1 acquire optimistic lock first
            System.out.println(" -- user 2 writing Article entity --");
            long start = System.currentTimeMillis();
            Article article2 = null;
            try {
                article2 = repo.findArticleForWrite(1L);
            } catch (Exception e) {
                System.err.println("User 2 got exception while acquiring the database lock:\n " + e);
                return;
            }
            System.out.println("user 2 got the lock, block time was: " + (System.currentTimeMillis() - start));
            article2.setContent("updated content by user 2.");
            repo.save(article2);
            System.out.println("User 2 updated article: " + article2);
        }

        private void ThreadSleep(long timeout) {
            try {
                Thread.sleep(timeout);
            } catch (InterruptedException e) {
                System.err.println(e);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(AppConfig.class);
        ExampleClient exampleClient = context.getBean(ExampleClient.class);
        ExecutorService es = exampleClient.run();
        es.shutdown();
        es.awaitTermination(5, TimeUnit.MINUTES);
        EntityManagerFactory emf = context.getBean(EntityManagerFactory.class);
        emf.close();
    }
}
 -- user 1 reading Article entity --
user 1 got the lock, block time was: 36
-- user 2 writing Article entity --
User 1 read article: Article{id=1, content='test article'}
user 2 got the lock, block time was: 2541
User 2 updated article: Article{id=1, content='updated content by user 2.'}

Note that user 2 blocked all the time during which user 1 held the lock.

If we decrease the value of javax.persistence.lock.timeout in persistence.xml to 1000 millisecs and run our client again then user 2 will throw PessimisticLockException:

-- user 1 reading Article entity --
user 1 got the lock, block time was: 37
 -- user 2 writing Article entity --
  ......
User 2 got exception while acquiring the database lock:
 javax.persistence.PessimisticLockException: could not extract ResultSet
  ......
User 1 read article: Article{id=1, content='test article'}

Example Project

Dependencies and Technologies Used:

  • spring-data-jpa 2.1.2.RELEASE: Spring Data module for JPA repositories.
    Uses org.springframework:spring-context version 5.1.2.RELEASE
  • hibernate-core 5.3.5.Final: Hibernate's core ORM functionality.
    Implements javax.persistence:javax.persistence-api version 2.2
  • h2 1.4.197: H2 Database Engine.
  • JDK 1.8
  • Maven 3.5.4

Spring Data JPA - @Lock Annotation Example Select All Download
  • spring-data-jpa-lock-annotation
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • ArticleRepository.java
          • resources
            • META-INF

    See Also