分布式鎖詳解以及Spring?Boot實(shí)戰(zhàn)示例代碼
前言
在分布式系統(tǒng)中,多個(gè)服務(wù)實(shí)例可能同時(shí)操作共享資源(如數(shù)據(jù)庫中的同一訂單、庫存記錄),若缺乏協(xié)調(diào)機(jī)制,會(huì)導(dǎo)致數(shù)據(jù)不一致(如超賣、重復(fù)下單)。分布式鎖正是解決這類問題的核心技術(shù),它能保證同一時(shí)間只有一個(gè)服務(wù)實(shí)例執(zhí)行特定臨界區(qū)代碼。
一、分布式鎖的核心特性
一個(gè)可靠的分布式鎖需滿足以下特性:
- 互斥性:任意時(shí)刻只有一個(gè)線程持有鎖。
- 安全性:鎖只能被持有它的線程釋放。
- 可用性:即使部分節(jié)點(diǎn)故障,鎖仍能正常獲取和釋放。
- 防死鎖:避免因線程崩潰導(dǎo)致鎖永久無法釋放。
- 冪等性:重復(fù)獲取 / 釋放鎖不會(huì)產(chǎn)生副作用。
二、分布式鎖的實(shí)現(xiàn)方案及原理
常見實(shí)現(xiàn)方式包括基于數(shù)據(jù)庫、Redis、ZooKeeper 等,不同方式的原理各有不同。
1. 基于數(shù)據(jù)庫的分布式鎖
原理:利用數(shù)據(jù)庫的唯一索引或悲觀鎖來實(shí)現(xiàn)。例如,創(chuàng)建一張鎖表,包含資源標(biāo)識(shí)、持有線程標(biāo)識(shí)、過期時(shí)間等字段,給資源標(biāo)識(shí)字段創(chuàng)建唯一索引。當(dāng)需要獲取鎖時(shí),向表中插入一條記錄,若插入成功則表示獲取到鎖;釋放鎖時(shí),刪除該記錄。為防止死鎖,可定期清理過期未釋放的鎖。
優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):實(shí)現(xiàn)簡單,無需額外中間件。
- 缺點(diǎn):性能較差,數(shù)據(jù)庫壓力大;易出現(xiàn)鎖表問題;不支持鎖自動(dòng)續(xù)期等高級(jí)特性。
2. 基于 Redis 的分布式鎖
原理:利用 Redis 的SET命令的原子性。核心命令如下:
# 僅當(dāng)key不存在時(shí)設(shè)置值,過期時(shí)間10秒,返回OK表示獲取鎖成功 SET lock:resource true NX PX 10000
- NX:僅在鍵不存在時(shí)才設(shè)置(保證互斥性)。
- PX 10000:設(shè)置鍵的過期時(shí)間為 10 秒(防死鎖)。
釋放鎖時(shí),需通過 Lua 腳本保證原子性,先判斷鎖是否由當(dāng)前線程持有,再刪除鎖:
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
else
return 0
end優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):性能高,操作簡單;支持過期時(shí)間設(shè)置。
- 缺點(diǎn):在 Redis 集群環(huán)境下,可能存在主從同步延遲導(dǎo)致的鎖丟失問題;需自行處理鎖續(xù)期等問題。
由于 Redis 的高性能和易用性,它成為分布式鎖的主流選擇,本文后續(xù)重點(diǎn)介紹基于 Redis 的分布式鎖實(shí)現(xiàn)。
3. 基于 ZooKeeper 的分布式鎖
原理:利用 ZooKeeper 的節(jié)點(diǎn)特性和 Watcher 機(jī)制。ZooKeeper 的節(jié)點(diǎn)分為持久節(jié)點(diǎn)、臨時(shí)節(jié)點(diǎn)、持久順序節(jié)點(diǎn)、臨時(shí)順序節(jié)點(diǎn)。分布式鎖通常使用臨時(shí)順序節(jié)點(diǎn),當(dāng)需要獲取鎖時(shí),在指定節(jié)點(diǎn)下創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn),然后判斷當(dāng)前節(jié)點(diǎn)是否為序號(hào)最小的節(jié)點(diǎn),若是則獲取到鎖;若不是,則監(jiān)聽序號(hào)比當(dāng)前節(jié)點(diǎn)小的最后一個(gè)節(jié)點(diǎn),當(dāng)該節(jié)點(diǎn)被刪除時(shí),重新判斷。釋放鎖時(shí),刪除創(chuàng)建的臨時(shí)節(jié)點(diǎn),由于是臨時(shí)節(jié)點(diǎn),當(dāng)持有鎖的線程崩潰時(shí),節(jié)點(diǎn)會(huì)自動(dòng)刪除,避免死鎖。
優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):可靠性高,不存在鎖丟失問題;支持公平鎖;自帶 Watcher 機(jī)制,可實(shí)現(xiàn)鎖的自動(dòng)釋放和喚醒。
- 缺點(diǎn):性能相對(duì) Redis 較低;部署和維護(hù)成本高。
三、Spring Boot 集成分布式鎖的相關(guān)依賴及對(duì)比
1. 基于 Redis 的依賴
- spring-boot-starter-data-redis:
- 提供了 Redis 的基本操作模板(RedisTemplate),可用于手動(dòng)實(shí)現(xiàn)分布式鎖。
- 需自行處理鎖的獲取、釋放、續(xù)期等邏輯,實(shí)現(xiàn)相對(duì)復(fù)雜,但靈活性高。
- redisson-spring-boot-starter:
- 是 Redis 官方推薦的 Java 客戶端,內(nèi)置了分布式鎖的完整實(shí)現(xiàn),支持自動(dòng)續(xù)期、公平鎖、可重入鎖等高級(jí)特性。
- 封裝了復(fù)雜的底層邏輯,使用簡單,適合生產(chǎn)環(huán)境。
2. 基于 ZooKeeper 的依賴
- spring-cloud-starter-zookeeper-discovery:
- 主要用于服務(wù)發(fā)現(xiàn),但也可借助 ZooKeeper 客戶端操作 ZooKeeper 實(shí)現(xiàn)分布式鎖。
- 需要自行基于 ZooKeeper 的 API 實(shí)現(xiàn)鎖的邏輯,較為繁瑣。
- curator-recipes:
- 是 ZooKeeper 的客戶端框架,提供了分布式鎖等常用功能的封裝,如 InterProcessMutex 等類可直接用于實(shí)現(xiàn)分布式鎖。
- 簡化了 ZooKeeper 分布式鎖的實(shí)現(xiàn),可靠性高。
3. 依賴對(duì)比
依賴 | 基于中間件 | 特點(diǎn) | 適用場景 |
spring-boot-starter-data-redis | Redis | 基礎(chǔ)操作支持,需自行實(shí)現(xiàn)鎖邏輯 | 簡單場景,對(duì)靈活性要求高 |
redisson-spring-boot-starter | Redis | 內(nèi)置完整鎖實(shí)現(xiàn),支持高級(jí)特性 | 生產(chǎn)環(huán)境,復(fù)雜業(yè)務(wù)場景 |
spring-cloud-starter-zookeeper-discovery | ZooKeeper | 主要用于服務(wù)發(fā)現(xiàn),鎖實(shí)現(xiàn)需自行開發(fā) | 已使用 ZooKeeper 做服務(wù)發(fā)現(xiàn),簡單鎖場景 |
curator-recipes | ZooKeeper | 封裝了分布式鎖功能,可靠性高 | 對(duì)鎖可靠性要求高的場景 |
四、Spring Boot 集成 Redis 分布式鎖實(shí)戰(zhàn)
1. 環(huán)境準(zhǔn)備
pom.xml 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId> <!-- 推薦使用Redisson簡化鎖操作 -->
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version>
</dependency>Redis 配置(application.yml):
spring:
redis:
host: localhost
port: 6379
database: 0
timeout: 3000ms2. 基于 Redisson 的分布式鎖實(shí)現(xiàn)
Redisson 是 Redis 官方推薦的 Java 客戶端,內(nèi)置了分布式鎖的完整實(shí)現(xiàn),支持自動(dòng)續(xù)期、公平鎖、可重入鎖等高級(jí)特性。
分布式鎖工具類:
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class RedisDistributedLock {
private final RedissonClient redissonClient;
public RedisDistributedLock(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
/**
* 獲取分布式鎖
* @param lockKey 鎖標(biāo)識(shí)
* @param waitTime 等待時(shí)間(獲取鎖的最大等待時(shí)長)
* @param leaseTime 鎖持有時(shí)間(自動(dòng)釋放時(shí)間)
* @return 鎖對(duì)象
*/
public RLock lock(String lockKey, long waitTime, long leaseTime) {
RLock lock = redissonClient.getLock(lockKey);
try {
// 嘗試獲取鎖,最多等待waitTime,持有l(wèi)easeTime后自動(dòng)釋放
boolean isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS);
if (isLocked) {
return lock;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
/**
* 釋放鎖
* @param lock 鎖對(duì)象
*/
public void unlock(RLock lock) {
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}3. 業(yè)務(wù)場景示例:庫存扣減
Service 層代碼:
import org.redisson.api.RLock;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class InventoryService {
@Resource
private RedisDistributedLock distributedLock;
@Resource
private InventoryMapper inventoryMapper; // 假設(shè)已實(shí)現(xiàn)數(shù)據(jù)庫操作
/**
* 扣減商品庫存
* @param productId 商品ID
* @param quantity 扣減數(shù)量
* @return 操作結(jié)果
*/
public boolean deductInventory(Long productId, int quantity) {
// 鎖標(biāo)識(shí):通常用業(yè)務(wù)資源唯一標(biāo)識(shí)(如商品ID)
String lockKey = "lock:inventory:" + productId;
RLock lock = null;
try {
// 獲取鎖:最多等待3秒,持有10秒后自動(dòng)釋放
lock = distributedLock.lock(lockKey, 3, 10);
if (lock == null) {
// 獲取鎖失?。ㄈ绯瑫r(shí))
return false;
}
// 臨界區(qū)代碼:查詢庫存并扣減
int currentStock = inventoryMapper.selectStockByProductId(productId);
if (currentStock >= quantity) {
inventoryMapper.updateStock(productId, currentStock - quantity);
return true;
} else {
// 庫存不足
return false;
}
} finally {
// 確保鎖釋放
distributedLock.unlock(lock);
}
}
}五、關(guān)鍵注意事項(xiàng)
- 鎖的粒度:鎖標(biāo)識(shí)應(yīng)精準(zhǔn)到具體資源(如lock:order:123而非lock:order),避免鎖范圍過大導(dǎo)致性能瓶頸。
- 過期時(shí)間設(shè)置:需大于業(yè)務(wù)執(zhí)行時(shí)間,Redisson 的watch dog機(jī)制會(huì)自動(dòng)續(xù)期(默認(rèn)每 30 秒續(xù)期一次)。
- 異常處理:必須在finally塊中釋放鎖,避免因業(yè)務(wù)異常導(dǎo)致鎖泄漏。
- 重試機(jī)制:獲取鎖失敗時(shí)可添加有限重試邏輯(如循環(huán) 3 次),提高成功率。
六、進(jìn)階優(yōu)化方向
- 公平鎖:通過redissonClient.getFairLock(lockKey)實(shí)現(xiàn),避免線程饑餓。
- 紅鎖(RedLock):在多 Redis 節(jié)點(diǎn)環(huán)境中,通過多個(gè)實(shí)例獲取鎖提高可靠性(適合極高一致性場景)。
- 緩存與數(shù)據(jù)庫一致性:結(jié)合本地鎖(synchronized)與分布式鎖,減少分布式鎖的使用頻率。
通過不同的分布式鎖實(shí)現(xiàn)方式和相關(guān)依賴,Spring Boot 應(yīng)用可在分布式環(huán)境中安全地操作共享資源。在實(shí)際開發(fā)中,需根據(jù)業(yè)務(wù)場景和性能需求選擇合適的實(shí)現(xiàn)方式和依賴,Redisson 等成熟工具可大幅降低實(shí)現(xiàn)復(fù)雜度,建議在生產(chǎn)環(huán)境中優(yōu)先采用。
總結(jié)
到此這篇關(guān)于分布式鎖詳解以及Spring Boot實(shí)戰(zhàn)的文章就介紹到這了,更多相關(guān)分布式鎖及Spring Boot實(shí)戰(zhàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決idea 通過build project 手動(dòng)觸發(fā)熱部署失敗的問題
在debug運(yùn)行項(xiàng)目的過程中,并且保證(不添加方法,不修改方法名)一定的規(guī)則的情況下,可以通過build project 來手動(dòng)熱部署項(xiàng)目,本文給大家介紹解決idea 通過build project 手動(dòng)觸發(fā)熱部署失敗的問題,感興趣的朋友一起看看吧2023-12-12
Java生產(chǎn)者和消費(fèi)者例子_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
生產(chǎn)者-消費(fèi)者(producer-consumer)問題,也稱作有界緩沖區(qū)(bounded-buffer)問題,兩個(gè)進(jìn)程共享一個(gè)公共的固定大小的緩沖區(qū)。下文通過實(shí)例給大家介紹java生產(chǎn)者和消費(fèi)者,感興趣的朋友一起學(xué)習(xí)吧2017-05-05
通過Java實(shí)現(xiàn)添加或刪除PDF中的附件
當(dāng)我們?cè)谥谱鱌DF文件或者PPT演示文稿的時(shí)候,為了讓自己的文件更全面詳細(xì),就會(huì)在文件中添加附件。本文為大家整理了Java實(shí)現(xiàn)添加或刪除PDF中的附件的方法,需要的可以參考下2023-01-01
原理分析SonarQube中IdentityProvider賬戶互斥現(xiàn)象
這篇文章主要為大家介紹分析SonarQube中IdentityProvider賬戶互斥現(xiàn)象原理,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-02-02
關(guān)于mybatis if else if 條件判斷SQL片段表達(dá)式取值和拼接問題
這篇文章主要介紹了mybatis if else if 條件判斷SQL片段表達(dá)式取值和拼接,文章通過自己真實(shí)使用的例子給大家詳細(xì)介紹,需要的朋友可以參考下2021-09-09
SpringCloud中的Ribbon負(fù)載均衡詳細(xì)解讀
這篇文章主要介紹了SpringCloud中的Ribbon負(fù)載均衡詳細(xì)解讀,當(dāng)系統(tǒng)面臨大量的用戶訪問,負(fù)載過高的時(shí)候,通常會(huì)增加服務(wù)器數(shù)量來進(jìn)行橫向擴(kuò)展(集群),多個(gè)服務(wù)器的負(fù)載需要均衡,以免出現(xiàn)服務(wù)器負(fù)載不均衡,部分服務(wù)器負(fù)載較大,部分服務(wù)器負(fù)載較小的情況,需要的朋友可以參考下2023-11-11

