我正在尝试创建一个简单的序列表数据访问器。问题是,我不确定,如果我的方法是正确的,并且 - 如果它是正确的方法 - 如何配置事务隔离。我们可以放心地假设@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)
//谢谢!
答案 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
块不是必需的,但是当数据库服务器不能正确支持事务时,我已将其作为第二道防线包含在内。性能损失与此方法无关。