[编辑]这个问题是“如何使用EJB 3和JPA 2.0对实体bean进行原子更改”。应该很简单吧?
我试图根据目前为止的答案修复我的代码。我正在使用JBoss 6.0.0M2和Hypersonic(只需下载并调用run.bat)。
我的测试用例:创建3个线程并在循环中调用testCounterMitLock*()
500次之一。所以一个成功的测试应该打印“Anzahl eingetragene Zeilen:1500”(3 * 500)。
我试过了:
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1);
manager.lock(ct, LockModeType.WRITE);
int wert = ct.getWert();
显然不起作用,因为不同的线程可以在应用锁之前更改数据库中的值。所以我试着解决这个问题:
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1);
manager.lock(ct, LockModeType.WRITE);
manager.refresh (ct);
int wert = ct.getWert();
refresh()
应该给我当前值,隐式查询也应该确保对象现在被锁定。没有这样的运气。让我们试试JPA 2.0:
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1, LockModeType.WRITE);
int wert = ct.getWert();
这也行不通。也许锁定还不够?
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1, LockModeType.PESSIMISTIC_WRITE);
int wert = ct.getWert();
嗯...也不起作用!最后一次绝望的尝试:
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1, LockModeType.PESSIMISTIC_WRITE);
manager.flush();
manager.refresh (ct);
int wert = ct.getWert();
好的......任何人都可以解释为什么没有效果?我没有想法。
[EDIT2] PS:为了加重伤害,这是最后一个运行线程的最后一个输出:
commit/rollback: 441/62
(441 + 62 = 503)......
这是完整的代码。首先是豆子:
package server.kap15;
import java.rmi.RemoteException;
import javax.ejb.*;
import javax.persistence.*;
@Stateful
public class CounterTestBean implements CounterTestRemote, SessionSynchronization {
@PersistenceContext(unitName = "JavaEE")
EntityManager manager;
private int commit = 0;
private int rollback = 0;
public void initDatenbank() {
manager.createNamedQuery("CounterTest.deleteAll").executeUpdate();
manager.createNamedQuery("TestTabelle.deleteAll").executeUpdate();
CounterTestVersion ct = new CounterTestVersion();
ct.setNr(1);
ct.setVersion(1);
ct.setWert(1);
manager.persist(ct);
}
public boolean testCounterOhneLock() {
try {
CounterTest ct = manager.find(CounterTest.class, 1);
int wert = ct.getWert();
ct.setWert(wert + 1);
TestTabelle tt = new TestTabelle();
tt.setNr(wert);
manager.persist(tt);
manager.flush();
return true;
} catch (Throwable t) {
return false;
}
}
public boolean testCounterMitLock() {
try {
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1);
manager.lock(ct, LockModeType.WRITE);
int wert = ct.getWert();
ct.setWert(wert + 1);
TestTabelle tt = new TestTabelle();
tt.setNr(wert);
manager.persist(tt);
manager.flush();
return true;
} catch (Throwable t) {
return false;
}
}
public boolean testCounterMitLock2() {
try {
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1);
manager.lock(ct, LockModeType.WRITE);
manager.refresh (ct);
int wert = ct.getWert();
ct.setWert(wert + 1);
TestTabelle tt = new TestTabelle();
tt.setNr(wert);
manager.persist(tt);
manager.flush();
return true;
} catch (Throwable t) {
return false;
}
}
public boolean testCounterMitLock3() {
try {
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1, LockModeType.WRITE);
int wert = ct.getWert();
ct.setWert(wert + 1);
TestTabelle tt = new TestTabelle();
tt.setNr(wert);
manager.persist(tt);
manager.flush();
return true;
} catch (Throwable t) {
return false;
}
}
public boolean testCounterMitLock4() {
try {
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1, LockModeType.PESSIMISTIC_WRITE);
int wert = ct.getWert();
ct.setWert(wert + 1);
TestTabelle tt = new TestTabelle();
tt.setNr(wert);
manager.persist(tt);
manager.flush();
return true;
} catch (Throwable t) {
return false;
}
}
public boolean testCounterMitLock5() {
try {
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1, LockModeType.PESSIMISTIC_WRITE);
manager.flush();
manager.refresh (ct);
int wert = ct.getWert();
ct.setWert(wert + 1);
TestTabelle tt = new TestTabelle();
tt.setNr(wert);
manager.persist(tt);
manager.flush();
return true;
} catch (Throwable t) {
return false;
}
}
public boolean testCounterMitVersion() {
try {
CounterTestVersion ctv = manager.find(CounterTestVersion.class, 1);
int wert = ctv.getWert();
ctv.setWert(wert + 1);
manager.flush();
TestTabelle tt = new TestTabelle();
tt.setNr(wert);
manager.persist(tt);
manager.flush();
return true;
} catch (OptimisticLockException e) {
System.out.println(">>> Versionskonflikt !");
return false;
} catch (Throwable t) {
System.out.println(t.getMessage());
return false;
}
}
public long anzTestZeilen() {
Query query = manager.createNamedQuery("TestTabelle.anzZeilen");
Long anzahl = (Long) query.getSingleResult();
return anzahl;
}
public void afterBegin() throws EJBException, RemoteException {
}
public void beforeCompletion() throws EJBException, RemoteException {
}
public void afterCompletion(boolean committed) throws EJBException,
RemoteException {
if (committed)
commit++;
else
rollback++;
System.out.println("commit/rollback: " + commit + "/" + rollback);
}
}
远程接口:
package server.kap15;
import javax.ejb.Remote;
@Remote
public interface CounterTestRemote {
public void initDatenbank();
public boolean testCounterOhneLock();
public boolean testCounterMitLock();
public boolean testCounterMitLock2();
public boolean testCounterMitLock3();
public boolean testCounterMitLock4();
public boolean testCounterMitLock5();
public boolean testCounterMitVersion();
public long anzTestZeilen();
}
persistence.xml:
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">
<persistence-unit name="JavaEE">
<jta-data-source>java:DefaultDS</jta-data-source>
</persistence-unit>
</persistence>
测试客户:
package client.kap15;
import java.util.Properties;
import javax.naming.*;
import javax.rmi.PortableRemoteObject;
import server.kap15.CounterTestRemote;
public class CounterTestMitLock extends Thread {
CounterTestRemote ctr;
public static void main(String[] args) {
try
{
testMitLock();
testMitLock2();
testMitLock3();
testMitLock4();
testMitLock5();
}
catch (Exception e)
{
e.printStackTrace ();
}
}
static int N = 3;
static CounterThread[] ct = new CounterThread[N];
private static void testMitLock () throws InterruptedException
{
System.out.println("--- Counter Test MIT Lock ----------------------");
System.out.println("Testinstanzen erzeugen...");
for (int i=0; i<N; i++)
ct[i] = new CounterThreadMitLock();
runTest ();
}
private static void testMitLock2 () throws InterruptedException
{
System.out.println("--- Counter Test MIT Lock2 ----------------------");
System.out.println("Testinstanzen erzeugen...");
for (int i=0; i<N; i++)
ct[i] = new CounterThreadMitLock2();
runTest ();
}
private static void testMitLock3 () throws InterruptedException
{
System.out.println("--- Counter Test MIT Lock3 ----------------------");
System.out.println("Testinstanzen erzeugen...");
for (int i=0; i<N; i++)
ct[i] = new CounterThreadMitLock3();
runTest ();
}
private static void testMitLock4 () throws InterruptedException
{
System.out.println("--- Counter Test MIT Lock4 ----------------------");
System.out.println("Testinstanzen erzeugen...");
for (int i=0; i<N; i++)
ct[i] = new CounterThreadMitLock4();
runTest ();
}
private static void testMitLock5 () throws InterruptedException
{
System.out.println("--- Counter Test MIT Lock5 ----------------------");
System.out.println("Testinstanzen erzeugen...");
for (int i=0; i<N; i++)
ct[i] = new CounterThreadMitLock5();
runTest ();
}
private static void runTest () throws InterruptedException
{
System.out.println("Datenbank initialisieren...");
ct[0].ctr.initDatenbank();
System.out.println("Test durchführen...");
for (int i=0; i<N; i++)
ct[i].start();
System.out.println("Auf Ende warten...");
for (int i=0; i<N; i++)
ct[i].join();
System.out.println("Anzahl eingetragene Zeilen: " + ct[0].ctr.anzTestZeilen());
}
private static CounterTestRemote verbinden() {
try {
Properties p = new Properties();
p.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
p.put(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");
p.put(Context.PROVIDER_URL, "jnp://localhost:1099");
Context ctx = new InitialContext(p);
Object ref = ctx.lookup("CounterTestBean/remote");
CounterTestRemote ctr = (CounterTestRemote) PortableRemoteObject.narrow(ref, CounterTestRemote.class);
return ctr;
} catch (NamingException e) {
System.out.println("ERROR - NamingException!");
System.exit(-1);
}
return null;
}
public abstract static class CounterThread extends Thread
{
protected CounterTestRemote ctr;
public CounterThread ()
{
this.ctr = verbinden ();
}
public void run() {
for (int i = 0; i < 500; i++)
test ();
}
public abstract void test ();
}
public static class CounterThreadMitLock extends CounterThread
{
@Override
public void test ()
{
this.ctr.testCounterMitLock();
}
}
public static class CounterThreadMitLock2 extends CounterThread
{
@Override
public void test ()
{
this.ctr.testCounterMitLock2();
}
}
public static class CounterThreadMitLock3 extends CounterThread
{
@Override
public void test ()
{
this.ctr.testCounterMitLock3();
}
}
public static class CounterThreadMitLock4 extends CounterThread
{
@Override
public void test ()
{
this.ctr.testCounterMitLock4();
}
}
public static class CounterThreadMitLock5 extends CounterThread
{
@Override
public void test ()
{
this.ctr.testCounterMitLock5();
}
}
}
答案 0 :(得分:2)
由于没有任何锁定模式有效,我尝试了ewernli的手册SELECT ... FOR UPDATE
的解决方案。这给了一个有趣的例外:“意外的令牌FOR”。所以我查看了数据库。
JBoss安装时带有Hypersonic 1.8(HSQLDB)作为默认值,不支持行锁定。亲爱的JBoss开发人员:当不支持锁定模式时,JPA实现应该抛出异常。
所以我添加了一个Oracle数据源并更改了我的persistence.xml。之后,两项测试工作:
CounterTestVersion ct = manager.find(CounterTestVersion.class, 1, LockModeType.PESSIMISTIC_WRITE);
int wert = ct.getWert();
和
Query query = manager.createNativeQuery ("select * from COUNTER_TEST where NR = 1 for update", CounterTestVersion.class);
CounterTestVersion ct = (CounterTestVersion)query.getSingleResult ();
int wert = ct.getWert ()+1;
这很有趣。它也应该与LockModeType.PESSIMISTIC_FORCE_INCREMENT
一起使用。在这种情况下,我在日志中看到了这个错误:
ORA-00054: resource busy and acquire with NOWAIT specified
这在调用manager.find()
中发生。我不明白为什么两者在负载阶段表现不同。也许是JBoss或Hibernate中的一个错误。
答案 1 :(得分:1)
我有几点意见:
@Version
字段。我不认为这可行。OptimisticLockingException
拒绝任何更新)但是使用悲观锁定。Throwable
确实是错误的,你希望容器能够完成他的工作(但我猜你知道)。 所以,我会在这里使用它:
manager.lock(ct, LockModeType.READ);
然后移除catch (Throwable t)
。
更新:我现在无法测试,但我会使用类似的东西(其余代码不变):
public boolean testCounterWithLock() {
CounterTest ct = manager.find(CounterTest.class, 1);
manager.lock(ct, LockModeType.READ);
int counter = ct.getCounter();
ct.setCounter(counter + 1);
manager.flush();
return true;
}
击> <击> 撞击>
我真的怀疑这会奏效。首先,读锁定不会阻止其他线程更新该行。其次,另一个线程可以更新find()和getCounter()
之间的行
你是对的,我走得太快,上面肯定不是解决方案而且@ewernli也是对的,JPA 1.0不支持悲观锁定策略,你必须依赖数据库(并使用SELECT FOR UPDATE
语义)。不知何故,我设法忘记了这一点,并对READ模式造成了很大的困惑。我的错。谢谢你指出了这一点。
我认为你必须使用LockModeType.WRITE,但也许你可以在lock()之后使用em.refresh()来确保实体不是陈旧的?
使用LockModeType.WRITE
时,在@Version
的{{1}}子句中添加了使用WHERE
注释的实体字段,并且在{{{}期间进行了并发检查1}}:
UPDATE
如果UPDATE
子句无法匹配记录(因为另一个线程已经更新了实体),则持久性提供程序将抛出UPDATE COUNTERTEST SET COUNTER = ?, OPT_LOCK = ?
WHERE ((ID = ?) AND (OPT_LOCK = ?))
。
换句话说,在WHERE
之后刷新实体不会改变任何东西,另一个线程仍然可以刷新同一个实体而另一个线程正在修改计数器。以自动方式处理乐观锁定的唯一方法是实现重试机制。
但是当OptimisticLockException
抛出PersitenceException
时(NoResultException
和NonUniqueResultException
的实例除外),当前事务被标记为回滚,因此不能用于事务处理目的。因此,必须使用新事务执行每次重试。在一个无状态bean中,你可以进行递归远程调用,但我不认为这在有状态bean中是有意义的,所以你必须从客户端处理它。
最后,这并不是很令人满意,在JPA 1.0中处理这个问题的方法越少,我认为可以通过SELECT FOR UPDATE获得锁定。
答案 2 :(得分:1)
即使使用LockModeType.READ
或LockModeType.WRITE
,JPA 1.0也只支持乐观锁定。锁定获取仍然可以延迟到提交时间,因此您遇到了问题。
来自JPA 2.0 concurrency and locking:
PA 1.0仅支持乐观阅读 或乐观的写锁定。 JPA 2.0 支持乐观和悲观 锁定
其他资源:EJB3 performance和Pessimist Locking with JPA
要使用JPA 1.0进行真正的悲观锁定,您需要依赖数据库或特定于实现的扩展。 E.g:
JPA 2.0(与Hibernate API类似的东西)
Account acc = em.find( Account.class, id, PESSIMISTIC );
JPA 1.0
Query query = em.createNativeQuery("SELECT * ... FOR UPDATE"); // works with most db
Account acc = (Account) query.getSingleResult();
至少,这是我最终使用的,因为lock
没有按预期工作。
(注意:当发生乐观异常时,您也可以实现重试逻辑。但它很复杂,因为事务由app。服务器管理。您需要使用@TRANSACTION_NEW
暂停当前事务并启动一个新的,等等...我认为太复杂了!)
答案 3 :(得分:0)
您不会使用testCounterWithLock的返回值显示您的操作。我的猜测是你得到乐观锁定失败,有时候返回值是假的。
当碰撞在实践中很少发生时,优化锁定是一种合理的模型,并且调用者可以合理地重做工作。因此,如果您获得乐观的失败,您可以重试。
或者,使用悲观锁定模型,这会在您读取的点锁定数据库中的行。你可以这样做,为你的find()调用添加一个悲观的LockMode。需要谨慎使用悲观锁定,这很容易导致错误的并发和/或死锁。