在併發環境,一個數據庫系統會同時爲各類各樣的客戶程序提供服務,也就是說,在同一時刻,會有多個客戶程序同時訪問數據庫系統,這多個客戶程序中的失誤訪問數據庫中相同的數據時,若是沒有采起必要的隔離機制,就會致使各類各樣的併發問題的發生,這些併發問題可概括爲如下幾類 java
多個事務併發引發的問題: mysql
1)第一類丟失更新:撤消一個事務時,把其它事務已提交的更新的數據覆蓋了。 sql
2)髒讀:一個事務讀到另外一個事務未提交的更新數據。 數據庫
3) 幻讀:一個事務執行兩次查詢,但第二次查詢比第一次查詢多出了一些數據行。 服務器
4)不可重複讀:一個事務兩次讀同一行數據,但是這兩次讀到的數據不同。 session
5)第二類丟失更新:這是不可重複讀中的特例,一個事務覆蓋另外一個事務已提交的更新數據。 併發
事務隔離級別 app
爲了解決多個事務併發會引起的問題。數據庫系統提供了四種事務隔離級別供用戶選擇。 eclipse
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字段來表示記錄的版本,來達到多用戶同時更改一條數據的衝突
數據庫腳本:
create table studentVersion (id varchar(32),name varchar(32),ver int);
POJO
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; }
Student.hbm.xml
<?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>
Hibernate.cfg.xml
<?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>
測試代碼
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(); } }
測試結果
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應爲這樣
@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; } }
悲觀鎖控制(Pressimistic Locking)
悲觀鎖...他依賴於數據庫機制,在整個過程當中將數據庫鎖定,其餘任何用戶都不能讀取或者修改..通俗一點說,先讀的用戶就一直佔用這個資源,直到結束.這裏的例子,咱們說一個帳戶信息.一共有三個字段,一個id,一個name,還有一個money,表示的是帳戶餘額.很明顯,當一我的在操做這個帳戶的時候,其餘人是不能操做這個帳戶的,不然就會形成數據的不一致.
悲觀鎖的通常實現方式是在應用程序中顯式採用數據庫系統的獨佔鎖來鎖定數據庫資源。在以下幾種方式時可能顯示指定鎖定模式爲LockMode.UPGRADE
1)調用session的get()或load()方法
2)調用session的lock()方法
3)調用Query的setLockMode()方法
實體類
Acount.java
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; } }
Account.hbm.xml
<?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>
上面兩個都沒啥能夠說的,算是最簡單的hibernate實體類和配置文件了...
咱們使用兩個測試方法來模擬兩個用戶.一樣,咱們使用JUnit4
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(); } }
具體的作法是,咱們在test1方法的事務提交前設置一個斷點,而後咱們用debug模式運行.而後,咱們再直接運行test2方法.咱們能夠看到下面這樣
也就是說,後面那個用戶就一直在等待,.只要第一個用戶沒有提交.他就沒法繼續運行....這就是悲觀鎖...
悲觀鎖的缺點顯而易見..他是完全的佔用了這個資源....因此,咱們通常須要用這個來解決短事務,也就是週期比較短的事務..不然,第一個用戶若是一直不操做,後面任何用戶都沒法進行...
通過測試,還有一個結論就是:使用悲觀鎖,session的load方法的延遲加載機制失效
總結:儘管悲觀鎖可以方式丟失更新和不可重複讀之類併發問題的發生的,可是它影響併發性能。所以不建議使用悲觀鎖,儘可能使用樂觀鎖