Verrous de mise en veille prolongée et de base de données
1. Pourquoi utiliser des verrous ?
Pour comprendre pourquoi le mécanisme de verrouillage existe, vous devez d'abord comprendre le concept de transactions.
Une transaction est une série d'opérations liées sur la base de données, qui doivent avoir des caractéristiques ACID :
A (atomicité) : soit toutes réussissent, soit toutes sont révoquées.
C (Cohérence) : Pour maintenir la cohérence de la base de données.
I (Isolement) : Lorsque différentes transactions opèrent sur les mêmes données, elles doivent disposer de leur propre espace de données.
D (Durabilité) : Une fois qu'une transaction se termine avec succès, les mises à jour qu'elle apporte à la base de données doivent être conservées en permanence.
Notre SGBDR de base de données relationnelle couramment utilisé implémente ces caractéristiques de transactions. Parmi eux, l'atomicité, la
consistance et la durabilité sont toutes garanties par les bûches. L'isolement est obtenu par le mécanisme de verrouillage
sur lequel nous nous concentrons aujourd'hui. C'est pourquoi nous avons besoin du mécanisme de verrouillage.
S'il n'y a pas de verrouillage et pas de contrôle de l'isolement, quelles sont les conséquences possibles ?
Mise à jour perdue : les données soumises par la transaction 1 ont été écrasées par la transaction 2.
Lecture sale : la transaction 2 interroge les données non validées de la transaction 1.
Fausse lecture : la transaction 2 interroge les nouvelles données soumises par la transaction 1.
Lecture non répétable : la transaction 2 interroge les données mises à jour soumises par la transaction 1.
Regardons l'exemple Hibernate. Deux threads démarrent deux transactions pour exploiter la même ligne de données col_id=1 dans la table tb_account
.
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(); } }
La transaction 1 diminue col_balance de 100, et la transaction 2 le diminue de 100, le résultat final peut être 0, ou
il peut être 200, une mise à jour de la transaction 1 ou 2 peut perdre. La sortie du journal le confirme également. Les journaux des transactions 1 et 2
sont imprimés de manière croisée.
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.
On voit que l'isolement est une question qui nécessite un examen attentif, et il est nécessaire de comprendre les verrous.
2. Combien de types de serrures existe-t-il ?
Les plus courants incluent les verrous partagés, les verrous de mise à jour et les verrous exclusifs.
1. Verrouillage partagé : utilisé pour les opérations de lecture de données, permettant à d'autres transactions de lire en même temps. Lorsqu'une transaction exécute une instruction select,
la base de données alloue automatiquement un verrou partagé à la transaction pour verrouiller les données lues.
2. Verrou exclusif : utilisé pour modifier les données, les autres transactions ne peuvent pas les lire ou les modifier. La base de données allouera automatiquement lorsque la transaction exécute l'insertion, la
mise à jour et la suppression.
3. Verrouillage de mise à jour : utilisé pour éviter les blocages causés par les verrous partagés lors des opérations de mise à jour. Par exemple, les transactions 1 et 2 détiennent
verrous partagés en même temps et attendent d'obtenir des verrous exclusifs. Lors de l'exécution de la mise à jour, la transaction obtient d'abord le verrou de mise à jour, puis met à niveau le verrou de mise à jour
vers un verrou exclusif, évitant ainsi les blocages.
De plus, ces verrous peuvent être appliqués à différents objets de la base de données, c'est-à-dire que ces verrous peuvent avoir différentes granularités.
Tels que les verrous au niveau de la base de données, les verrous au niveau de la table, les verrous au niveau de la page, les verrous au niveau de la clé et les verrous au niveau de la ligne.
Il existe donc de nombreux types de verrous. Il est trop difficile de maîtriser pleinement et d'utiliser autant de verrous de manière flexible. Nous ne sommes pas des administrateurs de base de données.
Que faire ? Heureusement, le mécanisme de verrouillage est transparent pour nous, les utilisateurs ordinaires. La base de données ajoutera automatiquement les
verrous appropriés, et mettra automatiquement à niveau et rétrogradera divers verrous au moment approprié ! Tout ce que nous devons faire est d'
apprendre à définir le niveau d'isolement en fonction des différents besoins de l'entreprise.
3. Comment régler le niveau d'isolement ?
De manière générale, le système de base de données fournira quatre niveaux d'isolation des transactions parmi lesquels les utilisateurs pourront choisir :
1 Sérialisable (sérialisable) : lorsque deux transactions manipulent les mêmes données en même temps, transaction. 2 Je ne peux que m'arrêter et attendre.
2. Lecture répétable : la transaction 1 peut voir les données nouvellement insérées de la transaction 2, mais ne peut pas voir les mises à jour des
données existantes.
3. Lecture validée (lecture des données validées) : la transaction 1 peut voir les données nouvellement insérées et mises à jour de la transaction 2.
4. Lire non validée (lire les données non validées) : la transaction 1 peut voir l'insertion non validée et mettre à jour les
données de la transaction 2.
4. Verrous dans l'application
Lorsque la base de données adopte le niveau d'isolement Read Commited, un verrouillage pessimiste ou optimiste peut être utilisé dans l'application.
1. Verrouillage pessimiste : on suppose que les données exploitées par la transaction en cours seront certainement accessibles par d'autres transactions, donc de manière pessimiste, spécifiez un verrou exclusif dans le programme d'application
pour verrouiller les ressources de données. Les formulaires suivants sont pris en charge dans MySQL et Oracle :
select ... for update
permet explicitement à select d'utiliser un verrou exclusif pour verrouiller les enregistrements interrogés. Les autres transactions doivent interroger, mettre à jour ou supprimer les données verrouillées par
. jusqu'à ce que la transaction soit complétée.
Dans Hibernate, vous pouvez passer LockMode.UPGRADE lors du chargement pour utiliser le verrouillage pessimiste. Modifiez l'exemple précédent,
transmettez un paramètre LockMode supplémentaire lors de l'appel de la méthode get des transactions 1 et 2. Comme le montre le journal, les transactions 1 et 2
ne s'exécutent plus de manière croisée. La transaction 2 attend la fin de la transaction 1 avant de lire les données, donc la valeur finale de col_balance est de 100, ce qui est correct
.
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.
Hibernate exécutera SQL pour SQLServer 2005 :
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=?
Ajouter un verrou de ligne et mettre à jour le verrou à la ligne de données sélectionnée avec col_id 1.
2. Verrouillage optimiste : on suppose que les données exploitées par la transaction en cours ne seront pas accessibles par d'autres transactions en même temps, elle repose donc entièrement sur le niveau d'isolement de la base de données
pour automatiquement gérer le travail de serrure. Utilisez le contrôle de version dans votre application pour éviter les problèmes de concurrence qui peuvent
se produire rarement.
Dans Hibernate, utilisez l'annotation Version pour définir le champ du numéro de version.
将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中文网!