1. 引言
在分布式系统中,多个客户端可能同时访问和操作共享资源。为了防止数据竞争和不一致,分布式锁是一个常见的解决方案。Redis 提供了强大的功能来实现高效且可靠的分布式锁。本文将通过 C++ 和 Redis(通过 hredis 库)实现一个简单的分布式锁。
2. 项目背景
- 分布式锁:它是一种机制,用于保证在分布式系统中,某一时刻只有一个客户端能够执行某些共享资源的操作。
- 使用 Redis 作为锁存储:Redis 被用作集中式存储,可以确保锁的状态在所有参与者之间同步。
3. 设计思路
分布式锁的核心思想是:
- 加锁:客户端通过设置一个 Redis 键来获取锁。通过 Redis 的原子操作,确保只有一个客户端能够成功设置该键。
- 持有者标识符:每个客户端在加锁时生成一个唯一的标识符(UUID),该标识符用来标识锁的持有者。
- 超时机制:锁会在一定时间后自动释放(过期),防止因程序异常导致的死锁。
- 解锁:只有锁的持有者才能释放锁,这通过 Redis Lua 脚本来保证。
4. 代码实现步骤
4.1 生成全局唯一标识符 (UUID)
使用 Boost UUID 库生成一个全局唯一的标识符(UUID)。这个标识符会被用作锁的持有者标识符。它确保每个客户端在加锁时拥有唯一的标识,从而能够确保锁的唯一性。
代码:
1 | std::string generateUUID() { |
4.2 尝试加锁(acquireLock 函数)
客户端通过 Redis 的 SET 命令尝试加锁。该命令的参数如下:
NX:确保只有当键不存在时才能成功设置(即,只有一个客户端能够成功设置锁)。EX:设置一个超时时间,锁会在超时后自动释放,避免死锁。
如果加锁成功,返回一个唯一标识符。如果加锁失败,则会在指定的超时时间内多次尝试。
代码如下:
1 | // 尝试获取锁,返回锁的唯一标识符(UUID),如果获取失败则返回空字符串 |
函数参数说明
redisContext* context
这是一个指向 Redis 连接上下文的指针,用于与 Redis 服务器通信。通过这个上下文,你可以发送命令和接收响应。
const std::string& lockName
这是你想要加锁的资源名称。例如,如果你需要对某个资源加锁,可以用 "my_resource",函数内部会把它拼接成 "lock:my_resource" 作为 Redis 中的 key。
int lockTimeout
这是锁的有效期,单位是秒。设置这个值的目的是防止因程序异常或崩溃而导致的死锁。当锁达到这个超时时间后,Redis 会自动删除这个 key,从而释放锁。
int acquireTimeout
这是获取锁的最大等待时间,单位也是秒。如果在这个时间内没有成功获取到锁,函数就会停止尝试,并返回空字符串。这样可以避免程序无限等待。
Redis 命令解释
在 acquireLock 函数中,使用的 Redis 命令格式是:
1 | "SET %s %s NX EX %d" |
这个命令实际上是一个格式化字符串,参数会被填入以下位置:
- SET
Redis 的基本命令,用于设置一个 key 的值。 - %s(第一个 %s)
代表锁的 key(例如"lock:my_resource")。 - %s(第二个 %s)
代表锁的持有者标识符,也就是通过generateUUID()生成的 UUID。 - NX
表示 “Not eXists”,意思是“只有当 key 不存在时才进行设置”。这可以保证如果其他客户端已经设置了这个 key(即已经有锁了),那么当前客户端就不会覆盖原来的锁。 - EX %d
EX参数用于指定 key 的过期时间,%d表示锁的有效期(lockTimeout),单位为秒。这样即使客户端因某些原因没有正常释放锁,锁也会在指定时间后自动失效。
构造锁的 Redis 键
1 | std::string lockKey = "lock:" + lockName; |
- 构造出 Redis 键,格式为
lock:lockName,用于存储锁的状态。
设置获取锁的截止时间
1 | auto endTime = std::chrono::steady_clock::now() + std::chrono::seconds(acquireTimeout); |
- 设置一个截止时间
endTime,表示最多尝试获取锁的时间,单位为秒。当前时间加上acquireTimeout秒即为截止时间。
尝试获取锁
1 | while (std::chrono::steady_clock::now() < endTime) { |
- 通过一个
while循环,不断尝试获取锁,直到超时或成功获取锁。
使用 Redis 的 SET 命令尝试加锁
1 | redisReply* reply = (redisReply*)redisCommand(context, "SET %s %s NX EX %d", lockKey.c_str(), identifier.c_str(), lockTimeout); |
- 通过 Redis 的
SET命令来尝试获取锁,命令格式为:SET lockKey identifier NX EX lockTimeout:NX:只有当lockKey不存在时才会设置成功(即实现了锁的功能,防止其他客户端重入)。EX lockTimeout:设置锁的过期时间为lockTimeout秒,防止锁永远占用。
- 如果锁成功获取,Redis 会返回
OK。
检查返回结果
1 | if (reply != nullptr) { |
- 如果 Redis 返回的
reply不为空,检查返回值类型是否为REDIS_REPLY_STATUS,并且返回的字符串是否是OK,表示锁成功获取。 - 如果获取锁成功,释放
redisReply对象,并返回生成的唯一标识符identifier,表示锁已经成功获得。
暂停并重试
1 | std::this_thread::sleep_for(std::chrono::milliseconds(1)); |
- 如果获取锁失败,则通过
std::this_thread::sleep_for暂停 1 毫秒,避免忙等待,提高 CPU 的利用率。
超时返回空字符串
1 | return ""; |
- 如果在指定的
acquireTimeout时间内没有成功获取锁,函数返回空字符串,表示获取锁失败。
4.3 释放锁(releaseLock 函数)
释放锁的操作使用 Redis Lua 脚本,确保只有持有锁的客户端才能释放锁。脚本通过判断当前锁的持有者是否与传入的标识符一致来决定是否删除锁。
Lua 脚本:
1 | if redis.call('get', KEYS[1]) == ARGV[1] then |
KEYS[1]:锁的 key(例如lock:my_resource)。ARGV[1]:客户端在加锁时生成的唯一标识符。- 如果当前锁的值(标识符)与传入的标识符一致,删除该锁。
Lua 脚本的作用是:
redis.call('get', KEYS[1]):从 Redis 获取lockKey对应的值。if redis.call('get', KEYS[1]) == ARGV[1]:检查获取到的值是否与传入的identifier相同,只有标识符匹配时才能删除锁。return redis.call('del', KEYS[1]):如果匹配,执行删除操作,释放锁。else return 0:如果标识符不匹配,返回0,表示没有成功释放锁。
代码:
1 | // 释放锁,只有锁的持有者才能释放,返回是否成功 |
函数参数说明
redisContext* context:指向 Redis 连接上下文的指针,用于执行 Redis 命令。const std::string& lockName:锁的名称,用于生成 Redis 键的名称。const std::string& identifier:标识符,用于标识哪个客户端持有锁。
函数返回一个布尔值,通常表示释放锁操作是否成功。
执行 Lua 脚本
1 | redisReply* reply = (redisReply*)redisCommand(context, "EVAL %s 1 %s %s", luaScript, lockKey.c_str(), identifier.c_str()); |
- 使用
redisCommand函数执行 Redis 的EVAL命令,传入脚本、键的数量(在这里是 1,因为只有一个lockKey),然后依次传入lockKey和identifier。 redisCommand会返回一个redisReply指针,表示命令的返回结果。
处理返回结果
1 | cpp复制bool success = false; |
- 如果返回的
redisReply不为空,表示 Redis 执行了命令。 - 检查返回值的类型是否是整数(
REDIS_REPLY_INTEGER),并且它的值是否是 1。如果是 1,表示删除锁成功,将success设置为true。 - 释放
redisReply对象,防止内存泄漏。
4.4 主函数(main 函数)
主函数执行以下操作:
- 创建 Redis 客户端并连接到 Redis 服务器。
- 尝试加锁,若成功获取锁,则执行临界区代码。
- 在临界区代码执行完后,释放锁。
代码:
1 | int main() { |
5. 封装为单例类操作
类声明如下
1 |
|
类定义如下
1 |
|
测试用例
1 | int TestDisLock() { |
6. 多进程测试
我们可以创建另一个项目,调用之前生成好的distribute.exe
1 |
|
测试效果

7. 总结
- 分布式锁:使用 Redis 和 Boost UUID 实现了一个简单的分布式锁,确保多个客户端可以同步地访问共享资源。
- 加锁:通过 Redis 的
SET命令,使用NX和EX参数确保只有一个客户端可以成功加锁。 - 解锁:通过 Lua 脚本确保只有锁的持有者能够释放锁,避免其他客户端误释放锁。
- 持有者标识符:每个客户端在加锁时生成一个唯一的标识符(UUID),它作为锁的持有者标识。