分布式锁的三种实现方式

2021/7/14 6:04:49

本文主要是介绍分布式锁的三种实现方式,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人。

文章不定期同步公众号,还有各种一线大厂面试原题、我的学习系列笔记。

eureka和nacos的区别

nacos eureka
应用 nacos是阿里巴巴的开源中间件,可以直接启动jar即可用 eureka需要连着springboot项目一起启动才可用
负载均衡 nacos默认提供权重设置功能,调整承载流量压力
心跳机制 nacos支持由客户端或服务端发起的健康检查 Eureka是由客户端发起心跳
负载均衡策略 用Ribion 用Ribion
dubbo和k8s的集成 支持 不支持
选型建议 希望引入alibaba生态圈;希望在线对服务上下线&在线流量管理 希望引入spring clound生态圈
一致性协议 支持AP+CP任一种实现 AP
动态配置 支持(方便管理所有环境的服务配置) 不支持

CAP理论

  • C = 一致性,consistency,任何时候的节点数据都是一样的
  • A = 可用性,avalibility,任何时候的节点请求不管成功失败都必须有回应
  • p = 容错性,partition tolerence,任何时候的数据丢失都不会影响系统正常运行

    任何分布式系统都无法同时满足CAP,只能满足其中两个,大部分IT公司只要求AP保证服务可用,并且在最终实现“C=最终一致性”即可,如何实现C最终一致性:分布式事务、分布式锁

幂等性

幂等性:多次重复请求/多次重复操作某一资源,产生的结果是一样的;对于数据库而言,幂等性就是多次重复地对数据库进行某一操作,得到的结果的一样的;对于接口而言,在设计的时候需要考虑幂等性,就是多次重复请求某一个接口,从接口处得到的结果是一样的

sql请求的幂等性
操作 是否幂等 示例
查询 select * from user where name='afei'
新增 insert into user(userid,name) values(1,'afei');若userid是主键,那这个sql就是幂等性的,因为只有第一次数据可以被插入,对数据库产生的结果是一样的;若userid不是主键,那这个sql就不是幂等性的,因为可以重复插入,对数据库产生的结果是不一样的

分布式锁

由上可知分布式锁主要用于解决CAP中的‘C’数据一致性问题:分布式环境中,可能存在多个进程竞争同一个资源,这就需要实现多进程间的“互斥锁”,在java中自带有实现线程间的互斥锁(Synchronized,Reentranlock),但是分布式环境下进程间的互斥需要自己实现,需要把这个“互斥锁”存在公共的地方被多个进程访问到,这样同一时刻只有一个进程能拿到“互斥锁”,进而保证了数据的一致性=‘C’,一般可用redis/zookeeper/数据库来实现分布式锁

基于数据库实现分布式锁

又分为两种方式:表锁、版本号机制

  • 表锁:创建一张表,当要操作某些资源时,先锁住这些资源,即把这些'资源记录'往表里面插入,解锁时删掉这些记录即可
CREATE TABLE `order`  (  //实现分布式锁的表
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `order_no` int(11) DEFAULT NULL comment `锁住的订单号资源`,
  PRIMARY KEY (`id`),
  unique key `unique_order_no`(`order_no`)
)ENGINE = INNODB

可知order_no为唯一性约束,当想锁住某个orderNo时->先把它插入表中->当有多个相同的order_no提交到数据库,只有一个能成功->想释放锁时,删除该条记录。可以先检查某个order_no是否在表中,不存在则插入,“检查+插入”应该放到同一个事务中:

  @Transactional  //“检查+插入”应该放到同一个事务中
  public boolean addOrder(int orderNo) {
    if(orderMapper.selectOrder(orderNo)==null){ //检查
               //order表不存在该条记录则插入,表示orderNo订单号被锁定
               int result = orderMapper.addOrder(orderNo);  
               if(result>0){
                      return true;
               }
         }
    return false;
  }
  
  public void fun(int orderNo){
      if(addOrder(orderNo)) {//拿到分布式锁
           //业务处理
          ...
          //处理完删除order表的orderNo记录,表示释放分布式锁
          orderMapper.delete(orderNo);
      }
  }
  • 版本号机制
CREATE TABLE `order`  (  //实现分布式锁的表
  `version` int(11) NOT NULL,  //版本号、
  `order_no` int(11) DEFAULT NULL comment `锁住的订单号资源`,
)ENGINE = INNODB

假如已存在version=1,orderNo='N1'的记录,则:

//先获取锁:

select version from order where order_no='N1';

//再占用锁:

udpate order ser version='2' where order_no='N1' and version='1';//更新成功则拿到锁

基于redis实现分布式锁

redis可以用【setnx(key,value)+设置expire】实现分布式锁,这是目前比较优的一种解决方案

public boolean fun(Jedis jedis,String key,String value,int expireTime){
    Long r=jedis.setnx(key,value);//若key不存在则保存(key,value)并返回1,代表拿到分布式锁;若key已存在则设置失败并返回0,代表分布式锁已被占用
    if(r==1){ //拿到分布式锁
    
        //设置锁的过期时间:从当前时点开始经过expireTime(s)之后该key失效,key-value会被删除,代表分布式锁被释放;
        jedis.expire(key,expireTime);
        return true;//拿到true可以向下执行业务操作
    }
    return false;
}

产生的问题:

  • 如果在【拿到锁,设置锁的过期时间】之间,程序奔溃,则设置锁的过期时间失败,也即释放锁失败,其他进程读取分布式锁失败,造成了死锁,解决:redis2.6.12之后提供一次性完成setnx和expireTime设置操作
//redis2.6.12之后:setnx提供了expireTime参数,可以一步设置过期时间
public boolean fun(Jedis jedis,String key,String value,int expireTime){
    String r=jedis.setnx(key,value,"NX","PX",expireTime);//分布式锁已被占用
    if("OK".equals(e)){ //拿到分布式锁
        //设置过期时间释放分布式锁
        jedis.expire(key,expireTime);
        return true;
    }
}
  • 拿到锁之后,如果进行业务操作时间太长而expireTime设置太短,则会造成原来只有拿到锁才能执行的代码块失效,解决:设置expireTime之后,马上启动一个定时器,在expireTime快要到来之前,用lua原子性脚本删除原锁并重新设置新锁和新的到期时间(删除原key-value,再重新setnx设key-value和expireTime);
  • 在redis集群下,主节点负责写,从节点负责读,由于主从复制是异步的,所以中间有一定的时间差,如果客户A从主节点获取到锁之后,还没来得及同步信息到其他的从节点,主节点就挂了,这时客户B再拿锁就会从 从节点获取到这个把锁,此时两个客户同时获取到了锁,解决:(待讨论)
  • 锁的可重入问题,因为setnx是保证锁唯一的,如果拥有锁的进程想再次获取到该锁,就会失败,解决:当前进程用lua原子脚本把原锁删了,重新设置新锁+新expireTime(删除原key-value,再重新setnx设key-value和expireTime);
基于zookeeper实现分布式锁

先创建一个持久型父节点,每当客户端们想竞争访问共享资源时,都会在该父节点下新建一个临时有序的子节点->

不断地会有新的临时有序子节点被创建->

后面来的客户端在访问资源时,会检查自己创建的节点是否序号最小,若最小则获取锁,否则阻塞等待->

被阻塞等待的节点均会获取到上一个节点,并为上一节点注册watch事件监听节点是否还存在->

等到上一节点使用完共享资源,则会删除自身,进而触发watch事件被下一节点监听到,下一节点重复上面步骤:检查自己序号是否最小......

OK,如果文章哪里有错误或不足,欢迎各位留言。

创作不易,各位的「三连」是二少创作的最大动力!我们下期见!
分布式锁的三种实现方式



这篇关于分布式锁的三种实现方式的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程