分布式锁(MySQL、redis、zookeeper)

分布式锁

分布式锁应该具有:

  • 互斥性:任意时刻只能有一个客户端持有锁
  • 锁超时释放: 锁超时会自动释放,防止死锁
  • 可重入性: 一个线程获取锁之后可以再次对请求加锁
  • 高可用、高性能:加锁和解锁需要开销尽可能低,同时要保证高可用
  • 安全性:锁只能被持有客户端删除,不能被其他客户端删除

为什么需要分布式锁?

在分布式系统中,多个节点(进程、服务实例)同时访问共享资源时,会面临一些并发控制的问题。这些问题包括数据一致性、竞态条件和资源冲突等。分布式锁的出现正是为了解决这些问题,确保在分布式环境下的数据访问的正确性和可靠性

分布式锁的实现方法

在分布式架构当中,单机模式下的Java对象锁已经无法起作用了(tomcat当中每个tomcat的服务器都能够持有一个锁,那么这个锁就没有意义了),所以需要一个独立的第三方来实现分布式锁,MySQL、Redis、zookeeper等等

MySQL分布式锁

  • 缺点:读取IO消耗大,获取锁的线程如果崩溃了没有释放锁,将会产生死锁
    实现步骤:
  1. 每次操作资源的时候插入一条数据(获取当前时间戳,利用定时任务反复读取表中时间是否超时,超时释放,用于防止线程崩溃后未释放。获取锁的线程应该每隔一段时间进行更新时间戳,防止自身并未崩溃但操作超时锁被另一个线程获取,当自身完成时释放了别的客户端的锁)
insert into locks(lock_key,locktimeout,...)values('stock','30',...)

2、操作完成后删除锁(根据lock_key实现可重入性,可以在表中增加一个计数器每次重入加锁则加1,释放则减1,当计数器为0时则代表操作完成,删除表数据)

Redis分布式锁

redis基于内存,所以在每次IO操作的时候比MySQL节省更多的时间,同时redis自身支持过期时间自动释放,所以利用Redis实现分布式锁会比MySQL更加合适
前置知识:

  • SET key value[EX seconds][PX milliseconds][NX|XX]
  • NX :表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁,才能获取。
  • EX seconds :设定key的过期时间,时间单位是秒。
  • PX milliseconds: 设定key的过期时间,单位为毫秒
  • XX 仅当key存在时设置值

最简单的实现如下(伪代码):

if(jedis.set(key_resource_id, uni_request_id, "NX", "EX", 100s) == 1){ //加锁
    try {
        do something  //业务处理
    }catch(){
  }
  finally {
       jedis.del(key_resource_id); //释放锁
    }
}

目前很多企业或者项目里使用lua脚本加锁,这边也进行叙述一下

if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
   redis.call('expire',KEYS[1],ARGV[2])
else
   return 0
end;

lua脚本操作redis主要是因为lua脚本保证了原子性(后续删锁还会用到),在不使用set扩展命令时(NX、EX等),如果是如下代码,就可能导致加锁后 还未来得及设置过期时间,线程崩溃了导致产生了永久锁

if(jedis.setnx(key_resource_id,lock_value) == 1){ //加锁
    expire(key_resource_id,100); //设置过期时间
...

最后为了保证防止其他线程误删,我们需要在finally上面进行一些操作

/*
    if (uni_request_id.equals(jedis.get(key_resource_id))) {
        jedis.del(lockKey); //释放锁
    }
    和前面的操作同理,该操作是非原子性的,所以我们要采用lua脚本进行操作
*/
if redis.call('get',KEYS[1]) == ARGV[1] then 
   return redis.call('del',KEYS[1]) 
else
   return 0
end;

Zookeeper分布式锁

Zookeeper底层是类似于文件系统那样的树结构(称为ZNode),这种树状结构和基于ZNode的数据模型使得ZooKeeper非常适合用于实现分布式协调和同步的场景,例如分布式锁、选举算法等。客户端可以通过创建、读取、更新和删除ZNode来实现对共享数据和协调状态的访问和操作。

下面是ZooKeeper实现分布式锁的基本原理和步骤:

  1. 客户端在ZooKeeper指定的目录下创建一个有序临时节点,代表自己的请求。
  2. 客户端获取目录下所有的子节点,并按节点名称的顺序进行排序。
  3. 客户端判断自己创建的节点是否是当前最小的节点,如果是,则认为获取到了锁;如果不是,则监听比自己小的节点的删除事件。
  4. 如果监听的节点被删除,客户端回到第2步重新判断自己是否获得了锁。
  5. 客户端在完成任务后,删除自己创建的节点,释放锁。

至于ZooKeeper为什么能够实现分布式锁的原因有以下几点:

  1. 有序节点:ZooKeeper的节点是有序的,可以根据节点名称的顺序来实现竞争顺序。通过对节点名称进行排序,客户端可以判断自己是否是当前最小的节点,从而决定是否获得锁。
  2. 临时节点:ZooKeeper的临时节点是会话级别的,当创建节点的客户端会话结束时,该节点会被自动删除。利用临时节点,可以实现锁的自动释放,避免锁被长时间占用。
  3. Watch机制:ZooKeeper提供了Watch机制,客户端可以对节点的变化进行监听。通过监听比自己小的节点的删除事件,可以实现客户端之间的协调,确保只有最小节点的客户端获得锁。
  4. 总体来说,ZooKeeper通过有序节点和临时节点的特性,结合Watch机制,提供了一种简单而可靠的机制来实现分布式锁。客户端通过竞争创建有序临时节点,并监听前一个节点的删除事件,从而实现分布式环境下的互斥访问和协调操作。

具体实现可以参考 Zookeeper实现分布式锁

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
手写Redis分布式锁的一个常见方法是使用Redis的SETNX命令和EXPIRE命令。SETNX用于设置一个键值对,只有在键不存在的情况下才能设置成功,用于表示获取锁的操作。EXPIRE用于设置键的过期时间,确保在获取锁的客户端崩溃或网络故障的情况下,锁最终会被释放。 下面是一个用C语言手写Redis分布式锁的简单示例代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/time.h> #include <sys/wait.h> #include <signal.h> #include <errno.h> #include <hiredis/hiredis.h> #define REDIS_HOST "localhost" #define REDIS_PORT 6379 #define LOCK_KEY "my_lock" #define LOCK_EXPIRE_TIME 10 int main() { pid_t child_pid; int status; // 创建子进程 child_pid = fork(); if (child_pid == 0) { // 子进程获取锁 redisContext *redis_conn = redisConnect(REDIS_HOST, REDIS_PORT); if (redis_conn == NULL || redis_conn->err) { printf("连接Redis失败\n"); exit(1); } // 设置锁 redisReply *reply = redisCommand(redis_conn, "SETNX %s 1", LOCK_KEY); if (reply == NULL || reply->type == REDIS_REPLY_ERROR || reply->integer != 1) { printf("获取锁失败\n"); exit(1); } // 设置锁的过期时间 reply = redisCommand(redis_conn, "EXPIRE %s %d", LOCK_KEY, LOCK_EXPIRE_TIME); if (reply == NULL || reply->type == REDIS_REPLY_ERROR || reply->integer != 1) { printf("设置锁的过期时间失败\n"); exit(1); } printf("获取锁成功\n"); // 模拟业务操作 sleep(5); // 释放锁 reply = redisCommand(redis_conn, "DEL %s", LOCK_KEY); if (reply == NULL || reply->type == REDIS_REPLY_ERROR || reply->integer != 1) { printf("释放锁失败\n"); exit(1); } printf("释放锁成功\n"); // 关闭Redis连接 redisFree(redis_conn); exit(0); } else if (child_pid > 0) { // 等待子进程结束 waitpid(child_pid, &status, 0); if (WIFEXITED(status)) { printf("子进程正常结束\n"); } else if (WIFSIGNALED(status)) { printf("子进程异常结束\n"); } } else { printf("创建子进程失败\n"); exit(1); } return 0; } ``` 在这个示例中,使用了 hiredis 库来连接 Redis,并通过 SETNX 和 EXPIRE 命令实现分布式锁的获取和释放。主进程创建一个子进程,子进程尝试获取锁并进行业务操作,然后释放锁。主进程等待子进程的结束并打印相应信息。 注意:这只是一个简单的示例,实际应用中可能需要考虑更多的场景,比如锁的重入、超时处理等。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [分布式锁_Redis分布式锁+Redisson分布式锁+Zookeeper分布式锁+Mysql分布式锁(原版)](https://blog.csdn.net/guan1843036360/article/details/127827270)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值