简体   繁体   English

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

[英]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. 我们可以安全地假设@Transactional在Spring Context中已正确配置,因为事务在其他地方正常工作。

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. 我想实现DAO(或使用DAO的服务)的完全线程安全的实现,该实现将为指定的序列提供保证的类似下一个值。 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. 而且,不幸的是,我不能使用内置的序列生成器,因为我需要实体之外的值,并且不能使用GUID生成ID。

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): 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 has some common methods, only one used here is this: AbstractHibernateDao有一些常用方法,这里仅使用一种方法:

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) . 在与getNextValue(String)不同的线程上调用initializeSequence(String)时,代码失败。 Therefore, moving the initialization code to getNextValue(String) solved the issue. 因此,将初始化代码移动到getNextValue(String)解决此问题。 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. Spring文档:在代理模式(默认)下,仅拦截通过代理传入的外部方法调用。 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. 这意味着自调用实际上是目标对象中调用目标对象另一个方法的方法,即使调用的方法标记有@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. synchronized块被用作第二道防线,并被移至SequenceService类,该类具有@Transactional批注,将在外部进行访问。

Final code for int getNextValue(String, boolean) : 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) : 对于SequenceService方法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. synchronized块不是必需的,但是当数据库服务器无法正确支持事务时,我将其作为第二道防线。 The performance loss is irrelevant for this method. 该方法的性能损失与该方法无关。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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