Hibernate 및 데이터베이스 잠금
1. 잠금을 사용하는 이유는 무엇입니까?
잠금 메커니즘이 존재하는 이유를 이해하려면 먼저 트랜잭션의 개념을 이해해야 합니다.
트랜잭션은 ACID 특성을 가져야 하는 데이터베이스에서의 일련의 관련 작업입니다.
A(원자성): 모두 성공하거나 모두 취소됩니다.
C(일관성): 데이터베이스의 일관성을 유지합니다.
I(격리): 동일한 데이터에 대해 서로 다른 트랜잭션이 작동하는 경우에는 자체 데이터 공간이 있어야 합니다.
D(내구성): 트랜잭션이 성공적으로 종료되면 데이터베이스에 대한 업데이트가 영구적으로 유지되어야 합니다.
저희가 흔히 사용하는 관계형 데이터베이스 RDBMS는 이러한 트랜잭션 특성을 구현합니다. 그 중 원자성,
일관성, 내구성은 모두 로그로 보장됩니다. 격리는 오늘 우리가 집중하고 있는
잠금 메커니즘에 의해 달성됩니다. 이것이 바로 잠금 메커니즘이 필요한 이유입니다.
격리를 차단하고 통제할 수 없다면 어떤 결과가 발생할 수 있나요?
업데이트 손실: 거래 1에서 제출한 데이터를 거래 2에서 덮어썼습니다.
더티 읽기: 트랜잭션 2는 트랜잭션 1의 커밋되지 않은 데이터를 쿼리합니다.
잘못된 읽기: 거래 2는 거래 1이 제출한 새 데이터를 쿼리합니다.
반복 불가능한 읽기: 트랜잭션 2는 트랜잭션 1이 제출한 업데이트된 데이터를 쿼리합니다.
Hibernate 예제를 살펴보겠습니다. 두 스레드는 tb_account 테이블의
에 있는 동일한 데이터 행 col_id=1을 작업하기 위해 두 개의 트랜잭션을 시작합니다. 의 로그가 교차 인쇄됩니다.
package com.cdai.orm.hibernate.annotation; import java.io.Serializable; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "tb_account") public class Account implements Serializable { private static final long serialVersionUID = 5018821760412231859L; @Id @Column(name = "col_id") private long id; @Column(name = "col_balance") private long balance; public Account() { } public Account(long id, long balance) { this.id = id; this.balance = balance; } public long getId() { return id; } public void setId(long id) { this.id = id; } public long getBalance() { return balance; } public void setBalance(long balance) { this.balance = balance; } @Override public String toString() { return "Account [id=" + id + ", balance=" + balance + "]"; } }
package com.cdai.orm.hibernate.transaction; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.cfg.AnnotationConfiguration; import com.cdai.orm.hibernate.annotation.Account; public class DirtyRead { public static void main(String[] args) { final SessionFactory sessionFactory = new AnnotationConfiguration(). addFile("hibernate/hibernate.cfg.xml"). configure(). addPackage("com.cdai.orm.hibernate.annotation"). addAnnotatedClass(Account.class). buildSessionFactory(); Thread t1 = new Thread() { @Override public void run() { Session session1 = sessionFactory.openSession(); Transaction tx1 = null; try { tx1 = session1.beginTransaction(); System.out.println("T1 - Begin trasaction"); Thread.sleep(500); Account account = (Account) session1.get(Account.class, new Long(1)); System.out.println("T1 - balance=" + account.getBalance()); Thread.sleep(500); account.setBalance(account.getBalance() + 100); System.out.println("T1 - Change balance:" + account.getBalance()); tx1.commit(); System.out.println("T1 - Commit transaction"); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); if (tx1 != null) tx1.rollback(); } finally { session1.close(); } } }; // 3.Run transaction 2 Thread t2 = new Thread() { @Override public void run() { Session session2 = sessionFactory.openSession(); Transaction tx2 = null; try { tx2 = session2.beginTransaction(); System.out.println("T2 - Begin trasaction"); Thread.sleep(500); Account account = (Account) session2.get(Account.class, new Long(1)); System.out.println("T2 - balance=" + account.getBalance()); Thread.sleep(500); account.setBalance(account.getBalance() - 100); System.out.println("T2 - Change balance:" + account.getBalance()); tx2.commit(); System.out.println("T2 - Commit transaction"); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); if (tx2 != null) tx2.rollback(); } finally { session2.close(); } } }; t1.start(); t2.start(); while (t1.isAlive() || t2.isAlive()) { try { Thread.sleep(2000L); } catch (InterruptedException e) { } } System.out.println("Both T1 and T2 are dead."); sessionFactory.close(); } }
격리(Isolation)는 신중한 고려가 필요한 문제이고, 잠금에 대한 이해가 필요하다고 볼 수 있다.
T1 - Begin trasaction T2 - Begin trasaction Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ where account0_.col_id=? Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ where account0_.col_id=? T1 - balance=100 T2 - balance=100 T2 - Change balance:0 T1 - Change balance:200 Hibernate: update tb_account set col_balance=? where col_id=? Hibernate: update tb_account set col_balance=? where col_id=? T1 - Commit transaction T2 - Commit transaction Both T1 and T2 are dead.
일반적인 잠금에는 공유 잠금, 업데이트 잠금, 배타적 잠금이 포함됩니다.
1. 공유 잠금: 데이터 읽기 작업에 사용되며 다른 트랜잭션을 동시에 읽을 수 있습니다. 트랜잭션이 select 문을 실행하면
데이터베이스는 자동으로 트랜잭션에 공유 잠금을 할당하여 읽은 데이터를 잠급니다.
업데이트 및 삭제를 실행할 때 데이터베이스가 자동으로 할당됩니다.
3. 업데이트 잠금: 업데이트 작업 중 공유 잠금으로 인한 교착 상태를 방지하는 데 사용됩니다. 예를 들어 트랜잭션 1과 2는 공유 잠금을 유지하고 배타적 잠금을 얻기 위해 기다립니다. 업데이트를 실행할 때 트랜잭션은 먼저 업데이트 잠금을 획득한 다음
업데이트 잠금을 배타적 잠금으로 업그레이드하여 교착 상태를 방지합니다.
또한 이러한 잠금은 데이터베이스의 다양한 개체에 적용될 수 있습니다. 즉, 이러한 잠금은 서로 다른 세부사항을 가질 수 있습니다.
예: 데이터베이스 수준 잠금, 테이블 수준 잠금, 페이지 수준 잠금, 키 수준 잠금, 행 수준 잠금.
그래서 잠금의 종류가 너무 많습니다. 우리는 DBA가 아닙니다.
잠금을 자동으로 추가하고 적절한 시기에 다양한 잠금을 자동으로 업그레이드 및 다운그레이드합니다. 우리가 해야 할 일은
다양한 비즈니스 요구 사항에 따라 격리 수준을 설정하는 방법을 배우는 것입니다.
3. 격리 수준은 어떻게 설정하나요?
일반적으로 데이터베이스 시스템은 사용자가 선택할 수 있는 4가지 트랜잭션 격리 수준을 제공합니다.
1. 직렬화 가능(직렬화 가능): 두 트랜잭션이 동시에 동일한 데이터를 조작하는 경우 트랜잭션 2 멈추고 기다릴 수만 있습니다.
기존 데이터에 대한 업데이트는 볼 수 없습니다.
3. Read Committed(커밋된 데이터 읽기): 트랜잭션 1은 트랜잭션 2에 새로 삽입되고 업데이트된 데이터를 볼 수 있습니다.
4. 커밋되지 않은 읽기(커밋되지 않은 데이터 읽기): 트랜잭션 1은 트랜잭션 2의 커밋되지 않은 삽입 및 업데이트
데이터를 볼 수 있습니다.
데이터베이스가 Read Commited 격리 수준을 채택하면 애플리케이션에서 비관적 잠금 또는 낙관적 잠금을 사용할 수 있습니다.
1. 비관적 잠금: 현재 트랜잭션이 운영하는 데이터는 반드시 다른 트랜잭션에서 접근할 것이라고 가정하므로, 비관적으로 애플리케이션
프로그램에서 배타적 잠금을 지정하여 데이터 리소스를 잠급니다. 다음 형식은 MySQL 및 Oracle에서 지원됩니다.
에 의해 잠긴 데이터를 쿼리, 업데이트 또는 삭제해야 합니다. 거래가 완료될 때까지.
select ... for update
는 더 이상 교차 절단을 실행하지 않습니다. 트랜잭션 2는 데이터를 읽기 전에 트랜잭션 1이 끝날 때까지 기다리므로 최종 col_balance 값은 100입니다. 🎜>.
Hibernate는 SQLServer 2005에 대해 SQL을 실행합니다.
package com.cdai.orm.hibernate.transaction; import org.hibernate.LockMode; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import com.cdai.orm.hibernate.annotation.Account; import com.cdai.orm.hibernate.annotation.AnnotationHibernate; public class UpgradeLock { @SuppressWarnings("deprecation") public static void main(String[] args) { final SessionFactory sessionFactory = AnnotationHibernate.createSessionFactory(); // Run transaction 1 Thread t1 = new Thread() { @Override public void run() { Session session1 = sessionFactory.openSession(); Transaction tx1 = null; try { tx1 = session1.beginTransaction(); System.out.println("T1 - Begin trasaction"); Thread.sleep(500); Account account = (Account) session1.get(Account.class, new Long(1), LockMode.UPGRADE); System.out.println("T1 - balance=" + account.getBalance()); Thread.sleep(500); account.setBalance(account.getBalance() + 100); System.out.println("T1 - Change balance:" + account.getBalance()); tx1.commit(); System.out.println("T1 - Commit transaction"); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); if (tx1 != null) tx1.rollback(); } finally { session1.close(); } } }; // Run transaction 2 Thread t2 = new Thread() { @Override public void run() { Session session2 = sessionFactory.openSession(); Transaction tx2 = null; try { tx2 = session2.beginTransaction(); System.out.println("T2 - Begin trasaction"); Thread.sleep(500); Account account = (Account) session2.get(Account.class, new Long(1), LockMode.UPGRADE); System.out.println("T2 - balance=" + account.getBalance()); Thread.sleep(500); account.setBalance(account.getBalance() - 100); System.out.println("T2 - Change balance:" + account.getBalance()); tx2.commit(); System.out.println("T2 - Commit transaction"); Thread.sleep(500); } catch (Exception e) { e.printStackTrace(); if (tx2 != null) tx2.rollback(); } finally { session2.close(); } } }; t1.start(); t2.start(); while (t1.isAlive() || t2.isAlive()) { try { Thread.sleep(2000L); } catch (InterruptedException e) { } } System.out.println("Both T1 and T2 are dead."); sessionFactory.close(); } }
T1 - Begin trasaction T2 - Begin trasaction Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ with (updlock, rowlock) where account0_.col_id=? Hibernate: select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ with (updlock, rowlock) where account0_.col_id=? T2 - balance=100 T2 - Change balance:0 Hibernate: update tb_account set col_balance=? where col_id=? T2 - Commit transaction T1 - balance=0 T1 - Change balance:100 Hibernate: update tb_account set col_balance=? where col_id=? T1 - Commit transaction Both T1 and T2 are dead.
. 잠금 작업을 관리합니다. 드물게 발생할 수 있는
동시성 문제를 방지하려면 애플리케이션에서 버전 제어를 사용하세요.select account0_.col_id as col1_0_0_, account0_.col_balance as col2_0_0_ from tb_account account0_ with (updlock, rowlock) where account0_.col_id=?
Hibernate에서는 버전 주석을 사용하여 버전 번호 필드를 정의합니다.
将DirtyLock中的Account对象替换成AccountVersion,其他代码不变,执行出现异常。
package com.cdai.orm.hibernate.transaction; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Version; @Entity @Table(name = "tb_account_version") public class AccountVersion { @Id @Column(name = "col_id") private long id; @Column(name = "col_balance") private long balance; @Version @Column(name = "col_version") private int version; public AccountVersion() { } public AccountVersion(long id, long balance) { this.id = id; this.balance = balance; } public long getId() { return id; } public void setId(long id) { this.id = id; } public long getBalance() { return balance; } public void setBalance(long balance) { this.balance = balance; } public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } }
log如下:
T1 - Begin trasaction T2 - Begin trasaction Hibernate: select accountver0_.col_id as col1_0_0_, accountver0_.col_balance as col2_0_0_, accountver0_.col_version as col3_0_0_ from tb_account_version accountver0_ where accountver0_.col_id=? Hibernate: select accountver0_.col_id as col1_0_0_, accountver0_.col_balance as col2_0_0_, accountver0_.col_version as col3_0_0_ from tb_account_version accountver0_ where accountver0_.col_id=? T1 - balance=1000 T2 - balance=1000 T1 - Change balance:900 T2 - Change balance:1100 Hibernate: update tb_account_version set col_balance=?, col_version=? where col_id=? and col_version=? Hibernate: update tb_account_version set col_balance=?, col_version=? where col_id=? and col_version=? T1 - Commit transaction 2264 [Thread-2] ERROR org.hibernate.event.def.AbstractFlushingEventListener - Could not synchronize database state with session org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [com.cdai.orm.hibernate.transaction.AccountVersion#1] at org.hibernate.persister.entity.AbstractEntityPersister.check(AbstractEntityPersister.java:1934) at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2578) at org.hibernate.persister.entity.AbstractEntityPersister.updateOrInsert(AbstractEntityPersister.java:2478) at org.hibernate.persister.entity.AbstractEntityPersister.update(AbstractEntityPersister.java:2805) at org.hibernate.action.EntityUpdateAction.execute(EntityUpdateAction.java:114) at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:268) at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:260) at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:180) at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:321) at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:51) at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:1206) at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:375) at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:137) at com.cdai.orm.hibernate.transaction.VersionLock$2.run(VersionLock.java:93) Both T1 and T2 are dead.
由于乐观锁完全将事务隔离交给数据库来控制,所以事务1和2交叉运行了,事务1提交
成功并将col_version改为1,然而事务2提交时已经找不到col_version为0的数据了,所以
抛出了异常。
Hibernate查询方法比较
Hibernate主要有三种查询方法:
1.HQL (Hibernate Query Language)
和SQL很类似,支持分页、连接、分组、聚集函数和子查询等特性,
但HQL是面向对象的,而不是面向关系数据库中的表。正因查询语句
是面向Domain对象的,所以使用HQL可以获得跨平台的好处,Hibernate
会自动帮我们根据不同的数据库翻译成不同的SQL语句。这在需要支持
多种数据库或者数据库迁移的应用中是十分方便的。
但得到方便的同时,由于SQL语句是由Hibernate自动生成的,所以这不
利于SQL语句的效率优化和调试,当数据量很大时可能会有效率问题,
出了问题也不便于排查解决。
2.QBC/QBE (Query by Criteria/Example)
QBC/QBE是通过组装查询条件或者模板对象来执行查询的。这在需要
灵活地支持许多查询条件自由组合的应用中是比较方便的。同样的问题
是由于查询语句是自由组装的,创建一条语句的代码可能很长,并且
包含许多分支条件,很不便于优化和调试。
3.SQL
Hibernate也支持直接执行SQL的查询方式。这种方式牺牲了Hibernate跨
数据库的优点,手工地编写底层SQL语句,从而获得最好的执行效率,
相对前两种方法,优化和调试方便了一些。
下面来看一组简单的例子。
package com.cdai.orm.hibernate.query; import java.util.Arrays; import java.util.List; import org.hibernate.Criteria; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.AnnotationConfiguration; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.Example; import org.hibernate.criterion.Expression; import com.cdai.orm.hibernate.annotation.Account; public class BasicQuery { public static void main(String[] args) { SessionFactory sessionFactory = new AnnotationConfiguration(). addFile("hibernate/hibernate.cfg.xml"). configure(). addPackage("com.cdai.orm.hibernate.annotation"). addAnnotatedClass(Account.class). buildSessionFactory(); Session session = sessionFactory.openSession(); // 1.HQL Query query = session.createQuery("from Account as a where a.id=:id"); query.setLong("id", 1); List result = query.list(); for (Object row : result) { System.out.println(row); } // 2.QBC Criteria criteria = session.createCriteria(Account.class); criteria.add(Expression.eq("id", new Long(2))); result = criteria.list(); for (Object row : result) { System.out.println(row); } // 3.QBE Account example= new Account(); example.setBalance(100); result = session.createCriteria(Account.class). add(Example.create(example)). list(); for (Object row : result) { System.out.println(row); } // 4.SQL query = session.createSQLQuery( " select top 10 * from tb_account order by col_id desc "); result = query.list(); for (Object row : result) { System.out.println(Arrays.toString((Object[]) row)); } session.close(); } }
Hibernate: select account0_.col_id as col1_0_, account0_.col_balance as col2_0_ from tb_account account0_ where account0_.col_id=? Account [id=1, balance=100] Hibernate: select this_.col_id as col1_0_0_, this_.col_balance as col2_0_0_ from tb_account this_ where this_.col_id=? Account [id=2, balance=100] Hibernate: select this_.col_id as col1_0_0_, this_.col_balance as col2_0_0_ from tb_account this_ where (this_.col_balance=?) Account [id=1, balance=100] Account [id=2, balance=100] Hibernate: select top 10 * from tb_account order by col_id desc [2, 100] [1, 100]
从log中可以清楚的看到Hibernate对于生成的SQL语句的控制,具体选择
哪种查询方式就要看具体应用了。
更多Java的Hibernate框架数据库操作中锁的使用和查询类型相关文章请关注PHP中文网!