什么是分布式锁
分布式场景下互斥类型的锁
解决什么问题
在分布式系统中协调多个进程/服务对共享资源的访问
futex:
快速用户空间互斥体,增加在用户态原子检查来决定是否陷入内核进行等待。
当没有发生竞争时,将在用户空间快速完成任务,当竞争发生时,将通过系统调用进入内核进行处理。
系统调用 syscall futex
futex wait 操作 插入等待队列,挂起当前线程 如果超时参数,创建定时任务,定时唤醒线程
futex wake 操作 遍历等待队列,唤起等待线程
怎么解决的?通过CAS操作和futex
分布式中DB存储锁
加锁对象和解锁对象必须为同一个除了因为网络异常而造成锁超时情况
此时需要“超进程”来解决
分布式锁的特性:互斥性 锁超时 可用性 容错性
RedLock的实现:
时钟漂移:
不同机器或进程之间的系统时钟因物理位置、硬件差异或网络时间协议(NTP)同步等问题
导致的时间流逝速度不一致现象。
重试机制:提高锁获取成功的概率
原子操作:使用 lua 脚本保证解锁和续锁操作的原子性
RedLock.h
#pragma once
#include <hiredis/hiredis.h>
#include <random>
#include <string>
#include <vector>
struct Lock {
Lock() : valid_time_(0) {}
Lock(const std::string &resource, const std::string &value, int valid_time)
: resource_(resource), value_(value), valid_time_(valid_time) {}
std::string resource_;
std::string value_; // 记录谁持有锁
int valid_time_; // 锁的有效时间
};
class RedLock {
public:
bool set_retry_count(int count);
bool add_server(const std::string& host, int port, std::string &err);
bool lock(const std::string& resource, int ttl_ms, Lock& lock);
bool unlock(const Lock &lock);
bool continue_lock(const std::string& resource, int ttl_ms, Lock& lock);
private:
bool lock_instance(redisContext* context, const std::string& resource, const std::string& value, int ttl_ms);
bool unlock_instance(redisContext* context, const std::string& resource, const std::string& value);
bool continue_lock_instance(redisContext* context, const std::string& resource, const std::string& value, int ttl_ms);
static constexpr float DEFAULT_LOCK_DRIFT_FACTOR = 0.01f; // Time drift factor
static constexpr int DEFAULT_LOCK_RETRY_COUNT = 3; // Number of retries
static constexpr int DEFAULT_LOCK_RETRY_DELAY = 200; // Delay between retries in milliseconds
std::vector<redisContext*> servers_;
int quorum_;
int retry_count_ = DEFAULT_LOCK_RETRY_COUNT;
int retry_delay_ms_ = DEFAULT_LOCK_RETRY_DELAY;
std::mt19937 rng_; // 随机数生成器
const std::string UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then "
"return redis.call('del', KEYS[1]) "
"else "
"return 0 "
"end";
const std::string CONTINUE_LOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then "
"return redis.call('pexpire', KEYS[1], ARGV[2]) "
"end";
};
RedLock.cc
#include "RedLock.h"
#include <bits/types/struct_timeval.h>
#include <chrono>
#include <random>
#include <thread>
#include <cstring>
static int64_t get_current_time_ms() {
using namespace std::chrono;
return duration_cast<milliseconds>(system_clock::now().time_since_epoch()).count();
}
static std::random_device rd;
static std::string generate_unique_id() {
std::uniform_int_distribution<int> dist;
uint32_t buffer[5];
for (auto& num : buffer) num = dist(rd);
char buf[41] = {0};
::snprintf(buf, sizeof(buf), "%08x%08x%08x%08x%08x",
buffer[0], buffer[1], buffer[2], buffer[3], buffer[4]);
return std::string(buf, 40);
}
bool RedLock::add_server(const std::string& host, int port, std::string &err) {
// 检查 redis 服务器是否已经存在
for (const auto &ctx : servers_) {
if (ctx->err == 0 && ctx->tcp.host == host && ctx->tcp.port == port) {
err = "Redis server already exists";
return false;
}
}
struct timeval timeout = {1, 500000}; // 1.5 seconds
redisContext* context = redisConnectWithTimeout(host.c_str(), port, timeout);
if (context == nullptr || context->err) {
if (context) {
err = std::string(context->errstr);
redisFree(context);
} else {
err = "Redis connection error: can't allocate redis context";
}
return false;
}
servers_.push_back(context);
quorum_ = (servers_.size() / 2) + 1;
return true;
}
bool RedLock::lock(const std::string& resource, int ttl_ms, Lock& lock) {
if (servers_.empty()) {
return false;
}
std::string value = generate_unique_id();
int attempts = retry_count_ + 1;
while (attempts -- > 0) {
int64_t start_time = get_current_time_ms();
int success_count = 0;
for (auto &ctx : servers_) {
if (lock_instance(ctx, resource, value, ttl_ms)) {
++success_count;
}
}
int64_t drift = static_cast<int64_t>(ttl_ms * DEFAULT_LOCK_DRIFT_FACTOR) + 2;
int64_t elapsed_time = get_current_time_ms() - start_time;
int64_t valid_time = ttl_ms - elapsed_time - drift;
if (success_count >= quorum_ && valid_time > 0) {
lock = Lock(resource, value, valid_time);
return true;
}
for (auto &ctx : servers_) {
unlock_instance(ctx, resource, value);
}
if (attempts > 0) {
std::uniform_int_distribution<int> dist(0, retry_delay_ms_);
std::this_thread::sleep_for(std::chrono::milliseconds(dist(rd)));
}
}
return false;
}
bool RedLock::lock_instance(redisContext* context, const std::string& resource, const std::string& value, int ttl_ms) {
auto * reply = (redisReply*)redisCommand(context, "SET %s %s NX PX %d", resource.c_str(), value.c_str(), ttl_ms);
if (!reply) {
return false;
}
bool ok = (reply->type == REDIS_REPLY_STATUS && ::strcmp(reply->str, "OK") == 0);
freeReplyObject(reply);
return ok;
}
bool RedLock::unlock(const Lock &lock) {
if (servers_.empty()) {
return false;
}
for (auto &ctx : servers_) {
unlock_instance(ctx, lock.resource_, lock.value_);
}
return true;
}
bool RedLock::unlock_instance(redisContext* context, const std::string& resource, const std::string& value) {
const char* argv[] = {
"EVAL", UNLOCK_SCRIPT.c_str(), "1", resource.c_str(), value.c_str()
};
redisReply* reply = (redisReply*)redisCommandArgv(context, sizeof(argv) / sizeof(argv[0]), argv, nullptr);
if (!reply) {
return false;
}
bool ok = (reply->type == REDIS_REPLY_INTEGER && reply->integer == 1);
freeReplyObject(reply);
return ok;
}
bool RedLock::continue_lock(const std::string& resource, int ttl_ms, Lock& lock) {
if (servers_.empty()) {
return false;
}
int attempts = retry_count_ + 1;
while (attempts -- > 0) {
int64_t start_time = get_current_time_ms();
int success_count = 0;
for (auto &ctx : servers_) {
if (continue_lock_instance(ctx, resource, lock.value_, ttl_ms)) {
++success_count;
}
}
int64_t drift = static_cast<int64_t>(ttl_ms * DEFAULT_LOCK_DRIFT_FACTOR) + 2;
int64_t elapsed_time = get_current_time_ms() - start_time;
int64_t valid_time = ttl_ms - elapsed_time - drift;
if (success_count >= quorum_ && valid_time > 0) {
lock.valid_time_ = static_cast<int>(valid_time);
return true;
}
if (attempts > 0) {
std::uniform_int_distribution<int> dist(0, retry_delay_ms_);
std::this_thread::sleep_for(std::chrono::milliseconds(dist(rd)));
}
}
return false;
}
bool RedLock::continue_lock_instance(redisContext* context, const std::string& resource, const std::string& value, int ttl_ms) {
std::string ttl_ms_str = std::to_string(ttl_ms);
const char* argv[] = {
"EVAL", CONTINUE_LOCK_SCRIPT.c_str(), "1", resource.c_str(), value.c_str(), ttl_ms_str.c_str()
};
redisReply* reply = (redisReply*)redisCommandArgv(context, sizeof(argv) / sizeof(argv[0]), argv, nullptr);
if (!reply) {
return false;
}
bool ok = (reply->type == REDIS_REPLY_INTEGER && reply->integer == 1);
freeReplyObject(reply);
return ok;
}
bool RedLock::set_retry_count(int count) {
if (count < 0) {
return false;
}
retry_count_ = count;
return true;
}