多個事務併發引起的問題:
1)第一類丟失更新:撤消一個事務時,把其它事務已提交的更新的數據覆蓋了。
2)髒讀:一個事務讀到另一個事務未提交的更新數據。
3) 幻讀:一個事務執行兩次查詢,但第二次查詢比第一次查詢多出了一些數據行。
4)不可重複讀:一個事務兩次讀同一行數據,可是這兩次讀到的數據不一樣。
5)第二類丟失更新:這是不可重複讀中的特例,一個事務覆蓋另一個事務已提交的更新數據。
事務隔離級別
爲了解決多個事務併發會引發的問題。數據庫系統提供了四種事務隔離級別供用戶選擇。
1) Serializable:串行化。隔離級別最高
2) Repeatable Read:可重複讀。--MySQL默認是這個
3) Read Committed:讀已提交數據。--Oracle默認是這個
4) Read Uncommitted:讀未提交數據。隔離級別最差。--sql server默認是這個
數據庫系統採用不同的鎖類型來實現以上四種隔離級別,具體的實現過程對用戶是透明的。用戶應該關心的是如何選擇合適的隔離級別。
對於多數應用程序,可以優先考慮把數據庫系統的隔離級別設爲Read Committed,它能夠避免髒讀,而且具有較好的併發性能。
每個數據庫連接都有一個全局變量@@tx_isolation,表示當前的事務隔離級別。JDBC數據庫連接使用數據庫系統默認的隔離級別。在Hibernate的配置文件中可以顯示地設置隔離級別。每一種隔離級別對應着一個正整數。
Read Uncommitted: 1
Read Committed: 2
Repeatable Read: 4
Serializable: 8
在hibernate.cfg.xml中設置隔離級別如下:
<session-factory>
<!-- 設置JDBC的隔離級別 -->
<property name="hibernate.connection.isolation">2</property>
</session-factory>
設置之後,在開始一個事務之前,Hibernate將爲從連接池中獲得的JDBC連接設置級別。需要注意的是,在受管理環境中,如果Hibernate使用的數據庫連接來自於應用服務器提供的數據源,Hibernate不會改變這些連接的事務隔離級別。在這種情況下,應該通過修改應用服務器的數據源配置來修改隔離級別。
併發控制
當數據庫系統採用Red Committed隔離級別時,會導致不可重複讀和第二類丟失更新的併發問題,在可能出現這種問題的場合。可以在應用程序中採用悲觀鎖或樂觀鎖來避免這類問題。
樂觀鎖(Optimistic Locking):
樂觀鎖假定當前事務操縱數據資源時,不會有其他事務同時訪問該數據資源,因此不作數據庫層次上的鎖定。爲了維護正確的數據,樂觀鎖使用應用程序上的版本控制(由程序邏輯來實現的)來避免可能出現的併發問題。
唯一能夠同時保持高併發和高可伸縮性的方法就是使用帶版本化的樂觀併發控制。版本檢查使用版本號、 或者時間戳來檢測更新衝突(並且防止更新丟失)。
三種方式。
1)Version版本號
2)時間戳
3)自動版本控制。
這裏不建議在新的應用程序中定義沒有版本或者時間戳列的版本控制:它更慢,更復雜,如果你正在使用脫管對象,它則不會生效。
通過在表中及POJO中增加一個version字段來表示記錄的版本,來達到多用戶同時更改一條數據的衝突
數據庫腳本:
- <span style="font-size: large;"><span style="font-size: large;">create table studentVersion (id varchar(32),name varchar(32),ver int); </span></span>
POJO
- <span style="font-size: large;"><span style="font-size: large;">package Version;
- public class Student {
- private String id;
- private String name;
- private int version;
- public String getId() {
- return id;
- }
- public void setId(String id) {
- this.id = id;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public int getVersion() {
- return version;
- }
- public void setVersion(int version) {
- this.version = version;
- }
- </span></span>
Student.hbm.xml
- <span style="font-size: large;"><span style="font-size: large;"><?xml version="1.0" encoding="utf-8"?>
- <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
- "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
- <!--
- Mapping file autogenerated by MyEclipse - Hibernate Tools
- -->
- <hibernate-mapping>
- <class name="Version.Student" table="studentVersion" >
- <id name="id" unsaved-value="null">
- <generator class="uuid.hex"></generator>
- </id>
- <!--version標籤必須跟在id標籤後面-->
- <version name="version" column="ver" type="int"></version>
- <property name="name" type="string" column="name"></property>
- </class>
- </hibernate-mapping>
- </span></span>
Hibernate.cfg.xml
- <span style="font-size: large;"><span style="font-size: large;"><?xml version='1.0' encoding='UTF-8'?>
- <!DOCTYPE hibernate-configuration PUBLIC
- "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
- "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
- <!-- Generated by MyEclipse Hibernate Tools. -->
- <hibernate-configuration>
- <session-factory>
- <property name="connection.username">root</property>
- <property name="connection.url">
- jdbc:mysql://localhost:3306/schoolproject?characterEncoding=gb2312&useUnicode=true
- </property>
- <property name="dialect">
- org.hibernate.dialect.MySQLDialect
- </property>
- <property name="myeclipse.connection.profile">mysql</property>
- <property name="connection.password">1234</property>
- <property name="connection.driver_class">
- com.mysql.jdbc.Driver
- </property>
- <property name="hibernate.dialect">
- org.hibernate.dialect.MySQLDialect
- </property>
- <property name="hibernate.show_sql">true</property>
- <property name="current_session_context_class">thread</property>
- <property name="jdbc.batch_size">15</property>
- <mapping resource="Version/Student.hbm.xml" />
- </session-factory>
- </hibernate-configuration> </span></span>
測試代碼
- <span style="font-size: large;"><span style="font-size: large;">package Version;
- import java.io.File;
- import java.util.Iterator;
- import java.util.Set;
- import org.hibernate.Session;
- import org.hibernate.SessionFactory;
- import org.hibernate.Transaction;
- import org.hibernate.cfg.Configuration;
- public class Test {
- public static void main(String[] args) {
- String filePath=System.getProperty("user.dir")+File.separator+"src/Version"+File.separator+"hibernate.cfg.xml";
- File file=new File(filePath);
- System.out.println(filePath);
- SessionFactory sessionFactory=new Configuration().configure(file).buildSessionFactory();
- Session session=sessionFactory.openSession();
- Transaction t=session.beginTransaction();
- Student stu=new Student();
- stu.setName("tom11");
- session.save(stu);
- t.commit();
- /*
- * 模擬多個session操作student數據表
- */
- Session session1=sessionFactory.openSession();
- Session session2=sessionFactory.openSession();
- Student stu1=(Student)session1.createQuery("from Student s where s.name='tom11'").uniqueResult();
- Student stu2=(Student)session2.createQuery("from Student s where s.name='tom11'").uniqueResult();
- //這時候,兩個版本號是相同的
- System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());
- Transaction tx1=session1.beginTransaction();
- stu1.setName("session1");
- tx1.commit();
- //這時候,兩個版本號是不同的,其中一個的版本號遞增了
- System.out.println("v1="+stu1.getVersion()+"--v2="+stu2.getVersion());
- Transaction tx2=session2.beginTransaction();
- stu2.setName("session2");
- tx2.commit();
- }
- }
- </span></span>
測試結果
Hibernate: insert into studentVersion (ver, name, id) values (?, ?, ?)
Hibernate: select student0_.id as id0_, student0_.ver as ver0_, student0_.name as name0_ from studentVersion student0_ where student0_.name='tom11'
Hibernate: select student0_.id as id0_, student0_.ver as ver0_, student0_.name as name0_ from studentVersion student0_ where student0_.name='tom11'
v1=0--v2=0
Hibernate: update studentVersion set ver=?, name=? where id=? and ver=?
v1=1--v2=0
Hibernate: update studentVersion set ver=?, name=? where id=? and ver=?
Exception in thread "main" org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect):
[Version.Student#4028818316cd6b460116cd6b50830001]
可以看到,第二個“用戶”session2修改數據時候,記錄的版本號已經被session1更新過了,所以拋出了紅色的異常,我們可以在實際應用中處理這個異常,例如在處理中重新讀取數據庫中的數據,同時將目前的數據與數據庫中的數據展示出來,讓使用者有機會比較一下,或者設計程序自動讀取新的數據
注意:
要注意的是,由於樂觀鎖定是使用系統中的程式來控制,而不是使用資料庫中的鎖定機制,因而如果有人特意自行更新版本訊息來越過檢查,則鎖定機制就會無效,例如在上例中自行更改stu的version屬性,使之與資料庫中的版本號相同的話就不會有錯誤,像這樣版本號被更改,或是由於資料是由外部系統而來,因而版本資訊不受控制時,鎖定機制將會有問題,設計時必須注意。
如果手工設置stu.setVersion()自行更新版本以跳過檢查,則這種樂觀鎖就會失效,應對方法可以將Student.java的setVersion設置成private
如果是註解方式的,POJO應爲這樣
- <span style="font-size: large;"><span style="font-size: large;">@Entity
- @Table(name="student ")
- public class Student {
- @Id @GeneratedValue
- private Integer id;
- private String name;
- private Integer version;
- public Integer getId() {
- return id;
- }
- public void setId(Integer id) {
- this.id = id;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public Integer getVersion() {
- return version;
- }
- public void setVersion(Integer version) {
- this.version = version;
- }
- }</span></span>
悲觀鎖控制(Pressimistic Locking)
悲觀鎖...他依賴於數據庫機制,在整個過程中將數據庫鎖定,其他任何用戶都不能讀取或者修改..通俗一點說,先讀的用戶就一直佔用這個資源,直到結束.這裏的例子,我們說一個賬戶信息.一共有三個字段,一個id,一個name,還有一個money,表示的是賬戶餘額.很明顯,當一個人在操作這個賬戶的時候,其他人是不能操作這個賬戶的,否則就會造成數據的不一致.
悲觀鎖的一般實現方式是在應用程序中顯式採用數據庫系統的獨佔鎖來鎖定數據庫資源。在如下幾種方式時可能顯示指定鎖定模式爲LockMode.UPGRADE
1)調用session的get()或load()方法
2)調用session的lock()方法
3)調用Query的setLockMode()方法
實體類
Acount.java
- <span style="font-size: large;"><span style="font-size: large;">package com.test.model;
- public class Acount
- {
- private int id;
- private String name;
- private int money;
- public int getId()
- {
- return id;
- }
- public void setId(int id)
- {
- this.id = id;
- }
- public String getName()
- {
- return name;
- }
- public void setName(String name)
- {
- this.name = name;
- }
- public int getMoney()
- {
- return money;
- }
- public void setMoney(int money)
- {
- this.money = money;
- }
- }</span></span>
Account.hbm.xml
- <span style="font-size: large;"><span style="font-size: large;"><?xml version="1.0" encoding="utf-8"?>
- <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
- "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
- <!--
- Mapping file autogenerated by MyEclipse Persistence Tools
- -->
- <hibernate-mapping package="com.test.model">
- <class name="Acount" table="Acount" >
- <id name="id">
- <generator class="native"></generator>
- </id>
- <property name="name"></property>
- <property name="money"></property>
- </class>
- </hibernate-mapping></span></span>
上面兩個都沒啥可以說的,算是最簡單的hibernate實體類和配置文件了...
我們使用兩個測試方法來模擬兩個用戶.同樣,我們使用JUnit4
- <span style="font-size: large;"><span style="font-size: large;">package com.test.junit;
- import org.hibernate.Session;
- import org.hibernate.Transaction;
- import org.junit.Test;
- import org.hibernate.LockMode;
- import com.test.model.Acount;
- import com.test.util.HibernateSessionFactory;
- public class extendsTest
- {
- @Test
- public void test1()
- {
- Session session = HibernateSessionFactory.getSession();
- Transaction tx = session.beginTransaction();
- Acount acount = (Acount)session.load(Acount.class, 1,LockMode.UPGRADE);//注意,這裏的最後那個參數..他將鎖定這個操作.
- System.out.println(acount.getName());
- System.out.println(acount.getMoney());
- acount.setMoney(acount.getMoney() - 20000);
- tx.commit();
- session.close();
- }
- @Test
- public void test2()
- {
- Session session = HibernateSessionFactory.getSession();
- Transaction tx = session.beginTransaction();
- Acount acount = (Acount)session.load(Acount.class, 1,LockMode.UPGRADE);
- System.out.println(acount.getName());
- System.out.println(acount.getMoney());
- acount.setMoney(acount.getMoney() - 20000);
- tx.commit();
- session.close();
- }
- }</span></span>
具體的做法是,我們在test1方法的事務提交前設置一個斷點,然後我們用debug模式運行.然後,我們再直接運行test2方法.我們可以看到下面這樣
也就是說,後面那個用戶就一直在等待,.只要第一個用戶沒有提交.他就無法繼續運行....這就是悲觀鎖...
悲觀鎖的缺點顯而易見..他是徹底的佔用了這個資源....所以,我們一般需要用這個來解決短事務,也就是週期比較短的事務..否則,第一個用戶如果一直不操作,後面任何用戶都無法進行...
經過測試,還有一個結論就是:使用悲觀鎖,session的load方法的延遲加載機制失效
總結:儘管悲觀鎖能夠方式丟失更新和不可重複讀之類併發問題的發生的,但是它影響併發性能。因此不建議使用悲觀鎖,儘量使用樂觀鎖