悲觀鎖(Pessimistic Lock)和樂(lè)觀鎖(Optimistic Lock)是數(shù)據(jù)庫(kù)系統(tǒng)中并發(fā)控制主要采用的技術(shù)手段。針對(duì)不同的業(yè)務(wù)場(chǎng)景,應(yīng)該選用不同的并發(fā)控制方式。不要把它們和數(shù)據(jù)庫(kù)中提供的鎖機(jī)制(行鎖、表鎖、排他鎖、共享鎖)混為一談。
Pessimistic Lock
概述
悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)block直到它拿到鎖。假定會(huì)發(fā)生并發(fā)沖突,屏蔽一切可能違反數(shù)據(jù)完整性的操作。
Java synchronized 就屬于悲觀鎖的一種實(shí)現(xiàn),每次線程要修改數(shù)據(jù)時(shí)都先獲得鎖,保證同一時(shí)刻只有一個(gè)線程能操作數(shù)據(jù),其他線程則會(huì)被block。
特性
- 需要依靠數(shù)據(jù)庫(kù)中的鎖機(jī)制來(lái)實(shí)現(xiàn),即通過(guò)常用的select ... for update操作來(lái)實(shí)現(xiàn)悲觀鎖。
- 需要開(kāi)啟事務(wù),在事務(wù)中實(shí)現(xiàn)鎖機(jī)制。
- 可以最大程度的保證數(shù)據(jù)操作的獨(dú)占性。
- select for update語(yǔ)句中所有掃描過(guò)的行都會(huì)被鎖上,這一點(diǎn)很容易造成問(wèn)題。如果用悲觀鎖請(qǐng)確保用到了索引,否則造成鎖表。
- 長(zhǎng)事務(wù)中的鎖等待,會(huì)導(dǎo)致其他用戶長(zhǎng)時(shí)間無(wú)法操作。
- 主要用于數(shù)據(jù)爭(zhēng)用激烈的環(huán)境,以及發(fā)生并發(fā)沖突時(shí)使用鎖保護(hù)數(shù)據(jù)的成本要低于回滾事務(wù)的成本的環(huán)境中。
Optimistic Lock
樂(lè)觀鎖,又稱樂(lè)觀并發(fā)控制(Optimistic Concurrency Control),樂(lè)觀地認(rèn)為不會(huì)發(fā)生并發(fā)問(wèn)題,只在提交更新操作時(shí)檢查是否違反數(shù)據(jù)的一致性。
概述
樂(lè)觀鎖在數(shù)據(jù)庫(kù)中的實(shí)現(xiàn)完全是邏輯性的,不需要數(shù)據(jù)庫(kù)提供特殊的支持。一般的做法是在數(shù)據(jù)表中增加一個(gè)字段(版本號(hào)或者時(shí)間戳),作為數(shù)據(jù)的版本標(biāo)識(shí)。讀取數(shù)據(jù)時(shí),將版本號(hào)一同讀出;之后更新數(shù)據(jù)時(shí),加入版本號(hào)條件,更新成功就將版本號(hào)加1。樂(lè)觀鎖的重點(diǎn)在于,更新數(shù)據(jù)時(shí),加入版本號(hào)匹配條件,將數(shù)據(jù)的版本與數(shù)據(jù)表中對(duì)應(yīng)記錄的當(dāng)前版本進(jìn)行匹配更新,如果數(shù)據(jù)的版本號(hào)等于數(shù)據(jù)表的當(dāng)前版本號(hào),則獲取鎖成功,也就是更新成功;否則,更新失敗,需要回滾整個(gè)業(yè)務(wù)操作。Java中的atomic包就是樂(lè)觀鎖的一種實(shí)現(xiàn),AtomicInteger 通過(guò)CAS(Compare And Set)操作實(shí)現(xiàn)線程安全的自增。
實(shí)現(xiàn)機(jī)制
在數(shù)據(jù)庫(kù)中,update同一行的情況是不允許并發(fā)的,即數(shù)據(jù)庫(kù)每次執(zhí)行一條update語(yǔ)句時(shí)會(huì)獲取被update行的寫鎖,直到這一行被成功更新后才釋放。因此在業(yè)務(wù)操作進(jìn)行前獲取需要鎖的數(shù)據(jù)的當(dāng)前版本號(hào),然后實(shí)際更新數(shù)據(jù)時(shí),以版本號(hào)作為條件,再次對(duì)比版本號(hào)確認(rèn)與之前獲取的相同,并更新版本號(hào),即可確認(rèn)沒(méi)有發(fā)生并發(fā)的修改。如果更新失敗即可認(rèn)為老版本的數(shù)據(jù)已經(jīng)被并發(fā)修改掉了,此時(shí)認(rèn)為獲取鎖失敗,需要回滾整個(gè)業(yè)務(wù)操作并可根據(jù)需要重試整個(gè)過(guò)程。
特性
- 不需要依靠數(shù)據(jù)庫(kù)中的鎖機(jī)制來(lái)實(shí)現(xiàn),但需要在表中新增一個(gè)版本號(hào),在邏輯上實(shí)現(xiàn)。
- 無(wú)論是否開(kāi)啟事務(wù),都可以在邏輯上實(shí)現(xiàn)樂(lè)觀鎖。
- 樂(lè)觀鎖在不發(fā)生取鎖失敗的情況下開(kāi)銷比悲觀鎖小,但是一旦發(fā)生失敗回滾開(kāi)銷則比較大,因此適合用在取鎖失敗概率比較小的場(chǎng)景,可以提升系統(tǒng)并發(fā)性能。
示例
悲觀鎖
用數(shù)據(jù)庫(kù)來(lái)演示悲觀鎖,首先悲觀鎖是必須用到數(shù)據(jù)庫(kù)的事務(wù)機(jī)制,其次要注意查詢條件字段必須是索引字段,否則會(huì)造成鎖表。
- 開(kāi)啟事務(wù)
begin
- 執(zhí)行for update操作。
select * from t_logs where id = '2' for update;
- 不要執(zhí)行commit操作,為了模仿并發(fā)操作。
在Navicat中開(kāi)啟另一個(gè)會(huì)話窗口
- 開(kāi)啟事務(wù)
begin
- 執(zhí)行update操作
update t_logs set action = '測(cè)試用例' where id = '2';
- 如果不執(zhí)行上一個(gè)會(huì)話的commit操作,會(huì)發(fā)現(xiàn)此會(huì)話一直處于block狀態(tài)。
- 執(zhí)行上一個(gè)會(huì)話的commit操作,提交數(shù)據(jù)。
- 執(zhí)行此會(huì)話的commit操作。
樂(lè)觀鎖
商品的庫(kù)存量是固定的,保證商品數(shù)量不超賣, 需要保證數(shù)據(jù)一致性:用樂(lè)觀鎖來(lái)保證某個(gè)人點(diǎn)擊秒殺后系統(tǒng)中查出來(lái)的庫(kù)存量和實(shí)際扣減庫(kù)存時(shí)庫(kù)存量是一致的。
- 商品表
CREATE TABLE `tb_product_stock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`product_id` bigint(32) NOT NULL COMMENT '商品ID',
`number` INT(8) NOT NULL DEFAULT 0 COMMENT '庫(kù)存數(shù)量',
`create_time` DATETIME NOT NULL COMMENT '創(chuàng)建時(shí)間',
`modify_time` DATETIME NOT NULL COMMENT '更新時(shí)間',
PRIMARY KEY (`id`),
UNIQUE KEY `index_pid` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品庫(kù)存表';
- POJO類
@Getter
@Setter
@ToString
public class ProductStock {
private Long productId; //商品id
private Integer number; //庫(kù)存量
}
- 鎖實(shí)現(xiàn)
public boolean updateStock(Long productId) {
int updateCnt = 0;
while (updateCnt == 0) {
ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
if (product.getNumber() > 0) {
// 確保庫(kù)存不會(huì)減為負(fù)數(shù)
updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number>=1", productId);
if(updateCnt > 0){ //更新庫(kù)存成功
return true;
}
} else { //賣完
return false;
}
}
return false;
}
UPDATE 語(yǔ)句的WHERE 條件字句上需要建索引,避免全表掃描。