![](/img/trans.png)
[英]Spring with JPA: Queue concurrent access to JPA entity from different requests
[英]Concurrent entity access in Hibernate (JPA)
我正在嘗試創建一個簡單的序列表數據訪問器。 問題是,我不確定我的方法是否正確,以及(如果正確)方法如何配置事務隔離。 我們可以安全地假設@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)
// 謝謝!
我終於使它工作了,發現了幾件事,但我並沒有完全意識到:
在與getNextValue(String)
不同的線程上調用initializeSequence(String)
時,代碼失敗。 因此,將初始化代碼移動到getNextValue(String)
解決此問題。 我無法在文檔中找到對此的正確解釋,因此我將其用作經驗法則,並將作進一步調查。
僅注釋了外部調用的方法,沒有注釋內部的調用(實際上,這不是我的代碼的問題,但我不知道這與它相關)。
Spring文檔:在代理模式(默認)下,僅攔截通過代理傳入的外部方法調用。 這意味着自調用實際上是目標對象中調用目標對象另一個方法的方法,即使調用的方法標記有@Transactional,也不會在運行時導致實際事務。
synchronized
塊被用作第二道防線,並被移至SequenceService
類,該類具有@Transactional
批注,將在外部進行訪問。 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
塊不是必需的,但是當數據庫服務器無法正確支持事務時,我將其作為第二道防線。 該方法的性能損失與該方法無關。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.