简体   繁体   中英

Concurrent entity access in Hibernate (JPA)

I am trying to create a simple sequence table data accessor. The problem is, I am not sure, if my approach is the correct one and - if it is correct approach - how to configure the transaction isolations. We can safely assume, that @Transactional is correctly configured in Spring Context, as transactions are working properly elsewhere.

I would like to achieve a completely thread-safe implementation of DAO (or service using the DAO), which will supply guaranteed next-in-like value for specified sequence. And, unfortunately, I can not use built-in sequence generators, as I need the value outside of entities and I can not use GUIDs to generate the IDs.

Entity:

@Entity
@Table(name = "sys_sequence")
public class SequenceEntity
{
    @Id
    @Column(name = "ID_SEQ_NAME", length = 32)
    private String name;

    @Basic
    @Column(name = "N_SEQ_VALUE")
    private int value;

    // constructors, getters & setters...
}

DAO implementation (please note, the current isolation and lock mode settings are just some other test values, i've tried (and did not work, as the entity kept being locked and query timed out):

public class SequenceDaoImpl
    extends AbstractHibernateDao
    implements SequenceDao
{
    private static final Logger logger = Logger.getLogger(SequenceDaoImpl.class);
    private static final Object lock = new Object();

    /**
     * Initializes sequence with default initial value zero (0).
     * Next value will be +1, therefore one (1).
     *
     * @param sequenceName Name of the sequence
     */
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE)
    public void initializeSequence(String sequenceName)
    {
        this.initializeSequence(sequenceName, 0);
    }

    /**
     * Initializes sequence with given initial value.
     * Next value will be +1, therefore initialValue + 1.
     *
     * @param sequenceName Name of the sequence
     * @param initialValue Initial value of sequence
     */
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE)
    public void initializeSequence(String sequenceName, int initialValue)
    {
        synchronized (lock)
        {
            Session session = this.getCurrentSession();

            try
            {
                logger.debug("Creating new sequence '" + sequenceName + "' with initial value " + initialValue);

                // create new sequence
                SequenceEntity seq = new SequenceEntity(sequenceName, initialValue);

                // save it to database
                session.persist(seq);
                session.flush();
            }
            catch (Exception ex)
            {
                throw new SequenceException("Unable to initialize sequence '" + sequenceName + "'.", ex);
            }
        }
    }

    /**
     * Returns next value for given sequence, incrementing it automatically.
     *
     * @param sequenceName Name of the sequence to use
     * @return Next value for this sequence
     * @throws SequenceException
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE, timeout = 5)
    public int getNextValue(String sequenceName)
    {
        synchronized (lock)
        {
            Session session = this.getCurrentSession();

            SequenceEntity seq = (SequenceEntity) session.createCriteria(SequenceEntity.class)
                .add(Restrictions.eq("name", sequenceName))
                .setLockMode(LockMode.PESSIMISTIC_WRITE)
                .uniqueResult();
            if (seq == null)
            {
                throw new SequenceException("Sequence '" + sequenceName + "' must be initialized first.");
            }

            seq.incValue();

            session.update(seq);
            session.flush();

            // return the new value
            return (seq.getValue());
        }
    }
}

AbstractHibernateDao has some common methods, only one used here is this:

public Session getCurrentSession()
{
    return (this.getEntityManager().unwrap(Session.class));
}

I am testing the code using simple tests:

public class SequenceDaoImplTest
    extends AbstractDbTest
{
    private static final int NUM_CONCURRENT_TASKS = 2;

    protected class GetNextValueTask
    implements Runnable
    {
        private int identifier;
        private String sequenceName;
        private List<Integer> nextValues = new LinkedList<>();
        private int iterations;
        private boolean error;

        public GetNextValueTask(int identifier, String sequenceName, int iterations)
        {
            this.identifier = identifier;
            this.sequenceName = sequenceName;
            this.iterations = iterations;
        }

        @Override
        public void run()
        {
            try
            {
                logger.debug("Starting test task #" + this.identifier + " with sequence: " + this.sequenceName);
                for (int x = 0; x < this.iterations; x++)
                {
                    logger.debug("Task #" + this.identifier + ": iteration #" + x + "; sequenceName=" + this.sequenceName);
                    nextValues.add(sequenceDao.getNextValue(this.sequenceName));
                }
                logger.debug("Completed test task #" + this.identifier);
                logger.debug(this.toValuesString());
            }
            catch (Exception ex)
            {
                logger.error("Task #" + this.identifier, ex);
                error = true;
            }
        }

        public String toValuesString()
        {
            return (StringUtils.join(nextValues, ','));
        }

        public boolean isError()
        {
            return error;
        }
    }

    @Autowired
    private SequenceDao sequenceDao;

    @Test
    public void testGetNextValue()
        throws Exception
    {
        sequenceDao.initializeSequence("SEQ_1");
        for (int x = 1; x <= 10; x++)
        {
            Assert.assertEquals(x, sequenceDao.getNextValue("SEQ_1"));
        }
    }

    @Test
    public void testGetNextValueConcurrent()
        throws Exception
    {
        sequenceDao.initializeSequence("SEQ_2");
        ExecutorService executorService = Executors.newCachedThreadPool();
        GetNextValueTask[] tasks = new GetNextValueTask[NUM_CONCURRENT_TASKS];
        for (int x = 0; x < NUM_CONCURRENT_TASKS; x++)
        {
            tasks[x] = new GetNextValueTask(x, "SEQ_2", 100);
            executorService.execute(tasks[x]);
        }
        executorService.awaitTermination(5, TimeUnit.SECONDS);

        boolean isError = false;
        for (int x = 0; x < NUM_CONCURRENT_TASKS; x++)
        {
            isError |= tasks[x].isError();
        }

        Assert.assertFalse("There was no error while running tasks.", isError);
    }
}

The first test runs just fine, I can only assume, it is because the test is running on single thread. The second test (concurrent), logs this:

pool-1-thread-2 | DEBUG | Starting test task #1 with sequence: SEQ_2 (SequenceDaoImplTest.java:41)
pool-1-thread-1 | DEBUG | Starting test task #0 with sequence: SEQ_2 (SequenceDaoImplTest.java:41)
pool-1-thread-2 | DEBUG | Task #1: iteration #0; sequenceName=SEQ_2 (SequenceDaoImplTest.java:44)
pool-1-thread-1 | DEBUG | Task #0: iteration #0; sequenceName=SEQ_2 (SequenceDaoImplTest.java:44)
pool-1-thread-1 | WARN  | SQL Error: -4872, SQLState: 40502 (SqlExceptionHelper.java:144)
pool-1-thread-1 | ERROR | statement execution aborted: timeout reached (SqlExceptionHelper.java:146)
pool-1-thread-1 | ERROR | Task #0 (SequenceDaoImplTest.java:52)

// Thank you!

I got it working at last and found out few things, which I was not completely aware of:

  • The code fails, when initializeSequence(String) is called on different thread than getNextValue(String) . Therefore, moving the initialization code to getNextValue(String) solved the issue. I was not able to find proper explanation for this in documentation, so I am using it as a rule of thumb and will investigate further.

  • Only externally called methods are annotated, internal calls are not (actually, this was not issue of my code, but I did not know that and it is related).

Spring Documentation: In proxy mode (which is the default), only external method calls coming in through the proxy are intercepted. This means that self-invocation, in effect, a method within the target object calling another method of the target object, will not lead to an actual transaction at runtime even if the invoked method is marked with @Transactional.

  • The synchronized block was meant as second line of defense and was moved to SequenceService class, that has @Transactional annotation and will be accessed externally.

Final code for int getNextValue(String, boolean) :

@Override
public int getNextValue(String sequenceName, boolean autoInit)
{
    Session session = this.getCurrentSession();

    SequenceEntity seq = (SequenceEntity) session.createCriteria(SequenceEntity.class)
        .add(Restrictions.eq("name", sequenceName))
        .setLockMode(LockMode.PESSIMISTIC_WRITE)
        .uniqueResult();
    if (seq == null)
    {
        if (!autoInit)
        {
            throw new SequenceException("Sequence '" + sequenceName + "' must be initialized first.");
        }
        seq = this.initializeSequence(sequenceName);
    }

    seq.incValue();

    session.update(seq);
    session.flush();

    // return the new value
    return (seq.getValue());
}

And for the SequenceService method int getNextValue(String) :

@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.SERIALIZABLE)
public int getNextValue(String sequenceName)
{
    synchronized (lock)
    {
        return (this.sequenceDao.getNextValue(sequenceName));
    }
}

That synchronized block is not necessary, but I've included it as a second line of defence, when the database server would not support transactions properly. The performance loss is irrelevant for this method.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM