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 ProjectDependencies 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
|