使用JTA-JPA悲观写锁时遇到并发问题求助
Hey there, let's tackle your JTA concurrency issue head-on. You're running a GlassFish 4 EJB app with EclipseLink JPA 2.1, and you're hitting race conditions when trying to fetch a Sequence entity by name, update its nextVal column, and return a range of values—even using @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW). I’ve seen this exact scenario plenty of times, so let’s break down why it’s happening and how to fix it.
- Even with
REQUIRES_NEW, each transaction first runs the query to fetch the entity, then updates it. Without proper locking, two concurrent transactions can fetch the same stalenextValvalue before either commits their update. This leads to duplicate sequence values or lost updates. REQUIRES_NEWonly ensures each method call runs in a fresh, independent transaction—it doesn’t prevent concurrent reads of the same entity state.
Since you’re doing a read-then-write operation that needs to be atomic across requests, pessimistic locking is your best bet. This acquires a database-level row lock when you fetch the entity, blocking other transactions until the first one commits.
Modify your method to include the lock:
import javax.persistence.LockModeType; import javax.persistence.TypedQuery; import javax.ejb.TransactionAttribute; import javax.ejb.TransactionAttributeType; import javax.persistence.PersistenceContext; import javax.persistence.EntityManager; import java.util.ArrayList; import java.util.List; // ... your stateless EJB class @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public List<Long> getWithWriteLock(final String name, final int size) { // Assume your named query is "Sequence.findByName" with a :name parameter TypedQuery<Sequence> q = em.createNamedQuery("Sequence.findByName", Sequence.class); q.setParameter("name", name); // Acquire a pessimistic write lock on the entity when fetching q.setLockMode(LockModeType.PESSIMISTIC_WRITE); Sequence sequence = q.getSingleResult(); long currentVal = sequence.getNextVal(); long newVal = currentVal + size; // Update the value (managed entities auto-persist changes on transaction commit) sequence.setNextVal(newVal); // Build and return the range of values you need List<Long> sequenceValues = new ArrayList<>(); for (long i = currentVal; i < newVal; i++) { sequenceValues.add(i); } return sequenceValues; }
- How this works: The first transaction locks the
Sequencerow in the database. Any subsequent requests will wait until the lock is released (after commit), ensuring they fetch the updatednextValinstead of the stale one. - Notes: Ensure your database supports row-level locking (PostgreSQL, MySQL InnoDB, Oracle all do). Keep the transaction as short as possible to avoid holding locks longer than necessary. You can also set a lock timeout in your persistence unit to prevent infinite waits:
<!-- In persistence.xml --> <persistence-unit name="db" transaction-type="JTA"> <!-- ... other config ... --> <property name="eclipselink.jdbc.lock.timeout" value="5000"/> <!-- 5 seconds --> </persistence-unit>
If your goal is just generating unique sequential IDs, let the database handle the concurrency for you with a native sequence. This eliminates the need to manage a Sequence entity entirely.
Option A: Map the sequence directly to an entity ID
import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.SequenceGenerator; import javax.persistence.Entity; @Entity public class YourBusinessEntity { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "business_seq") @SequenceGenerator( name = "business_seq", sequenceName = "BUSINESS_ENTITY_SEQ", // Name of the DB sequence allocationSize = 10 // Batch size for performance ) private Long id; // ... other fields and methods }
Option B: Fetch a range of sequence values manually
If you need to get a batch of values at once, use a native query to call the database's sequence function:
import javax.persistence.Query; import java.util.ArrayList; import java.util.List; // ... inside your stateless EJB @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public List<Long> getSequenceRange(String sequenceName, int size) { List<Long> values = new ArrayList<>(size); for (int i = 0; i < size; i++) { // Syntax varies by DB: adjust for your database (e.g., Oracle uses SELECT SEQ.NEXTVAL FROM DUAL) Query nativeQuery = em.createNativeQuery("SELECT NEXT VALUE FOR " + sequenceName); values.add((Long) nativeQuery.getSingleResult()); } return values; }
- Benefits: No race conditions, less code to maintain, and databases are optimized for sequence generation.
If you can tolerate occasional retries, optimistic locking uses a version column to detect conflicts. When a transaction tries to update an entity that’s been modified since it was fetched, EclipseLink throws an OptimisticLockException—which you can catch and retry the operation.
First, add a version column to your Sequence entity:
import javax.persistence.Version; import javax.persistence.Entity; import javax.persistence.Id; @Entity public class Sequence { @Id private String name; private Long nextVal; @Version // This column tracks the entity version for optimistic locking private Long version; // ... getters and setters }
Then modify your method to handle retries:
import javax.persistence.OptimisticLockException; import java.util.ArrayList; import java.util.Collections; import java.util.List; // ... inside your stateless EJB @TransactionAttribute(TransactionAttributeType.REQUIRES_NEW) public List<Long> getWithWriteLock(final String name, final int size) { int maxRetries = 3; while (maxRetries > 0) { try { TypedQuery<Sequence> q = em.createNamedQuery("Sequence.findByName", Sequence.class); q.setParameter("name", name); Sequence sequence = q.getSingleResult(); long currentVal = sequence.getNextVal(); long newVal = currentVal + size; sequence.setNextVal(newVal); // Build the value list List<Long> sequenceValues = new ArrayList<>(); for (long i = currentVal; i < newVal; i++) { sequenceValues.add(i); } return sequenceValues; } catch (OptimisticLockException e) { maxRetries--; if (maxRetries == 0) { throw new RuntimeException("Failed to fetch sequence after retries", e); } // Optional: Wait a short time before retrying to reduce conflicts try { Thread.sleep(100); } catch (InterruptedException ie) { Thread.currentThread().interrupt(); } } } return Collections.emptyList(); }
- Caveat: This works best for low-concurrency scenarios where retries are feasible. For high traffic, pessimistic locking or database sequences are more reliable.
Make sure your persistence unit is set up for JTA (which it should be for EJBs):
<persistence-unit name="db" transaction-type="JTA"> <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider> <jta-data-source>jdbc/YourDataSourceName</jta-data-source> <!-- ... other properties like entity mappings ... --> </persistence-unit>
内容的提问来源于stack exchange,提问作者anshu.gupta

