跳到主要内容

设计分布式锁

问题

如何用 Rust 实现分布式锁?

答案

Redis 分布式锁

use redis::AsyncCommands;
use uuid::Uuid;
use std::time::Duration;

pub struct RedisLock {
client: redis::Client,
key: String,
value: String, // 唯一标识(防止误删)
ttl: Duration,
}

impl RedisLock {
pub fn new(redis_url: &str, key: &str, ttl: Duration) -> Self {
Self {
client: redis::Client::open(redis_url).unwrap(),
key: key.to_string(),
value: Uuid::new_v4().to_string(),
ttl,
}
}

/// 尝试获取锁
pub async fn try_lock(&self) -> Result<bool, redis::RedisError> {
let mut conn = self.client.get_multiplexed_async_connection().await?;
let result: bool = redis::cmd("SET")
.arg(&self.key)
.arg(&self.value)
.arg("NX") // 仅当 key 不存在时设置
.arg("PX") // 过期时间(毫秒)
.arg(self.ttl.as_millis() as u64)
.query_async(&mut conn)
.await
.unwrap_or(false);
Ok(result)
}

/// 释放锁(Lua 脚本保证原子性)
pub async fn unlock(&self) -> Result<bool, redis::RedisError> {
let mut conn = self.client.get_multiplexed_async_connection().await?;
// Lua 脚本:只有 value 匹配才删除(防止误删别人的锁)
let script = redis::Script::new(
r#"
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
"#,
);
let result: i32 = script
.key(&self.key)
.arg(&self.value)
.invoke_async(&mut conn)
.await?;
Ok(result == 1)
}
}

RAII 锁守卫

/// 自动释放的锁守卫
pub struct LockGuard {
lock: RedisLock,
}

impl Drop for LockGuard {
fn drop(&mut self) {
// 在 Drop 中释放锁
let lock_key = self.lock.key.clone();
let lock_value = self.lock.value.clone();
let client = self.lock.client.clone();
tokio::spawn(async move {
// 异步释放
let mut conn = client.get_multiplexed_async_connection().await.unwrap();
let script = redis::Script::new(
"if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end"
);
let _: i32 = script.key(&lock_key).arg(&lock_value)
.invoke_async(&mut conn).await.unwrap_or(0);
});
}
}

分布式锁方案对比

方案实现优点缺点
Redis SETNX简单性能高非强一致
Redlock多 Redis 实例更可靠复杂,存在争议
etcd基于 Raft强一致性能较低
ZooKeeper临时节点成熟运维复杂

常见面试问题

Q1: Redis 分布式锁有什么问题?

答案

  1. 锁过期:任务未完成但锁已过期 → 使用看门狗续期
  2. Redis 故障:主从切换导致锁丢失 → Redlock 多节点
  3. 时钟偏差:不同节点时钟不同步 → 使用单调时钟
  4. 误删锁:其他客户端删掉自己的锁 → value 唯一标识 + Lua 脚本

相关链接