分布式鎖的三種實現!
分布式鎖是一種用于保證分布式系統中多個進程或線程同步訪問共享資源的技術。同時它又是面試中的常見問題,所以我們本文就重點來看分布式鎖的具體實現(含實現代碼)。
在分布式系統中,由于各個節點之間的網絡通信延遲、故障等原因,可能會導致數據不一致的問題。分布式鎖通過協調多個節點的行為,保證在任何時刻只有一個節點可以訪問共享資源,以避免數據的不一致性和沖突。
1、分布式鎖要求
分布式鎖通常需要滿足以下幾個要求:
- 互斥性:在任意時刻只能有一個客戶端持有鎖。
- 不會發生死鎖:即使持有鎖的客戶端發生故障,也能保證鎖最終會被釋放。
- 具有容錯性:分布式鎖需要能夠容忍節點故障等異常情況,保證系統的穩定性。
2、實現方案
在 Java 中,實現分布式鎖的方案有多種,包括:
- 基于數據庫實現的分布式鎖:可以通過數據庫的樂觀鎖或悲觀鎖實現分布式鎖,但是由于數據庫的 IO 操作比較慢,不適合高并發場景。
- 基于 ZooKeeper 實現的分布式鎖:ZooKeeper 是一個高可用性的分布式協調服務,可以通過它來實現分布式鎖。但是使用 ZooKeeper 需要部署額外的服務,增加了系統復雜度。
- 基于 Redis 實現的分布式鎖:Redis 是一個高性能的內存數據庫,支持分布式部署,可以通過Redis的原子操作實現分布式鎖,而且具有高性能和高可用性。
3、數據庫分布式鎖
數據庫的樂觀鎖或悲觀鎖都可以實現分布式鎖,下面分別來看。
(1)悲觀鎖
在數據庫中使用 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("數據不存在");
}
}
(2)樂觀鎖
在 MyBatis 中,可以通過給表添加一個版本號字段來實現樂觀鎖。在 Mapper 中,使用標簽定義更新語句,同時使用 set 標簽設置版本號的增量。
<!-- 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("數據不存在");
}
}
4、Zookeeper 分布式鎖
在 Spring Boot 中,可以使用 Curator 框架來實現 ZooKeeper 分布式鎖,具體實現分為以下 3 步:
- 引入 Curator 和 ZooKeeper 客戶端依賴;
- 配置 ZooKeeper 連接信息;
- 編寫分布式鎖實現類。
(1)引入 Curator 和 ZooKeeper
<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>
(2)配置 ZooKeeper 連接
在 application.yml 中添加 ZooKeeper 連接配置:
spring:
zookeeper:
connect-string: localhost:2181
namespace: demo
(3)編寫分布式鎖實現類
@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();
}
}
}
5、Redis 分布式鎖
我們可以使用 Redis 客戶端 Redisson 實現分布式鎖,它的實現步驟如下:
- 添加 Redisson 依賴
- 配置 Redisson 連接信息
- 編寫分布式鎖代碼類
(1)添加 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>
(2)配置 Redisson 連接
在 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}"
(3)編寫分布式鎖代碼類
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();
}
}
6、Redis VS Zookeeper
Redis 和 ZooKeeper 都可以用來實現分布式鎖,它們在實現分布式鎖的機制和原理上有所不同,具體區別如下:
- 數據存儲方式:Redis 將鎖信息存儲在內存中,而 ZooKeeper 將鎖信息存儲在 ZooKeeper 的節點上,因此 ZooKeeper 需要更多的磁盤空間。
- 鎖的釋放:Redis 的鎖是通過設置鎖的過期時間來自動釋放的,而 ZooKeeper 的鎖需要手動釋放,如果鎖的持有者出現宕機或網絡中斷等情況,需要等待鎖的超時時間才能自動釋放。
- 鎖的競爭機制:Redis 使用的是單機鎖,即所有請求都直接連接到同一臺 Redis 服務器,容易發生單點故障;而 ZooKeeper 使用的是分布式鎖,即所有請求都連接到 ZooKeeper 集群,具有較好的可用性和可擴展性。
- 一致性:Redis 的鎖是非嚴格意義下的分布式鎖,因為在多臺機器上運行多個進程時,由于 Redis 的主從同步可能會存在數據不一致的問題;而 ZooKeeper 是強一致性的分布式系統,保證了數據的一致性。
- 性能:Redis 的性能比 ZooKeeper 更高,因為 Redis 將鎖信息存儲在內存中,而 ZooKeeper 需要進行磁盤讀寫操作。
總之,Redis 適合實現簡單的分布式鎖場景,而 ZooKeeper 適合實現復雜的分布式協調場景,也就是 ZooKeeper 適合強一致性的分布式系統。
“強一致性是指系統中的所有節點在任何時刻看到的數據都是一致的。ZooKeeper 中的數據是有序的樹形結構,每個節點都有唯一的路徑標識符,所有節點都共享同一份數據,當任何一個節點對數據進行修改時,所有節點都會收到通知,更新數據,并確保數據的一致性。在 ZooKeeper 中,強一致性體現在數據的讀寫操作上。ZooKeeper 使用 ZAB(ZooKeeper Atomic Broadcast)協議來保證數據的一致性,該協議確保了數據更新的順序,所有的數據更新都需要經過集群中的大多數節點確認,保證了數據的一致性和可靠性。”
小結
在 Java 中,使用數據庫、ZooKeeper 和 Redis 都可以實現分布式鎖。但數據庫 IO 操作比較慢,不適合高并發場景;Redis 執行效率最高,但在主從切換時,可能會出現鎖丟失的情況;ZooKeeper 是一個高可用性的分布式協調服務,可以保證數據的強一致性,但是使用 ZooKeeper 需要部署額外的服務,增加了系統復雜度。所以沒有最好的解決方案,只有最合適自己的解決方案。