1. 程式人生 > >事務的樂觀鎖和悲觀鎖

事務的樂觀鎖和悲觀鎖

Select …forupdate語句是我們經常使用手工加鎖語句。通常情況下,select語句是不會對資料加鎖,妨礙影響其他的DML和DDL操作。同時,在多版本一致讀機制的支援下,select語句也不會被其他型別語句所阻礙。

藉助for update子句,我們可以在應用程式的層面手工實現資料加鎖保護操作。本篇我們就來介紹一下這個子句的用法和功能。

從for update子句的語法狀態圖中,我們可以看出該子句分為兩個部分:加鎖範圍子句和加鎖行為子句。下面我們分別針對兩個方面的進行介紹。

加鎖範圍子句

在select…for update之後,可以使用of子句選擇對select的特定資料表進行加鎖操作。預設情況下,不使用of子句表示在select所有的資料表中加鎖。

//採用預設格式for update

SQL> select * from emp where rownum<2 for update;

此時,我們觀察vlockvlocked_object檢視,可以看到鎖資訊。

//事務資訊檢視

SQL> select addr,xidusn,xidslot,xidsqn from v$transaction;

//鎖物件資訊

SQL> select xidusn,xidslot,xidsqn,object_id,session_id, oracle_username from v$locked_object;

SQL> select owner,object_name from dba_objects where object_id=73181;

SQL> select addr, sid, type, id1,id2,lmode, request, block from v$lock where sid=36;

從上面的情況看,預設情況下的for update語句,效果相當於啟動了一個會話級別的事務,在對應的資料表(select所涉及的所有資料表)上加入一個數據表級共享鎖(TM,lmode=3)。同時,在對應的資料行中加入獨佔鎖(TX,lmode=6)。

根據我們以前的知識,如果此時有另一個會話檢視獲取對應資料行的獨佔權限(無論是用update/delete還是另一個for update),都會以block而告終。

SQL> select sid from v$mystat where rownum<2;

SQL> select * from emp where empno=7369 for update;

//系統blocking

此時系統中狀態,切換到另一個使用者下進行觀察:

SQL> select addr, sid, type, id1,id2,lmode, request, block from v$lock where sid in (36,37);

ADDR SID TYPE ID1 ID2 LMODE REQUEST BLOCK

SQL> select * from dba_waiters;

WAITING_SESSION HOLDING_SESSION LOCK_TYPE MODE_HELD MODE_REQUESTED LOCK_ID1 LOCK_ID2

由此,我們可以獲取到結論:for update子句的預設行為就是自動啟動一個事務,藉助事務的鎖機制將資料進行鎖定。

Of子句是配合for update語句使用的一個範圍說明標記。從官方的語法結構看,後面可以跟一個或者多個數據列列表。這種語法場景常常使用在進行連線查詢的select中,對其中一張資料表資料進行鎖定。

SQL> select empno,ename,job,mgr,sal from emp,dept where emp.deptno=dept.deptno and empno=7369 for update of emp.empno;

SQL> select addr, sid, type, id1,id2,lmode, request, block from v$lock where sid=36;

ADDR SID TYPE ID1 ID2 LMODE REQUEST BLOCK

上面的語句中,我們看到使用for update of指定資料列之後,鎖定的範圍限制在了所在的資料表。也就是說,當我們使用連線查詢配合of子句的時候,可以實現有針對性的鎖定。

同樣在連線查詢的時候,如果沒有of子句,同樣採用預設的模式,會如何呢?

SQL> select empno,ename,job,mgr,sal from emp,dept where emp.deptno=dept.deptno and empno=7369 for update;

SQL> select owner,object_name from dba_objects where object_id=73179;

明顯可以看到,當我們沒有使用of子句的時候,預設就是對所有select的資料表進行lock操作。

加鎖行為子句

加鎖行為子句相對比較容易理解。這裡分別介紹。
Nowait子句

當我們進行for update的操作時,與普通select存在很大不同。一般select是不需要考慮資料是否被鎖定,最多根據多版本一致讀的特性讀取之前的版本。加入for update之後,Oracle就要求啟動一個新事務,嘗試對資料進行加鎖。如果當前已經被加鎖,預設的行為必然是block等待。

使用nowait子句的作用就是避免進行等待,當發現請求加鎖資源被鎖定未釋放的時候,直接報錯返回。

///session1中

SQL> select * from emp for update;

//變換session,進行執行。

SQL> select * from emp for update nowait;

select * from emp for update nowait

ORA-00054:資源正忙,但指定以NOWAIT方式獲取資源,或者超時失效

對應的還有就是wait子句,也就是預設的for update行為。一旦發現對應資源被鎖定,就等待blocking,直到資源被釋放或者使用者強制終止命令。

對wait子句還存在一個數據引數位,表示當出現blocking等待的時候最多等待多長時間。單位是秒級別。

//接上面的案例

SQL> select * from emp for update wait 3;

select * from emp for update wait 3

ORA-30006:資源已被佔用;執行操作時出現WAIT超時

Skip locked引數

Skip locked引數是最新引入到for update語句中的一個引數。簡單的說,就是在對資料行進行加鎖操作時,如果發現數據行被鎖定,就跳過處理。這樣for update就只針對未加鎖的資料行進行處理加鎖。

//session1中,對一部分資料加鎖;

SQL> select * from emp where rownum<4 for update;

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO

//在session2中;

SQL> select * from emp for update skip locked;

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO

總資料一共14行。Session1中,先lock住了3行資料。之後的seesion2中,由於使用的skip locked子句引數,將剩下的11條資料進行讀取到並且加鎖。

對for update的使用

在日常中,我們對for update的使用還是比較普遍的,特別是在如pl/sql developer中手工修改資料。此時只是覺得方便,而對for update真正的含義缺乏理解。

For update是Oracle提供的手工提高鎖級別和範圍的特例語句。Oracle的鎖機制是目前各型別資料庫鎖機制中比較優秀的。所以,Oracle認為一般不需要使用者和應用直接進行鎖的控制和提升。甚至認為死鎖這類鎖相關問題的出現場景,大都與手工提升鎖有關。所以,Oracle並不推薦使用for update作為日常開發使用。而且,在平時開發和運維中,使用了for update卻忘記提交,會引起很多鎖表故障。

那麼,什麼時候需要使用for update?就是那些需要業務層面資料獨佔時,可以考慮使用for update。場景上,比如火車票訂票,在螢幕上顯示郵票,而真正進行出票時,需要重新確定一下這個資料沒有被其他客戶端修改。所以,在這個確認過程中,可以使用for update。這是統一的解決方案方案問題,需要前期有所準備

Hibernate中的樂觀鎖和悲觀鎖

在瞭解Hibernate的LockMode之前,我們先講一下LockMode是什麼東西?其實LockMode只是在使用Hibernate 中 的session.load()載入資料時指定的模式,也叫悲觀鎖(模式),然而,悲觀鎖是為了彌補read-committed 機制的不足,從而解決non-repeatable (不可重複讀)和 phantom-read (幻讀)問題,而non-repeatable 和 phantom-read 這兩個問題也只是事務併發 是產生的兩種問題…
看了我寫的這一段後,我相信很多讀者會有點懵,這就對了,看完下面的文章,再後過頭來讀這一段,就全都明白了。

我們知道,事務由那幾個特性,四個(ACID):

1.原子性(Atomicity):
整個事務中的所有操作,要麼全部完成,要麼全部不完成,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣。

2.一致性(Consistency)
在事務開始之前和事務結束以後,資料庫的完整性約束沒有被破壞。

3.隔離性(Isolation)
兩個事務的執行是互不干擾的,一個事務不可能看到其他事務執行時,中間某一時刻的資料。

4.永續性(Durability)
  在事務完成以後,該事務所對資料庫所作的更改便持久的儲存在資料庫之中,並不會被回滾。

由於一項操作通常會包含許多子操作,而這些子操作可能會因為硬體的損壞或其他因素產生問題,要正確實現ACID並不容易。ACID建議資料庫將所有需要更新以及修改的資料一次操作完畢,但實際上並不可行。

   一個支援事務(Transaction)的資料庫系統,必需要具有這四種特性,否則在事務過程(Transaction processing)當中無法保證資料的正確性,交易過程極可能達不到交易方的要求。

在處理事務過程中,事務併發是不可避免的,從而會出現以下幾個問題:

1.丟失更新(Lost Update)(第一類和第二類)
2.髒讀(Dirty Read)
3.不可重複讀(Non-repeatable Read)
4.幻讀(Phantom Read)

針對併發事務出現的問題,Hibernate 採用了資料庫的事務隔離機制(詳細文件見Herbernate 參考文件的 java.sql.Connection ),一般有以下四種處理機制:

1.read-uncommitted
2.read-committed
3.repeatable read
4.serializable

四種機制的具體價值:

A. 只要資料庫支援事務,就不可能出現第一類丟失更新
B. read-uncommitted(允許讀取未提交的資料)會出現dirty read , phantom-read,non-repeatable read 問題
C. read-committed(讀取已提交的資料,專案中一般使用這個,MySql 資料庫預設是這種機制)不會出現dirty read ,因為只有另一個事務提交才會讀出來結果,但仍然會出現non-repeatable和phantom-read

(所以使用read-committed 機制可用悲觀鎖和樂觀鎖來解決non-repeatable 和 phantom-read 問題)

在hibernate中的事務隔離級別的設定(使用 hibernate.connection.isolation配置,取之1,2,4,8)

1.hibernate.connection.isolation = 2 (如果不設定,預設依賴資料庫本身的級別,例如MySql為2,read-committed)
2.使用悲觀鎖解決repeatable read 的問題(依賴於資料庫的鎖)

a. 使用悲觀鎖查詢資料或者更新資料時,預設加上for update ,表示對當前事務加鎖,其他事務暫時不能使用,例如select … for update
b. 在程式中如何加上悲觀鎖呢,很簡單,直接在session的load方法中新增一個引數即可,例如:

Account a = (Account)session.load(Account.class, 1, LockMode.UPGRADE);

鎖的模式:

a.LockMode.None 無鎖的機制,Transaction介紹時,切換到此模式
b.LockMode.read 在查詢的時候hibernate會自動獲取鎖
c.LockMode.write insert update hibernate會自動獲取鎖
d.以上3種鎖的模式,是hibernate內部使用的(不需要設定)
e.LockMode.UPGRADE_NOWAIT 是Oracle 支援的索的模式

3.使用樂觀鎖解決repeatable read 的問題

直接在實體類中新增 version 屬性(資料庫也會對應生成該欄位,初始值為0),並在其get 方法前加 @version 註解(這裡介紹註解方式配置),則在操作過程中每更新一次改行資料則在 version 值上加1,即可在事務提交前判斷該資料是否被其他事務修改過

D. repeatable read (事務執行中其他事務無法執行修改或插入操作 較安全)
E. serializable 解決事務隔離級別(順序執行事務,不併發,實際當中很少使用)

悲觀鎖

在應用程式中顯示地為資料資源加鎖.悲觀鎖假定當前事務操縱資料資源時,肯定還會有其它事務同時訪問該資料資源,為了避免當前事務的操作受到干擾,先鎖定資源.儘管悲觀鎖能防止丟失更新和不可重複讀這類併發問題,但會影響併發效能.(簡單理解,就是每次在操作資料時總是悲觀地認為會有別的事務也會來操縱同一資料,從此鎖住該筆資料,直到自己操作完成後再解除鎖)

樂觀鎖

假定當前事務操縱資料資源時,不會有其它事務同時訪問該資料資源,因此完全依靠資料庫的隔離級別來自動管理鎖的工作.應用程式採用版本控制手段來避免可能出現的併發問題.(所謂樂觀鎖,它通常認為多個事務同時操縱同一資料的情況是很少的,因為根本不做資料庫層次上的鎖定,只是基於資料的版本標識實現應用程式級別上的鎖定機制,既保證了多個事務的併發訪問,又有效地防止了第二類丟失更新的出現)

LockMode類表示的幾種鎖定模式

鎖定模式

描述

LockMode.NONE

如果快取中存在物件,直接返回該物件的引用,否則通過select語句到資料庫中載入該物件,預設值.
LockMode.READ

不管快取中是否存在物件,總是通過select語句到資料庫中載入該物件,如果對映檔案中設定了版本元素,就執行版本檢查,比較快取中的物件是否和資料庫中物件版本一致

LockMode.UPGRADE

不管快取中是否存在物件,總是通過select語句到資料庫中載入該物件,如果對映檔案中設定了版本元素,就執行版本檢查,比較快取中的物件是否和資料庫中物件的版本一致,如果資料庫系統支援悲觀鎖(如Oracle/MySQL),就執行select…for update語句,如果不支援(如Sybase),執行普通select語句

LockMode.UPGRADE_NOWAIT

和LockMode.UPGRADE具有同樣功能,此外,對於Oracle等支援update nowait的資料庫,執行select…for update nowait語句,nowait表明如果執行該select語句的事務不能立即獲得悲觀鎖,那麼不會等待其它事務釋放鎖,而是立刻丟擲鎖定異常

LockMode.WRITE

儲存物件時會自動使用這種鎖定模式,僅供Hibernate內部使用,應用程式中不應該使用它

LockMode.FORCE

強制更新資料庫中物件的版本屬性,從而表明當前事務已經更新了這個物件

多個事務併發執行時的併發問題

第一類丟失更新:撤銷一個事務時,把其它事務已提交的更新資料覆蓋.
第二類丟失更新:不可重複讀中的特例,一個事務覆蓋另一事務已提交的更新資料.
髒讀:一個事務讀到另一事務未提交的更新資料.
幻讀:一個事務讀到另一事務已提交的新插入的資料.
不可重複讀:一個事務讀到另一個事物已提交的更新資料.

鎖的型別和相容性

共享鎖
l加鎖條件:當一個事務執行select語句時,資料庫系統會為這個事務分配一把共享鎖,鎖定被查詢的資料.
l解鎖條件:資料被讀取後,資料庫系統立即解除共享鎖.
l與其它鎖的相容性:如果資料資源上放置了共享鎖,還能再放置共享鎖和更新鎖.
l併發效能:良好的併發效能.當多個事務讀相同資料時,每個事務都會獲得一把共享鎖,可以同時讀鎖定的資料.

獨佔鎖
l加鎖條件:當一個事務執行insert,update,delete時,資料庫系統會自動對SQL語句操縱的資料資源使用獨佔鎖.如果該資料資源已經有其它鎖存在時,無法對其再放置獨佔鎖.
l解鎖條件:獨佔鎖一直到事務結束後才能被解除.
l與其它鎖的相容性:獨佔鎖不能和其他鎖相容,如果資料資源已經加上了獨佔鎖, 就不能再放置其它鎖,同樣,如果已經有了其它鎖,就不能放置獨佔鎖.
l併發效能:併發效能較差,只允許有一個事務訪問鎖定的資料,如果其他事務也需要訪問該資料,就必須等待,直到前一個事務結束,解除了獨佔鎖,其它事務才能訪問該資料.

更新鎖
l加鎖條件:當一個事務進行update操作時,資料庫系統會先為事務分配一把更新鎖.
l解鎖條件:當讀取資料完畢,執行更新操作時,會把更新鎖升級為獨佔鎖.
l與其它鎖的相容性:更新鎖與共享鎖相容,即一個資源可以同時放置更新鎖和共享鎖,但是最多隻能放置一把更新鎖,這樣,當多個事務更新相同的資料時,只有一個事務能獲得更新鎖,然後再把更新鎖升級為獨佔鎖,其它事務必須等到前一個事務結束後,才能獲得更新鎖,避免了死鎖.
l併發效能:允許多個事務同時讀鎖定資源,但不允許其它事務修改它.
這裡寫圖片描述

ReadUncommited讀未提交資料

LockMode FORCE
Similiar to UPGRADE except that, for versioned entities, it results in a forced version increment.

LockMode NONE
No lock required.

LockMode READ
A shared lock. Objects in this lock mode were read from the database in the current transaction, rather than being pulled from a cache (注:也就是從資料庫中讀資料,繞過了Hibernate的Cache)

LockMode UPGRADE
An upgrade lock.(注:相當於SQL語句select xxx from xxxx for update,也就是把事務的處理交給了資料庫)

LockMode UPGRADE_NOWAIT
Attempt to obtain an upgrade lock, using an Oracle-style select for update nowait.

LockMode WRITE
A WRITE lock is obtained when an object is updated or inserted.This lock mode is for internal use only and is not a valid mode for load() or lock() (both of which throw exceptions if WRITE is specified).(注:不能在load的時候用,否則丟擲異常)

不過,“紙上得來終覺淺,覺知此事要躬行”,博主做了下實驗來比較這些“鎖”的不同。

先看程式碼:

package com.javaye;

import org.hibernate.LockMode;
import org.hibernate.Session;
import org.hibernate.Transaction;

import com.javaye.models.Article;

public class Main {

private static void insert(){
Session session = HibernateSessionFactory.getSession();
Transaction tx = session.beginTransaction();
Article art = new Article();
art.setTitle("AAA");
art.setVisitAmount(0);
session.saveOrUpdate(art);
tx.commit();
}

privatestatic void update(){
Session session = HibernateSessionFactory.getSession();
System.out.println("session:"+session.hashCode());
Transaction tx =session.beginTransaction();
Article art = (Article) session.load(Article.class, 1,LockMode.UPGRADE);
System.out.println("loaded");
art.setVisitAmount(art.getVisitAmount()+1);
session.save(art);
tx.commit();
session.evict(art);

}

private static void work(){
for(int i=0;i<10;i++){
System.out.println(Thread.currentThread().getName()+":"+(i+1)+"times.");
update();
}
}

public static void main(String[] args) throws Exception{
Thread t1 = new Thread(
new Runnable(){
public void run(){
work();
}
}
);

Thread t2 = new Thread(
new Runnable(){
public void run(){
work();
}
}
);
t1.setName("Thread1");
t2.setName("Thread2");
t1.setDaemon(true);
t2.setDaemon(true);
t1.start();
t2.start();
t1.join();
t2.join();
}
}

這是一個多執行緒程式,每個執行緒都會從資料庫中取出visit_amount,然後加一,再存回資料庫,每個執行緒重複10遍。

請注意藍色的部分,我們在這裡設一個斷點,那麼用Eclipse除錯的時候,到達這個斷點的執行緒就會停下來,由於它的事務還沒有commit(),LockMode.UPGRADE的鎖就還沒有釋放,那麼另外一個執行緒中事務就會在load的時候因為不能獲得鎖而阻塞,那麼理論上我們只會看到只有一句“loaded”輸出。實驗結果證明了我的猜想,LockMode.UPGRADE的情況下,如果一個事務獲得了鎖,即使另外的事務想讀取資料也是不行的,必須等待鎖的釋放。

那麼,改寫資料可以嗎?筆者又做了一個實驗,開啟MySQL Query Browser,直接生改資料庫,把visit_amount欄位的值硬生生改過來,結果發現提交的時候就阻塞了,MySQL的海豚標誌一個勁的游泳,這說明,LockMode.UPGRADE級別的鎖不是由Hibernate控制的,而是由資料庫控制的。

再試一試LockeMode.Read,斷點還是設在原來的位置,發現有兩次“loaded”輸出,證明兩個事務可以同時讀取這條資料,那麼這個鎖有什麼作用呢?根據我實驗的結果,似乎只是為了繞過cache,從資料庫直接讀取。為了證明我的猜想,我直接通過MySQL Query Browser更改了visit_amount,除錯發現,Hibernate是從資料庫中讀取的新值,而不是cache中的老值。

最後在補充一點,LockMode.UPGRADE加鎖是有超時時間的,如果加鎖後超過一定的時間不commit,Hibernate會丟擲異常。

為了得到最大的效能,一般資料庫都有併發機制,不過帶來的問題就是資料訪問的衝突。為了解決這個問題,大多數資料庫用的方法就是資料的鎖定。

資料的鎖定分為兩種方法,第一種叫做悲觀鎖,第二種叫做樂觀鎖。什麼叫悲觀鎖呢,悲觀鎖顧名思義,就是對資料的 搜尋衝突採取一種悲觀的態度,也就是說假設資料肯定會衝突,所以在資料開始讀取的時候就把資料鎖定住。而樂觀鎖就是認為資料一般情況下不會造成衝突,所以在資料進行提交更新的時候,才會正式對資料的衝突與否進行檢測,如果發現衝突了,則讓使用者返回錯誤的資訊,讓使用者決定如何去做。

先從悲觀鎖開始說。在SqlServer等其餘很多資料庫中,資料的鎖定通常採用頁級鎖的方式,也就是說對一張表內的資料是一種序列化的更新插入機制,在任何時間同一張表只會插1條資料,別的想插入的資料要等到這一條資料插完以後才能依次插入。帶來的後果就是效能的降低,在多使用者併發訪問的時候,當對一張表進行頻繁操作時,會發現響應效率很低,資料庫經常處於一種假死狀態。而Oracle用的是行級鎖,只是對想鎖定的資料才進行鎖定,其餘的資料不相干,所以在對Oracle表中併發插資料的時候,基本上不會有任何影響。

注:對於悲觀鎖是針對併發的可能性比較大,而一般在我們的應用中用樂觀鎖足以。

Oracle的悲觀鎖需要利用一條現有的連線,分成兩種方式,從SQL語句的區別來看,就是一種是for update,一種是for update nowait的形式。比如我們看一個例子。首先建立測試用的資料庫表。

CREATE TABLE TEST(ID,NAME,LOCATION,VALUE,CONSTRAINT test_pk PRIMARY KEY(ID))AS SELECT deptno, dname, loc, 1 FROM scott.dept

這裡我們利用了Oracle的Sample的scott使用者的表,把資料copy到我們的test表中。首先我們看一下for update鎖定方式。首先我們執行如下的select for update語句。

select * from test where id = 10 for update

通過這條檢索語句鎖定以後,再開另外一個sql*plus視窗進行操作,再把上面這條sql語句執行一便,你會發現sqlplus好像死在那裡了,好像檢索不到資料的樣子,但是也不返回任何結果,就屬於卡在那裡的感覺。這個時候是什麼原因呢,就是一開始的第一個Session中的select for update語句把資料鎖定住了。由於這裡鎖定的機制是wait的狀態(只要不表示nowait那就是wait),所以第二個Session(也就是卡住的那個sql*plus)中當前這個檢索就處於等待狀態。當第一個session最後commit或者rollback之後,第二個session中的檢索結果就是自動跳出來,並且也把資料鎖定住。不過如果你第二個session中你的檢索語句如下所示。

select * from test where id = 10

也就是沒有for update這種鎖定資料的語句的話,就不會造成阻塞了。另外一種情況,就是當資料庫資料被鎖定的時候,也就是執行剛才for update那條sql以後,我們在另外一個session中執行for update nowait後又是什麼樣呢。比如如下的sql語句。 由於這條語句中是制定採用nowait方式來進行檢索,所以當發現數據被別的session鎖定中的時候,就會迅速返回ORA-00054錯誤,內容是資源正忙, 但指定以 NOWAIT 方式獲取資源。所以在程式中我們可以採用nowait方式迅速判斷當前資料是否被鎖定中,如果鎖定中的話,就要採取相應的業務措施進行處理。

select * from test where id = 10 for update nowait

那這裡另外一個問題,就是當我們鎖定住資料的時候,我們對資料進行更新和刪除的話會是什麼樣呢。比如同樣,我們讓第一個Session鎖定住id=10的那條資料,我們在第二個session中執行如下語句。

update test set value=2 where id = 10

這個時候我們發現update語句就好像select for update語句一樣也停住卡在這裡,當你第一個session放開鎖定以後update才能正常執行。當你update執行後,資料又被你update語句鎖定住了,這個時候只要你update後還沒有commit,別的session照樣不能對資料進行鎖定更新等等。

總之,Oracle中的悲觀鎖就是利用Oracle的Connection對資料進行鎖定。在Oracle中,用這種行級鎖帶來的效能損失是很小的,只是要注意程式邏輯,不要給你一不小心搞成死鎖了就好。而且由於資料的及時鎖定,在資料提交時候就不撥出現衝突,可以省去很多惱人的資料衝突處理。缺點就是你必須要始終有一條資料庫連線,就是說在整個鎖定到最後放開鎖的過程中,你的資料庫聯接要始終保持住。與悲觀鎖相對的,我們有了樂觀鎖。樂觀鎖一開始也說了,就是一開始假設不會造成資料衝突,在最後提交的時候再進行資料衝突檢測。在樂觀鎖中,我們有3種

常用的做法來實現。

[1]第一種就是在資料取得的時候把整個資料都copy到應用中,在進行提交的時候比對當前資料庫中的資料和開始的時候更新前取得的資料。當發現兩個資料一模一樣以後,就表示沒有衝突可以提交,否則則是併發衝突,需要去用業務邏輯進行解決。

[2]第二種樂觀鎖的做法就是採用版本戳,這個在Hibernate中得到了使用。採用版本戳的話,首先需要在你有樂觀鎖的資料庫table上建立一個新的column,比如為number型,當你資料每更新一次的時候,版本數就會往上增加1。比如同樣有2個session同樣對某條資料進行操作。兩者都取到當前的資料的版本號為1,當第一個session進行資料更新後,在提交的時候檢視到當前資料的版本還為1,和自己一開始取到的版本相同。就正式提交,然後把版本號增加1,這個時候當前資料的版本為2。當第二個session也更新了資料提交的時候,發現數據庫中版本為2,和一開始這個session取到的版本號不一致,就知道別人更新過此條資料,這個

時候再進行業務處理,比如整個Transaction都Rollback等等操作。在用版本戳的時候,可以在應用程式側使用版本戳的驗證,也可以在資料庫側採用Trigger(觸發器)來進行驗證。不過資料庫的Trigger的效能開銷還是比較的大,所以能在應用側進行驗證的話還是推薦不用Trigger。

[3]第三種做法和第二種做法有點類似,就是也新增一個Table的Column,不過這次這個column是採用timestamp型,儲存資料最後更新的時間。在Oracle9i以後可以採用新的資料型別,也就是timestamp with time zone型別來做時間戳。這種Timestamp的資料精度在Oracle的時間型別中是最高的,精確到微秒(還沒與到納秒的級別),一般來說,加上資料庫處理時間和人的思考動作時間,微秒級別是非常非常夠了,其實只要精確到毫秒甚至秒都應該沒有什麼問題。和剛才的版本戳類似,也是在更新提交的時候檢查當前資料庫中資料的時間戳和自己更新前取到的時間戳進行對比,如果一致則OK,否則就是版本衝突。如果不想把程式碼寫在程式中或者由於別的原因無法把程式碼寫在現有的程式中,也可以把這個時間戳樂觀鎖邏輯寫在Trigger或者儲存過程中。