分散式鎖是一種用於保證分散式系統中多個程序或執行緒同步存取共用資源的技術。同時它又是面試中的常見問題,所以我們本文就重點來看分散式鎖的具體實現(含實現程式碼)。
在分散式系統中,由於各個節點之間的網路通訊延遲、故障等原因,可能會導致資料不一致的問題。分散式鎖通過協調多個節點的行為,保證在任何時刻只有一個節點可以存取共用資源,以避免資料的不一致性和衝突。
分散式鎖通常需要滿足以下幾個要求:
在 Java 中,實現分散式鎖的方案有多種,包括:
資料庫的樂觀鎖或悲觀鎖都可以實現分散式鎖,下面分別來看。
在資料庫中使用 for update 關鍵字可以實現悲觀鎖,我們在 Mapper 中新增 for update 即可對資料加鎖,實現程式碼如下:
<!-- UserMapper.xml -->
<select id="selectByIdForUpdate" resultType="User">
SELECT * FROM user WHERE id = #{id} FOR UPDATE
</select>
在 Service 中呼叫 Mapper 方法,即可獲取到加鎖的資料:
@Transactional
public void updateWithPessimisticLock(int id, String name) {
User user = userMapper.selectByIdForUpdate(id);
if (user != null) {
user.setName(name);
userMapper.update(user);
} else {
throw new RuntimeException("資料不存在");
}
}
在 MyBatis 中,可以通過給表新增一個版本號欄位來實現樂觀鎖。在 Mapper 中,使用
<!-- UserMapper.xml -->
<update id="updateWithOptimisticLock">
UPDATE user SET
name = #{name},
version = version + 1
WHERE id = #{id} AND version = #{version}
</update>
在 Service 中呼叫 Mapper 方法,需要傳入更新資料的版本號。如果更新失敗,說明資料已經被其他事務修改,具體實現程式碼如下:
@Transactional
public void updateWithOptimisticLock(int id, String name, int version) {
User user = userMapper.selectById(id);
if (user != null) {
user.setName(name);
user.setVersion(version);
int rows = userMapper.updateWithOptimisticLock(user);
if (rows == 0) {
throw new RuntimeException("資料已被其他事務修改");
}
} else {
throw new RuntimeException("資料不存在");
}
}
在 Spring Boot 中,可以使用 Curator 框架來實現 ZooKeeper 分散式鎖,具體實現分為以下 3 步:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>latest</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>latest</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>latest</version>
</dependency>
在 application.yml 中新增 ZooKeeper 連線設定:
spring:
zookeeper:
connect-string: localhost:2181
namespace: demo
@Component
public class DistributedLock {
@Autowired
private CuratorFramework curatorFramework;
/**
* 獲取分散式鎖
*
* @param lockPath 鎖路徑
* @param waitTime 等待時間
* @param leaseTime 鎖持有時間
* @param timeUnit 時間單位
* @return 鎖物件
* @throws Exception 獲取鎖異常
*/
public InterProcessMutex acquire(String lockPath, long waitTime, long leaseTime, TimeUnit timeUnit) throws Exception {
InterProcessMutex lock = new InterProcessMutex(curatorFramework, lockPath);
if (!lock.acquire(waitTime, timeUnit)) {
throw new RuntimeException("獲取分散式鎖失敗");
}
if (leaseTime > 0) {
lock.acquire(leaseTime, timeUnit);
}
return lock;
}
/**
* 釋放分散式鎖
*
* @param lock 鎖物件
* @throws Exception 釋放鎖異常
*/
public void release(InterProcessMutex lock) throws Exception {
if (lock != null) {
lock.release();
}
}
}
我們可以使用 Redis 使用者端 Redisson 實現分散式鎖,它的實現步驟如下:
在 pom.xml 中新增如下設定:
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.20.0</version>
</dependency>
在 Spring Boot 專案的組態檔 application.yml 中新增 Redisson 設定:
spring:
data:
redis:
host: localhost
port: 6379
database: 0
redisson:
codec: org.redisson.codec.JsonJacksonCodec
single-server-config:
address: "redis://${spring.data.redis.host}:${spring.redis.port}"
database: "${spring.data.redis.database}"
password: "${spring.data.redis.password}"
import jakarta.annotation.Resource;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class RedissonLockService {
@Resource
private Redisson redisson;
/**
* 加鎖
*
* @param key 分散式鎖的 key
* @param timeout 超時時間
* @param unit 時間單位
* @return
*/
public boolean tryLock(String key, long timeout, TimeUnit unit) {
RLock lock = redisson.getLock(key);
try {
return lock.tryLock(timeout, unit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
/**
* 釋放分散式鎖
*
* @param key 分散式鎖的 key
*/
public void unlock(String key) {
RLock lock = redisson.getLock(key);
lock.unlock();
}
}
Redis 和 ZooKeeper 都可以用來實現分散式鎖,它們在實現分散式鎖的機制和原理上有所不同,具體區別如下:
總之,Redis 適合實現簡單的分散式鎖場景,而 ZooKeeper 適合實現複雜的分散式協調場景,也就是 ZooKeeper 適合強一致性的分散式系統。
強一致性是指系統中的所有節點在任何時刻看到的資料都是一致的。ZooKeeper 中的資料是有序的樹形結構,每個節點都有唯一的路徑識別符號,所有節點都共用同一份資料,當任何一個節點對資料進行修改時,所有節點都會收到通知,更新資料,並確保資料的一致性。
在 ZooKeeper 中,強一致性體現在資料的讀寫操作上。ZooKeeper 使用 ZAB(ZooKeeper Atomic Broadcast)協定來保證資料的一致性,該協定確保了資料更新的順序,所有的資料更新都需要經過叢集中的大多數節點確認,保證了資料的一致性和可靠性。
在 Java 中,使用資料庫、ZooKeeper 和 Redis 都可以實現分散式鎖。但資料庫 IO 操作比較慢,不適合高並行場景;Redis 執行效率最高,但在主從切換時,可能會出現鎖丟失的情況;ZooKeeper 是一個高可用性的分散式協調服務,可以保證資料的強一致性,但是使用 ZooKeeper 需要部署額外的服務,增加了系統複雜度。所以沒有最好的解決方案,只有最合適自己的解決方案。
本文已收錄到我的面試小站 www.javacn.site,其中包含的內容有:Redis、JVM、並行、並行、MySQL、Spring、Spring MVC、Spring Boot、Spring Cloud、MyBatis、設計模式、訊息佇列等模組。