Tuesday, December 31, 2013

I have an ordering platform on a remote webserver and a local production machine here at my office. Both webservers access the same remote MySQL (InnoDB) database.
My Problem: The production takes several minutes in which 2-3 transactions are open. In this time I generate new invoice numbers and increment them. The most recent invoice number is saved in a Numbers-Table on the database.
public Long getNewInvoiceNumber() {
    Criteria crit = getSession().createCriteria(Numbers.class);
    Numbers n = ((Numbers)crit.uniqueResult());
    Long newNumber = n.getInvoiceNumber() + 1L;
    n.setInvoiceNumber(newNumber);
    return newNumber;
}
Now when someone is saving a new order during the production they are accessing the same Numbers-Table to generate another number (not the invoice). All the orders processed by the production are saved with the correct invoice numbers. However, the Numbers-Table isn't updated with the newest value and the invoice number remains the same as before the production.
I understand that one of the transaction gets a "stale table" message. But what is the behavior of MySQL / Hibernate / Java? I'd like to get an exception from one of the transactions so I can rollback and don't have this dangerous database inconsistency.
EDIT : This is how the table Numbers looks like:
id | invoice_number | tag_number
0  | 16533          | 1055
id is the primary key. I only access this one row and increase the needed number.
EDIT 2 : Okey, I see that this table structure is kinda bad. I updated it to:
id             | number
invoice_number | 16533
tag_number     | 1055
Now I can access each row independently. Don't know if that solves my problem though.
share|improve this question
 
Does your Numbers.class table only have 1 row ever? Take a look at the APIs for setLockMode() / setLockOptions(). You need to SELECT ... FOR UPDATE to ensure no other user is doing the same thing. Also you need to getSession().update(n); right ? –  Darryl Miles Sep 20 '12 at 13:10 
 
Yes, as of now there is only 1 row ever. I thought the database has its own automatic locking mode, I wanna know the behavior if there are 2 users changing the row simultaneously. –  rotsch Sep 20 '12 at 13:20
 
Two independent users can view the same or old data at the same time from two different transactions. To stop this in the relatively few cases SELECT ... FOR UPDATE exists. I'd research this topic more and test out the theory with a command line SQL client. –  Darryl Miles Sep 20 '12 at 13:26
add comment

2 Answers

public Long getNewInvoiceNumber() {
    Criteria crit = getSession().createCriteria(Numbers.class);
    crit.setLockMode(LockMode.PESSIMISTIC_WRITE);   // LINE ADDED
    Numbers n = ((Numbers)crit.uniqueResult());
    Long newNumber = n.getInvoiceNumber() + 1L;
    n.setInvoiceNumber(newNumber);
    getSession().update(n);              // LINE ADDED
    return newNumber;
}
You must use InnoDB if using MySQL, you should ensure Hibernate is adding the "FOR UPDATE" to the SQL generated for the crit.uniqueResult() part. I would also set a Java breakpoint just after (to stop execution) and manually test the same SQL query causes another client to block.
This is more to test your SQL server is sane and your Dialect is setup correctly and basically the feature works for you.
This forces serialization at the SQL server in the generation of the next InvoiceNumber.
This way you never get 2 InvoiceNumber the same even if 1000 users simultaneously try to make invoices.
NOTE: LockOptions replaces the LockMode to specify mode things about this aspect of concurrency. See the hibernate release notes for help.
NOTE: You talked of transactions lasting minutes. While the transaction is not committed no other user can generate a new invoice number. Maybe you need to run this as a nested and separate transaction. Then you have the problem holes appearing in InvoiceNumber's if there is a failure to use the number due to some other processing error. Accountants / accounting systems don't like holes in invoice numbers, nor them being out of date sequence.
share|improve this answer
add comment
There are different ways to concurrently work with the database, for instance using Optimistic lock or Pessimistic one (you should be careful with the latter since it may cause delays and dead locks).
But I guess in your case you have to change the Numbers table right after some transaction starts, not by the end of it. This would ensure that the slot for the invoice is reserved.
Also makes sense to look at the Id Generators of the Hibernate, you can see there different ideas of how to implement value-generating mechanisms.
share|improve this answer
add comment

No comments: