Reputation: 132
Situation:
We have a webapp (Java EE-Spring-Hibernate) that generates a PDF with a barcode. The barcode contains an ID that is generated at runtime. We have a Java service that uses Hibernate to check if there's an ID for the current date, if none exists, an entry is created. If one exists it is incremented and updated. There is one row per day containing the following fields: id, coverPageId, date
So when a user creates the first barcode of the day, a row is added for the current date with coverPageId=1. Then all subsequent barcodes will get the last id from the database, increment it and save the row with the incremented value.
Problem:
When multiple users are creating PDF's at the same time, Hibernate will fetch the same value for both users resulting in the same barcode on the PDFs.
Tried Solution:
The code that fetches the current id, increments it and updates the row is all executed in the same method. This is a method inside a Spring bean. Since Spring beans are singletons I tried making the method synchronized
. This did not help.
Method:
@Transactional(isolation=Isolation.SERIALIZABLE)
public synchronized long getNextId(Date date)
{
List<CoverpageID> allCoverpageIds = this.getAllCurrentIds(date);
if(allCoverpageIds.size() == 0)
{
loggingService.warn(String.format("Found no existing ID for date '%tD', creating new record", date));
System.out.println(String.format("Found no existing ID for date '%tD', creating new record", date));
return this.createNewCoverpageIdRecord(new Date());
}
else if(allCoverpageIds.size() == 1)
{
CoverpageID coverpageId = allCoverpageIds.get(0);
loggingService.debug(String.format("Found existing ID for date '%tD': %d, incrementing and persisting", date, coverpageId.getCoverPageId()));
System.out.println(String.format("Found existing ID for date '%tD': %d, incrementing and persisting", date, coverpageId.getCoverPageId()));
coverpageId.setCoverPageId(coverpageId.getCoverPageId() + 1);
dao.save(coverpageId);
loggingService.debug(String.format("Saved existing ID for date '%tD' with incremented ID: %d", date, coverpageId.getCoverPageId()));
return coverpageId.getCoverPageId();
}
else
{
loggingService.warn(String.format("Found multiple records for date '%tD'", date));
return -1;
}
}
private List<CoverpageID> getAllCurrentIds(Date date)
{
String exClause = "where date = :date";
Map<String, Object> values = new HashMap<String, Object>();
values.put("date", date);
return dao.getAllEx(CoverpageID.class, exClause, values);
}
private long createNewCoverpageIdRecord(Date date)
{
dao.save(new CoverpageID(new Date(), new Long(1)));
return 1;
}
CoverpageID.class (entity for the barcode):
@NamedQueries({
@NamedQuery(
name = "getCoverPageIdForDate",
query = "from CoverpageID c where c.date = :date",
readOnly = true
)})
@Entity
public class CoverpageID
{
private Long id;
private Date date;
private long coverPageId;
public CoverpageID() {}
public CoverpageID(Date date, Long coverPageId)
{
this.date = date;
this.coverPageId = coverPageId;
}
public void setId(Long id)
{
this.id = id;
}
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
public Long getId()
{
return id;
}
@Temporal(TemporalType.DATE)
public Date getDate()
{
return date;
}
public void setDate(Date date)
{
this.date = date;
}
public long getCoverPageId()
{
return coverPageId;
}
public void setCoverPageId(long coverPageId)
{
this.coverPageId = coverPageId;
}
}
Does anyone have any other idea how we can prevent this concurrency issue from happening?
Upvotes: 1
Views: 2012
Reputation: 40036
I believe it has nothing to do with Hibernate. Even you are not using Hibernate you will still face such problem.
There are some choices for you:
Consider not using Hibernate for related parts. Many DB has facilities to do atomic "insert-if-absent-update-if-exists" kind of logic.
OR
If you actually want to use Hibernate, here is what you need:
With respect to your tried solution by using a object instance for handling the insert/update action, your way is not going to work.
First, if you actually keep all IDs as the state of that bean, and if your application will have only process, then it is going to work. Your code is going to DB to check everytime, and do insert/update accordingly. Even this piece of code is synchronized, it is not going to work. Please remember that among different DB sessions, changes of one session is only visible to other when the transaction is actually committed. So you will have situation like this
THREAD 1 THREAD 2
-------------------------------------------------
enter method
check DB, no record found enter method (wait coz synchronized)
insert record
exit method
check DB, no record found (coz prev txn not commited yet)
insert record
exit method
commit
commit
See? You are still facing the very same issue.
There are other way to solve apart from mine, but at least, the way you are using is not going to help.
Upvotes: 1