You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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.

Why the Race Condition Happens
  • 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 stale nextVal value before either commits their update. This leads to duplicate sequence values or lost updates.
  • REQUIRES_NEW only ensures each method call runs in a fresh, independent transaction—it doesn’t prevent concurrent reads of the same entity state.
Fix 1: Pessimistic Write Locking (Most Reliable for High Concurrency)

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 Sequence row in the database. Any subsequent requests will wait until the lock is released (after commit), ensuring they fetch the updated nextVal instead 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>
    
Fix 2: Use Native Database Sequences (Simpler Alternative)

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.
Fix 3: Optimistic Locking (For Low Concurrency Scenarios)

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 09:14:55