Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring with JPA lock with long processing time

I have multiple quartz workers Each worker picks a db record (printer) and then doing some work on it (reading info from the printer over the network).
It can take up to 30 sec to 1 min to complete each job.

Back in JDBC days I would run (pseudo code)

     printer = "select from printer where status=? for update"
     do the work, (takes 1 min)
     update the printer record.

My question is this approach with PESSIMISTIC_WRITE is ok:

public interface PrinterRepo extends CrudRepository<Printer,String>
 {
 @Lock(LockModeType.PESSIMISTIC_WRITE)
 @Query("SELECT r FROM Printers r where r.status = :status")
 Printer findOneAndLock(@Param("status")String status);
}

Then the worker:

@Transactional
public void execute(JobExecutionContext jobExecutionContext) {
      Printer p = printerRepo.findOneAndLock("status");
     //do the work here (30 sec to 1 min)
     printerRepo.save(p);
 }

For my understanding lock will be release at the end of the function annotated with @Transactional correct? My question is what will happen to other workers?
Will they starve while waiting for findOneAndLock?

Thank you

like image 867
JavaSheriff Avatar asked Apr 25 '19 17:04

JavaSheriff


People also ask

Is JDBCTemplate faster than JPA?

At work we use Hibernate JDBCTemplate because it has more flexibility. It also has better performance than JPA because you are not "loading" a lot of unnecessary data into your app. In the JDBCTemplate case, your SQL skills go a long way in giving you exactly what you need at the right speed.

How do you handle a pessimistic lock exception?

PessimisticLockException indicates that obtaining a lock or converting a shared to exclusive lock fails and results in a transaction-level rollback. LockTimeoutException indicates that obtaining a lock or converting a shared lock to exclusive times out and results in a statement-level rollback.

Is Spring data JPA better than hibernate?

JPA uses EntityManager interface to create/read/delete operation and maintains the persistence context. Hibernate uses Session interface to create/read/delete operation and maintains the persistence context. JPA uses JPQL (Java Persistence Query Language) as Object Oriented Query language for database operations.


1 Answers

Regardless of which type and level of locks you are going to use, and what will happen to other workers, the long-term lock, as well as long-term transaction, is not a good solution. IMHO in your case is better to use different approach without any locks, for example, an additional table to record the printer 'locks':

create table printer_locks (
  printer_id bigint    not null constraint printer_locks_pkey primary key,
  created_at timestamp not null,
  worker_id  bigint    not null constraint fk_printer_locks_worker_id references workers,

  constraint fk_printer_locks_printer_id foreign key (printer_id) references printers(id)
);

When a worker wants to start the job with some printer, first it tries to insert the record to this table. Then, in case of success, it starts the job. When the job is completed then the worker deletes this record.

Because the printer_id column is unique - other workers will not be able to start working with the same printer at the same time.

Implementation:

@Entity
@Table(name = "workers")
public class Worker {

   @Id 
   @GeneratedValue(...)
   private Long id;
   // other stuff...
}
@Entity
@Table(name = "printers")
public class Printer {

   @Id 
   @GeneratedValue(...)
   private Long id;
   // other stuff...
}
@Entity
@Table(name = "printer_locks")
public class PrinterLock {

   @Id 
   private Long id;

   private Instant createdAt = Instant.now();

   @OneToOne(fetch = FetchType.LAZY)
   @MapsId
   private Printer printer;

   @ManyToOne(fetch = FetchType.LAZY)
   private Worker worker;

   public PrinterLock(Printer printer, Worker worker) {
       this.printer = printer;
       this.worker = worker;
   } 

   // other stuff...
}
public void execute(...) {

     Worker worker = ...;
     Long printerId = ...;

     printerRepo.findById(printerId)
         .map(printer -> {
             try {
                 printerLockRepo.save(new PrinterLock(printer, worker)); 
                 try {
                     // do the work here (30 sec to 1 min)
                 } finally {
                     printerLockRepo.deleteById(printerId); 
                 }
             } catch(Exception e) {
                log.warn("Printer {} is busy", printerId);
             }         
         })
         .orElseThrow(() -> new PrinterNotFoundException(printerId));
 }

Note that the execute method even doesn't have @Transactional annotation.

An additional advantage of this approach is the column createdAt which allow you to control hanging jobs.

Further reading:

  • Row Level Locking in Mysql
  • Pessimistic Locking in JPA
  • Explicit Locking in PostgreSQL
  • Locking Reads in MySQL
  • The best way to map a @OneToOne relationship with JPA and Hibernate
like image 112
Cepr0 Avatar answered Sep 23 '22 09:09

Cepr0