I am working on a project using Java 17.0.11, Spring Boot 3.1.1, Hibernate, and Postgres. I have a piece of code that has high concurrency requirements, and I used a pessimistic lock hoping to ensure that every time the function is called, it gets the most up-to-date data. Here is the code:
pom
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.my.app</groupId>
<artifactId>myapp</artifactId>
<version>1.0.0</version>
<name>myapp</name>
<description>myapp</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<log4j2.version>2.17.1</log4j2.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.17.5</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.awspring.cloud</groupId>
<artifactId>spring-cloud-aws-dependencies</artifactId>
<version>3.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- JPA-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>5.3.7.Final</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- column validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- email -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
// ......
Controller:
public void processStockOut(
@RequestBody @Valid PDADestockingStockOutDTO pdaDestockingStockOutDTO,
@RequestHeader(value = "pda-version", required = false) String version) {
log.info("[PDA][DEPART][{}][START] requestBody: {}", pdaDestockingStockOutDTO.getDestockingId(), JacksonSerializeUtil.serialize(pdaDestockingStockOutDTO));
app5ExStock.pdaDepart(pdaDestockingStockOutDTO, version, GlobalParam.getUserName());
log.info("[PDA][DEPART][{}][END]", pdaDestockingStockOutDTO.getDestockingId());
}
Repository:
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("from Outbound where id =:id")
Optional<Outbound> findByIdForUpdate(@Param("id") String id);
Service:
@Transactional
public void pdaDepart(PDADestockingStockOutDTO pdaDestockingStockOutDTO, String version, String userName) {
Outbound outbound = outboundRepository.findByIdForUpdate(pdaDestockingStockOutDTO.getDestockingId())
.orElseThrow(() -> new BadRequestException(DESTOCKING_NOT_FOUND));
// Log the status of the outbound
log.info("[PDA][DEPART][{}] {}", outbound.getId(), outbound.getStatus());
if (outbound.getStatus() == OutboundStatusEnum.STOCK_OUT) {
throw new InternalServerException("Déjà Départ avec succès");
}
// ...
domainOutbound.exStock(outbound, stockOutAt);
// ...
}
@Transactional
public void exStock(Outbound outbound, LocalDateTime stockOutAt) {
outbound.setStatus(OutboundStatusEnum.STOCK_OUT);
outbound.setStockOutAt(stockOutAt);
outboundRepository.save(outbound);
}
application.yml
spring:
jpa:
database: POSTGRESQL
generate-ddl: false
hibernate:
ddl-auto: none
database-platform: org.hibernate.dialect.PostgreSQLDialect
properties:
hibernate:
connection:
isolation: 2
// ......
In practical use cases, due to frontend design issues, there are situations where a button that should be clicked only once ends up being clicked multiple times. This leads to the controller being triggered multiple times. I expect the subsequent function triggers to fetch the latest data (which should have already been updated to STOCK_OUT
by the first function call). However, most of the time (but not always), it fetches the old data (STOCK_IN
).
Interestingly, this issue never occurs during local debugging; it only happens on the production server. I have similarly used pessimistic locking in other parts of the application, and they occasionally fail as well.
NORMAL Log Info
just one [END] and one ‘STOCK_IN’
Jul 16 15:16:19 my-app-api app[web] INFO 2024-07-16 13:16:19 INFO c.c.m.a.b.p.PdaStep4And5StockOutController:118 - 0 0 - [PDA][DEPART][L-FR-16072024-2992][START] requestBody: {"destockingId":"L-FR-16072024-2992","licensePlate":"R0234BCS","bindingSwapBodyList":[{"id":"00686203","plomb":"00686203"}],"isUploaded":true,"isCmr2Uploaded":true}
Jul 16 15:16:19 my-app-api app[web] INFO 2024-07-16 13:16:19 INFO c.c.m.s.App5ExStock:1243 - 0 0 - [PDA][DEPART][L-FR-16072024-2992] STOCK_IN
Jul 16 15:16:21 my-app-api app[web] INFO 2024-07-16 13:16:21 INFO c.c.m.a.b.p.PdaStep4And5StockOutController:118 - 0 0 - [PDA][DEPART][L-FR-16072024-2992][START] requestBody: {"destockingId":"L-FR-16072024-2992","licensePlate":"R0234BCS","bindingSwapBodyList":[{"id":"00686203","plomb":"00686203"}],"isUploaded":true,"isCmr2Uploaded":true}
Jul 16 15:16:23 my-app-api app[web] INFO 2024-07-16 13:16:23 INFO c.c.m.a.b.p.PdaStep4And5StockOutController:118 - 0 0 - [PDA][DEPART][L-FR-16072024-2992][START] requestBody: {"destockingId":"L-FR-16072024-2992","licensePlate":"R0234BCS","bindingSwapBodyList":[{"id":"00686203","plomb":"00686203"}],"isUploaded":true,"isCmr2Uploaded":true}
Jul 16 15:16:24 my-app-api app[web] INFO 2024-07-16 13:16:24 INFO c.c.m.a.b.p.PdaStep4And5StockOutController:118 - 0 0 - [PDA][DEPART][L-FR-16072024-2992][START] requestBody: {"destockingId":"L-FR-16072024-2992","licensePlate":"R0234BCS","bindingSwapBodyList":[{"id":"00686203","plomb":"00686203"}],"isUploaded":true,"isCmr2Uploaded":true}
Jul 16 15:16:26 my-app-api app[web] INFO 2024-07-16 13:16:26 INFO c.c.m.a.b.p.PdaStep4And5StockOutController:118 - 0 0 - [PDA][DEPART][L-FR-16072024-2992][START] requestBody: {"destockingId":"L-FR-16072024-2992","licensePlate":"R0234BCS","bindingSwapBodyList":[{"id":"00686203","plomb":"00686203"}],"isUploaded":true,"isCmr2Uploaded":true}
Jul 16 15:16:29 my-app-api app[web] INFO 2024-07-16 13:16:29 INFO c.c.m.a.b.p.PdaStep4And5StockOutController:120 - 0 0 - [PDA][DEPART][L-FR-16072024-2992][END]
Jul 16 15:16:29 my-app-api app[web] INFO 2024-07-16 13:16:29 INFO c.c.m.s.App5ExStock:1243 - 0 0 - [PDA][DEPART][L-FR-16072024-2992] STOCK_OUT
Jul 16 15:16:29 my-app-api app[web] INFO 2024-07-16 13:16:29 INFO c.c.m.s.App5ExStock:1243 - 0 0 - [PDA][DEPART][L-FR-16072024-2992] STOCK_OUT
Jul 16 15:16:29 my-app-api app[web] INFO 2024-07-16 13:16:29 INFO c.c.m.s.App5ExStock:1243 - 0 0 - [PDA][DEPART][L-FR-16072024-2992] STOCK_OUT
Jul 16 15:16:29 my-app-api app[web] INFO 2024-07-16 13:16:29 INFO c.c.m.s.App5ExStock:1243 - 0 0 - [PDA][DEPART][L-FR-16072024-2992] STOCK_OUT
ERROR Log Info:
multiple [END] and ‘STOCK_IN’
Jul 16 13:33:17 my-app-api app[web] INFO 2024-07-16 11:33:17 INFO c.c.m.a.b.p.PdaStep4And5StockOutController:118 - 0 0 - [PDA][DEPART][IT-POZ-POS2024071529999][START] requestBody: {"destockingId":"IT-POZ-POS2024071529999","licensePlate":"AA247AA","bindingSwapBodyList":[{"plomb":"00689999"}],"isUploaded":true}
Jul 16 13:33:17 my-app-api app[web] INFO 2024-07-16 11:33:17 INFO c.c.m.s.App5ExStock:1243 - 0 0 - [PDA][DEPART][IT-POZ-POS2024071529999] STOCK_IN
Jul 16 13:33:24 my-app-api app[web] INFO 2024-07-16 11:33:24 INFO c.c.m.a.b.p.PdaStep4And5StockOutController:120 - 0 0 - [PDA][DEPART][IT-POZ-POS2024071529999][END]
Jul 16 13:33:24 my-app-api app[web] INFO 2024-07-16 11:33:24 INFO c.c.m.a.b.p.PdaStep4And5StockOutController:118 - 0 0 - [PDA][DEPART][IT-POZ-POS2024071529999][START] requestBody: {"destockingId":"IT-POZ-POS2024071529999","licensePlate":"AA247AA","bindingSwapBodyList":[{"plomb":"00689999"}],"isUploaded":true}
Jul 16 13:33:24 my-app-api app[web] INFO 2024-07-16 11:33:24 INFO c.c.m.s.App5ExStock:1243 - 0 0 - [PDA][DEPART][IT-POZ-POS2024071529999] STOCK_IN
Jul 16 13:33:27 my-app-api app[web] INFO 2024-07-16 11:33:27 INFO c.c.m.a.b.p.PdaStep4And5StockOutController:120 - 0 0 - [PDA][DEPART][IT-POZ-POS2024071529999][END]
How can I ensure that the pessimistic lock is effective?
I’d like the pessimistic lock to work properly, with the updated data on the second fetch
Leo is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
9
Here is some adjustment:
@Transactional
public void pdaDepart(PDADestockingStockOutDTO pdaDestockingStockOutDTO,
String version, String userName) {
Outbound outbound = outboundRepository.findByIdForUpdate(pdaDestockingStockOutDTO.getDestockingId())
.orElseThrow(() -> new BadRequestException(DESTOCKING_NOT_FOUND));
log.info("[PDA][DEPART][{}] {}", outbound.getId(), outbound.getStatus());
if (outbound.getStatus() == OutboundStatusEnum.STOCK_OUT) {
throw new InternalServerException("Déjà Départ avec succès");
}
domainOutbound.exStock(outbound, stockOutAt);
outboundRepository.save(outbound);
log.info("[PDA][DEPART][{}] Transaction completed", pdaDestockingStockOutDTO.getDestockingId());
}
and check in your application.properties that you have configured the lock correctly for pessimistic locking
#TRANSACTION_READ_COMMITTED
spring.jpa.properties.hibernate.connection.isolation=2
8