Whiteship's Note


Isolation 단계 더 높이기

Hibernate/Chapter 10 : 2008.03.30 00:25


Explicit pessimistic locking

  • 격리 수준을 read comitted 보다 높게 설정하는 것은 애플리케이션의 확장성을 고려할 때 좋치 않다.
  • Persistence context cache가 repeatable read를 제공하긴 하지만 이걸로 항상 만족스럽지 않을 수도 있다.
Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
Item i = (Item) session.get(Item.class, 123);
String description = (String)
session.createQuery("select i.description from Item i" +
" where i.id = :itemid")
.setParameter("itemid", i.getId() )
.uniqueResult();
tx.commit();
session.close();
  • 위의 코드는 DB에서 같은 데이터를 두 번 읽어온다. 이 때 isolation level이 read committed 였다면, 두 번째에 읽어오는 값이 처음 읽어온 데이터와 다를 수 있다.(둘 사이에 어떤 트랜잭션이 해당하는 값을 바꾸고 커밋했을 수 있다.)
  • 전체 트랜잭션의 isolation level을 높이는 것이 아니라 lock() 메소드를 사용하여 해당하는 부분의 트랜잭젼의 isolation level을 높일 수 있다.
Session session = sessionFactory.openSession(); 
Transaction tx = session.beginTransaction();
Item i = (Item) session.get(Item.class, 123);

session.lock(i, LockMode.UPGRADE);

String description = (String)
session.createQuery("select i.description from Item i" +
" where i.id = :itemid")
.setParameter("itemid", i.getId() )
.uniqueResult();
tx.commit();
session.close();
  • 위의 LockMode.UPGRADE 는 item 객체 대응하는 레코드의 pessimistic lock을 가지고 다니게 된다.
Item i = (Item) session.get(Item.class, 123, LockMode.UPGRADE);
  • 위와같이 코드를 한 줄 줄일 수도 있다.
  • LockMode.UPGRADE는 롹을 가져올 때까지 대기하게 된다. 대기 시간은 사용하는 DB에 따라 다르다.
  • LockMode.NOWAIT는 롹이 없으면 기다리지 않고 쿼리가 바로 fail하도록 한다.

The Hibernate lock modes

  • LockMode.NONE - 락 사용하지 않음. 캐시에 객체가 존재하면 그 객체를 사용.
  • LockMode.READ - 모든 캐시를 무시하고 현재 메모리에 있는 엔티티의 버전과 실제 DB에 있는 버전이 같은지 확인한다.
  • LockMode.UPGRADE - LockMode.READ가 하는 일에 더해서 DB에서 pessimistic upgrade lock을 가져온다. SELECT ... FOR UPDATE 문을 지원하지 않는 DB를 사용할 때는 자동으로 LockMode.READ로 전환된다.
  • LockMode.UPGRADE_NOWAIT - UPDGRADE와 동일한데, SELECT ... FOR UPDATE NOWAIT를 사용한다. 락이 없으면 바로 예외를 던진다. NOWAIT를 지원하지 않으면 자동으로 LockMode.UPGRADE로 전환된다.
  • LockMode.FORCE - 객체에 버전을 DB에서 증가시키도록 강제한다.
  • LockMode.WRITE - 하이버네이트가 현재 트랜잭션에 레코드를 추가했을 때 자동으로 얻어온다.(사용자가 명시적으로 애플리케이션에서 사용할 일 없음.)
  • load()와 get()은 기본으로 LockMode.NONE을 사용한다.
  • Detached 상태의 객체를 reattach 할 때 LockMode.READ 를 유용하게 사용할 수 있다. 자동으로 reattach까지 해주니까.(하이버네이트의 lock()메소드만 reattch까지 해주지, JP의 lock()메소드는 reattch해주지 않느다.)

reattche를 할 때 반드시 lock() 메소드를 사용해야 하는 것은 아니다. 이전에도 살펴봤듯이 Session에 update() 메소드를 사용하면 Transiecnt 상태의 객체가 Persistent 상태가 된다. lock(LockMode.READ)는 Persistent 상태로 사용하려는 객체의 데이터들이 이전에 로딩된 그 상태 그대로 인지, 혹시 다른 트랜잭션에 의해 데이터들이 변경되지는 않았는지 확인하기 위한 용도다. 그렇게 확인을 함과 동시에 덤으로 Persistent 상태로 전환(reattach)시켜 주는 것이다. 즉, Transient 상태의 객체를 lock() 메소드의 인자로 넘겨줄 수 있다는 것인데, 이것은 하이버네이트에서만 할 수 있다. JP에서는 이미 Persistent 상태인 객체한테만 lock()을 호출할 수 있다.

Item item = ... ; 
Bid bid = new Bid();
item.addBid(bid);
...
Transaction tx = session.beginTransaction();
session.lock(item, LockMode.READ);
tx.commit();

Forcing a version increment

  • 하이버네이트가 개발자가 수정한 내용을 버전을 올려야 하는 변경사항인지 모를 수가 있다. 이럴 때 명시적으로 버전을 올리라고 알려줘야 한다.
Session session = getSessionFactory().openSession(); 
Transaction tx = session.beginTransaction();

User u = (User) session.get(User.class, 123);
u.getDefaultBillingDetails().setOwner("John Doe");

tx.commit();
session.close();
  • 하이버네이트는 객체와 직접적으로 닿아있는 값의 변화만을 알지 한 단계 걸친 변화는 해당 객체의 수정사항으로 인식하지 않는다.
  • 위의 코드에서 BillingDetail 객체만 버전을 올리게 된다. 하지만 개발자는 정보를 수정한 BillingDetail(aggregate)을 가지고 있는 User(root object) 역시 버전을 올리고 싶어 할 수 있다.
Session session = getSessionFactory().openSession(); 
Transaction tx = session.beginTransaction();

User u = (User) session.get(User.class, 123);
session.lock(u, LockMode.FORCE);
u.getDefaultBillingDetails().setOwner("John Doe");

tx.commit();
session.close();
  • 이렇게 하면 현재 User 객체에 대응하는 레코드를 가지고 작업하고 있는 모든 Unit of work들이 해당 객체의 버전이 올라갔다고 인식한다.
  • 심지어 아무런 변경을 가하지 않았더라도 해당 Root 객체의 버전은 올라간다.

공부할 것

  • 사용하는 DB에 따라 다른 결과가 나올 수 있다. Select ... FOR UPDATE NOWAIT 문을 지원하느냐 안 하느냐에 따라 LockMode.UPGRADE 와 LockMode.UPGRADE_NOWAIT의 결과가 LockMode.READ 와 같게 나올 수도 있다.
  • 결국 원하는 Isolation level을 정하는 것이 중요하고, repeatable read를 보장하려면 LockMode.UPGRADE 또는 LockMode.UPGRADE_NOWAIT를 사용하여 pessimisitc locking하면된다.
  • LockMode.READ는 DB에서 데이터를 읽어와서 버전을 확인한다. isolation level을 미리 올려두는 것이 아니라, optimistic 한 방법으로 DB에 쓰기 직전에 확인하기 위한 용도라고 생각된다.
  • 자동 버전 증가는 오직 엔티티가 직접적으로 물고 있는 속성, 콜렉션 자체의 변화만 인식한다. 객체 맵의 루트를 올려야 한다면, 해당 루트 객체를 가져올 때 LockMode.FORCE를 사용하며 이 녀석은 isolation level과 별 상관이 없어 보이지만, 해당 엔티티를 사용하는 트랜잭션들의 isolation level을 repeatable read로 보장해야 하는 경우에 유용하게 사용할 수 있을 것 같다.
  • 결국 테스트 코드를 많이 만들어서 테스트해봐야겠다.
top


DB 수준에서 동시접근 이해하기

Hibernate/Chapter 10 : 2008.03.28 10:23


특징

  • 하이버네이트 애플리케이션 개발자로써 반드시 갖춰야 할 역량은 사용하는 DB의 기능과 특정 상황에서 DB isolation 레벨을 어떻게 조정할 수 있을지 이해하는 것이다.
  • 완벽한 격리(Isolation)는 비용이 크다. 그 단계(lebel)를 조정해서 완전한 분리의 정도를 낮추고 시스템의 성능과 확장성을 높일 수 있다.

트랜잭션 격리 이슈

  • 이슈들을 지칭하는 용어는 ANSI SQL 표준에 정의되어 있다.
  • lost update: 두 개의 트랜잭션이 하나의 데이터에 업데이트를 할 때 나중에 업데이트한 트랜잭션이 롤백 되버리면 첫 번째 트랜잭션이 커밋한 데이터까지 날아가 버린다.

사용자 삽입 이미지

  • dirty read: 하나의 트랜잭션이 다른 트랜잭션이 아직 커밋하지 않은 데이터를 읽었을 때 다른 트랜잭션이 롤백 될 수 있기 때문에 이상한 데이터를 읽은 꼴이 될수도 있다.

사용자 삽입 이미지

  • unrepeatable read: 하나의 트랜잭션이 두 번 같은 데이터를 읽어왔는데 그 사이에 다른 트랜잭션이 해당 데이터를 조작하여 그 값이 다를 수 있다.

사용자 삽입 이미지

  • second lost updates problem: unprepeatble read의 한가지 형태로 첫 번째 트랜잭션이 데이터에 쓰기를 하고 커밋을 했는데 다른 트랜잭션이 같은 데이터에 쓰기를 하고 커밋을 하면 앞선 트랜잭션의 결과는 날아가 버린다. 근데 이건 어쩌면 당연한 걸 수도 있다. 나중에 자세히 다룬다.
  • phantom read: 트랜잭션이 쿼리를 두 번 날렸는데 그 사이에 다른 트랜잭션이 데이터를 추가하거나 삭제해서 그 결과가 다를 수 있다.

사용자 삽입 이미지

ANSI 트랜잭션 격리 레벨

Read uncommitted

  • lost update는 막고 dirty read는 허용.
  • 다른 트랜잭션에 의해 커밋되지 않은 데이터에 쓰기 작업을 할 수 없다. 하지만 읽기는 가능하다.
  • exclusive write lock을 사용한다.

Read committed

  • dirty read는 막고 unrepeatable read는 허용.
  • 커밋되지 않은 쓰기 트랜잭션은 다른 트랜잭션들이 해당 레코드에 접근도 못하게 한다. 읽기 쓰기 전부 안 됨. 하지만 읽기 트랜잭션은 다른 트랜잭션이 자신이 읽고 있는 레코드에 (읽기 쓰기 모두) 접근하는 것을 허용한다.
  • shared read lock과 exclusive write lock을 사용한다.

Repeatable read

  • unrepeatable read와 dirty read 둘 다 막는다. phantom read는 허용.
  • 읽기 트랜잭션이 사용하는 데이터에 대한 쓰기 트랜잭션을 막는다. 쓰기 트랜잭션이 사용하는 데이터에 접근하는 다른 모든 트랜잭션을 막는다.

Serializable

  • 트랜잭션들을 일렬로 세워서 차례 차례 실행시키는 것과 같다.
  • low-level lock만 사용해서는 구현하기 어렵다.

격리 수준(Isolation level) 선택하기

  • 다음은 권고 사항이지 돌에 새겨넣은 말 같은 것이 아니다.
  • read uncommitted 격리 수준은 사용하지 말아라.
  • 대부분의 경우 serializable 격리 수준 까지느 필요없다. phantom read가 보통 문제를 일어키지는 않는다. 성능에 심각한 영향을 끼친다.
  • 그러면 read committed와 repeatable read 둘 만 남았네.
  • repeatable read: read lock이 write lock을 막는다. 이 방법 말고 versioned data를 사용하면 하이버네이트가 알아서 해준다. 하이버네이트의 Persistence context cache와 versioning이 repeatable read를 보장해준다. 따라서, versioned data를 사용하기만 하면 모든 DB 트랜잭션이 이 격리 수준을 사용할 수 있다.

격리 수준 설정하기

  • DBMS마다 기본 격리 수준이 있다. 보통 read committed 또는 repeatable read 중 하나다.
  • hibernate.connection.isolation = 4 이렇게 설정할 수 있다.
    • 1—Read uncommitted isolation
    • 2—Read committed isolation
    • 4—Repeatable read isolation
    • 8—Serializable isolation
  • 이렇게 하이버 설정에 설정해버리면 이건 global 한 설정이 된다.
  • 때로는 특정 트랜잭션 마다 설정할 수 있다.

top