Lockのメモ
JPAでは、EntityManager.lock(Object, LockModeType) でLockを取得できるらしい。
LockModeTypeは、WRITEとREADが用意されている。
とりあえず、次のようなコードでロックの動作を確認をしてみる。
package sample; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.EntityTransaction; import javax.persistence.LockModeType; import javax.persistence.Persistence; public class LockExample { public static void main(String[] args) { int id = -1; EntityManagerFactory emf = Persistence.createEntityManagerFactory("hibernate"); EntityManager em = emf.createEntityManager(); final EntityTransaction tx = em.getTransaction(); tx.begin(); Actor brad = new Actor("Brad Pitt"); em.persist(brad); id = brad.getId(); tx.commit(); em.close(); System.out.println("################################################ "); Thread t1 = new Thread(new Updater(emf, "Johnny Depp", id, 3)); Thread t2 = new Thread(new Updater(emf, "Orlando Bloom", id, 0)); t1.start(); // 1秒Wait try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } t2.start(); /* * Lockされているなら、次の順で更新されるはず。 * 「Brad Pitt」→「Johnny Depp」→「Orlando Bloom」 */ try { t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } emf.close(); System.out.println("################################################ "); } private static class Updater implements Runnable { private EntityManager em; private String name; private int actorId; private int minuts; public Updater(EntityManagerFactory emf, String name, int actorId, int minuts) { this.em = emf.createEntityManager(); this.name = name; this.actorId = actorId; this.minuts = minuts; } public void run() { final EntityTransaction tx = em.getTransaction(); tx.begin(); Actor actor = em.find(Actor.class, this.actorId); System.out.println("############# find: " + name); em.lock(actor, LockModeType.WRITE); System.out.println("############# lock: " + name); try { Thread.sleep(1000 * minuts); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("############# before update: " + name); actor.setName(name); tx.commit(); System.out.println("############# commit: " + name); } @Override public void finalize() { em.close(); em = null; } } }
Lockを取得すれば、「Brad Pitt」→「Johnny Depp」→「Orlando Bloom」という順に更新されるはず。
いざ、実行・・・・してみるといきなりNullPointerException発生。
どうやらEntityManager.lock()でthrowされているらしい・・・
悩むこと数十分。。
どうもLockModeType.WRITEを使用する場合は@Versionが必要らしい。
で、実行結果
Hibernate: insert into Actor (id, name, version) values (null, ?, ?) ################################################ Hibernate: select actor0_.id as id0_0_, actor0_.name as name0_0_, actor0_.version as version0_0_ from Actor actor0_ where actor0_.id=? ############# find: Johnny Depp Hibernate: update Actor set version=? where id=? and version=? ############# lock: Johnny Depp Hibernate: select actor0_.id as id0_0_, actor0_.name as name0_0_, actor0_.version as version0_0_ from Actor actor0_ where actor0_.id=? ############# before update: Johnny Depp Hibernate: update Actor set name=?, version=? where id=? and version=? ############# commit: Johnny Depp ############# find: Orlando Bloom Hibernate: update Actor set version=? where id=? and version=? ############# lock: Orlando Bloom ############# before update: Orlando Bloom Hibernate: update Actor set name=?, version=? where id=? and version=? ############# commit: Orlando Bloom ################################################
SELECT * FROM ACTOR;
ID | NAME | VERSION |
---|---|---|
41 | Orlando Bloom | 4 |
期待通り「Orlando Bloom」で更新されているけど、無駄にversionを更新しているし、FOR UPDATE してくれていない。
悩むこと数十分。。
LockModeType.WRITE は、バージョンをインクリメントするものでFOR UPDATEするものではないらしい。
やはり、ドキュメントはちゃんとみないと駄目かORZ
というわけで、 LockModeType.WRITE を LockModeType.READ に変更して実行。
Hibernate: insert into Actor (id, name, version) values (null, ?, ?) ################################################ Hibernate: select actor0_.id as id0_0_, actor0_.name as name0_0_, actor0_.version as version0_0_ from Actor actor0_ where actor0_.id=? ############# find: Johnny Depp Hibernate: select id from Actor where id =? and version =? for update ############# lock: Johnny Depp Hibernate: select actor0_.id as id0_0_, actor0_.name as name0_0_, actor0_.version as version0_0_ from Actor actor0_ where actor0_.id=? ############# before update: Johnny Depp Hibernate: update Actor set name=?, version=? where id=? and version=? ############# commit: Johnny Depp ############# find: Orlando Bloom Hibernate: select id from Actor where id =? and version =? for update ############# lock: Orlando Bloom ############# before update: Orlando Bloom Hibernate: update Actor set name=?, version=? where id=? and version=? ############# commit: Orlando Bloom ################################################
SELECT * FROM ACTOR;
ID | NAME | VERSION |
---|---|---|
42 | Orlando Bloom | 2 |
今度は、 FOR UPDATE してくれているみたい。
そ〜いうもんなんだということで終了。。
と思ったけど、試しにEntityManager.lock()の呼び出しをコメントアウトしたらデッドロックが発生した。。
え〜?とまた悩むこと数十分。。
どうもは、H2(使用しているRDB)ではSELECTで共有ロックを取得することが関係しているみたい。
で、排他ロックは共有ロックが開放されるまでWAITするらしい。
つまり、次のようなことが起こっているのかな?多分。。
①Johnny がSELECTし、共有ロック取得
②Orlando がSELECTし、共有ロック取得
③Orlando がUPDATE。排他ロックを取得したいが、Johnny の共有ロックの開放待ち
④Johnny がUPDATE。排他ロックを取得したいが、Orlando の共有ロックの開放待ち
⑤デッドロック・・・
昔DB2でも似たような現象に遭遇したような気がするなぁ。
もう疲れたので今度こそ終了。。