Hibernate中的并发实体访问(JPA)

时间:2015-03-16 11:04:13

标签: java spring hibernate jpa transactions

我正在尝试创建一个简单的序列表数据访问器。问题是,我不确定,如果我的方法是正确的,并且 - 如果它是正确的方法 - 如何配置事务隔离。我们可以放心地假设@Transactional在Spring Context中正确配置,因为事务在其他地方正常工作。

我想实现DAO(或使用DAO的服务)的完全线程安全的实现,它将为指定的序列提供保证的下一个类似的值。不幸的是,我不能使用内置的序列生成器,因为我需要实体之外的值,我不能使用GUID来生成ID。

实体:

@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实现(请注意,当前的隔离和锁定模式设置只是其他一些测试值,我已经尝试过(并且没有工作,因为实体一直被锁定并且查询超时):

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有一些常用方法,这里只使用一个:

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

我正在使用简单的测试来测试代码:

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);
    }
}

第一个测试运行得很好,我只能假设,这是因为测试是在单线程上运行的。第二个测试(并发),记录下来:

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)

//谢谢!

1 个答案:

答案 0 :(得分:0)

我终于开始工作,发现了一些我并不完全清楚的事情:

  • 在与initializeSequence(String)不同的线程上调用getNextValue(String)时代码失败。因此,将初始化代码移至getNextValue(String)解决了问题。 我无法在文档中找到适当的解释,因此我将其作为经验法则使用,并将进一步调查。

  • 只有外部调用的方法被注释,内部调用不是(实际上,这不是我的代码的问题,但我不知道它并且它是相关的。)

  

Spring文档:在代理模式(默认设置)下,只拦截通过代理进入的外部方法调用。这意味着实际上,自调用目标对象中的一个方法调用目标对象的另一个方法,即使被调用的方法用@Transactional标记,也不会在运行时导致实际的事务。

  • synchronized块意味着作为第二道防线,并被移至SequenceService类,其中包含@Transactional注释并将在外部访问。 < / LI>

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());
}

对于SequenceService方法int getNextValue(String)

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

synchronized块不是必需的,但是当数据库服务器不能正确支持事务时,我已将其作为第二道防线包含在内。性能损失与此方法无关。