概念
樂觀鎖也叫共享鎖、讀鎖,事務(wù)T給對象A加了樂觀鎖,則其他事務(wù)還可以給對象A加樂觀鎖。但不能加被排他鎖。也就是可以同時(shí)讀數(shù)據(jù),但讀的過程中,不允許任何寫。
悲觀鎖也叫排他鎖、寫鎖,事務(wù)T給對象A加了悲觀鎖,則其他事務(wù)不可以給對象A加任何鎖。也就是在寫期間,不允許任何讀、寫操作。
實(shí)現(xiàn)
在數(shù)據(jù)庫用select for update屬于樂觀鎖,update/delete屬于悲觀鎖。
在java中,同步塊屬于悲觀鎖。ReentrantReadWriteLock有ReadLock/WriteLock,分別實(shí)現(xiàn)樂觀鎖和悲觀鎖。注意:java的鎖只對單個(gè)jvm有效。所以在集群環(huán)境下,必須借助數(shù)據(jù)庫或者第三方組件(例如:zookeeper,redis)。
應(yīng)用場景
減庫存:庫存為10,每一個(gè)訂單庫存減1。
分析:減庫存分兩個(gè)步驟:1.判斷是否有庫存(庫存為0);2.如果有則減1,如果沒有則返回。
并發(fā)環(huán)境下,存在兩個(gè)問題:
- A/B兩個(gè)用戶同時(shí)判斷庫存都只剩一件,進(jìn)行減1操作,產(chǎn)生超發(fā)。
- 同時(shí)進(jìn)行對庫存做減1操作,庫存少減了1(例如庫存10。兩個(gè)進(jìn)程同時(shí)來減1,A進(jìn)程做stock=10-1,B進(jìn)程也做stock=10-1,則庫存變成了9,而不是期望的8)。
假設(shè)庫存是一個(gè)變量stock 。
** 方案1(悲觀鎖)**:判斷stock 是否大于0前,先將變量加一把寫鎖,然后再判斷。這樣其他進(jìn)程連判斷語句都進(jìn)不了。
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock w = lock.writeLock();
w.lock();
if(stock >0) stock --;
else return false;
方案2(樂觀鎖):判斷A是否大于0前,先將變量加一把讀鎖,判斷成功準(zhǔn)備寫之前,將讀鎖升級為寫鎖。
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock r = lock.readLock();
r.lock();
if(a>0) {
w.lock();
a--;
}
else return false;
在集群環(huán)境下的方案:借助數(shù)據(jù)庫的悲觀鎖和樂觀鎖。由于數(shù)據(jù)庫的鎖資源非常寶貴,為了增加系統(tǒng)的吞吐量,一般采用版本號(hào)的方案。
假設(shè)數(shù)據(jù)庫表Stock,有兩個(gè)字段num,version,初始<10,0>。
select * from Stock; //這里并沒有加鎖
if(Stock.num>0) {
ver = Stock.version;
update Stock set num=num-1,version=version+1 where version = ver;
if (影響的行數(shù) == 1) {
return true;
} else {
return false;
}
} else {
return false;
}
另外,可以將上述方案結(jié)合起來。在java端做一個(gè)庫存是否為空的變量(靜態(tài)),這樣就避免在庫存已經(jīng)為0的情況下還去查數(shù)據(jù)庫。
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock w = lock.writeLock();
static boolean isZero = false;
r.lock();
if(!isZero) {
select * from Stock; //這里并沒有加鎖
if(Stock.num>0) {
ver = Stock.version;
update Stock set num=num-1,version=version+1 where version = ver;
if (影響的行數(shù) == 1) {
//判斷是否還有庫存并修改變量
select count(1) cnt from Stock where num = 0;
w.lock();
isZero = true;
w.unlock();
return true;
} else {
return false;
}
} else {
return false;
}
這里簡化了一個(gè)問題,庫存是針對某件商品的,所以應(yīng)該是一個(gè)Map的鎖。如果直接針對Map加鎖勢必導(dǎo)致多個(gè)商品同時(shí)被加鎖。可以使用:
- java.util.concurrent.ConcurrentHashMap 類;
- 利用ReentrantReadWriteLock實(shí)現(xiàn)一個(gè)ReadWriteMap。見網(wǎng)絡(luò)。