分布式锁实现方法

分布式锁

什么时候需要加锁

  • 有并发,多线程
  • 有写操作
  • 有竞争关系

场景:

电商系统,下单流程:用户下单–>秒杀系统检查redis商品库存信息–>用户锁定并更新库存(mysql)—>秒杀系统更新redis

问题:单机部署,单线程执行无问题,多线程并发操作会引起超卖

解决:对用户下单后的步骤加锁,让线程排队,避免超卖(synchronized 或reentrantLock)

问题:单机部署变为多机部署时,仍然有超卖现象。因为synchronized和reentrantLock都只作用于自己的jvm

解决:使用分布式锁。可以基于Mysql、redis、zookeeper、consule等进行实现

部署:通常将锁和应用分开部署,把这个锁作为一个公用的组件,然后多个不同应用的不同节点,都去共同访问这个组件。

分布式锁解决方法:

1.基于数据库实现

1.1基于数据库表实现

新建表,记录当前哪个程序正在使用数据

步骤:

  • 程序访问数据时,将程序的编号(insert)存入表。
  • 当insert成功,代表该程序获得了锁,即可执行逻辑。
  • 当程序编号相同的其他程序进行insert时,由于主键冲突会导致insert失败,则代表获取锁失败。
  • 获取锁成功的程序在逻辑执行完以后,删除该数据,代表释放锁。

1.2基于条件

MySQL乐观锁:思想就是利用MySQL的InnoDB引擎的行锁机制来完成。

乐观锁的实现分为根据条件根据版本号

  • 根据条件

    • @Update("update tb_book set stock=stock-#{saleNum} where id = #{id} and stock-#{saleNum}>=0")
          void updateNoLock(@Param("id") int id, @Param("saleNum") int saleNum);
      
  • 根据版本号,更新成功后版本号+1

    • @Update("update tb_book set name=#{name},version=version+1 where id=#{id} and version=#{version}")
          int updateByVersion(@Param("id")int id,@Param("name")String name,@Param("version")int version);
      
2.zookeeper分布式锁

实现思想:内部主要是利用znode节点特性和watch机制完成。

  • znode节点
    • 持久节点:一旦创建,则永久存在于zookeeper中,除非手动删除。
    • 持久有序节点:一旦创建,则永久存在于zookeeper中,除非手动删除。同时每个节点都会默认存在节点序号,每个节点的序号都是有序递增的。如demo000001、demo000002…demo00000N。
    • 临时节点:当节点创建后,一旦服务器重启或宕机,则被自动删除。
    • 临时有序节点:当节点创建后,一旦服务器重启或宕机,则被自动删除。同时每个节点都会默认存在节点序号,每个节点的序号都是有序递增的。如demo000001、demo000002…demo00000N。
  • watch监听机制
    • watch监听机制主要用于监听节点状态变更,用于后续事件触发,假设当B节点监听A节点时,一旦A节点发生修改、删除、子节点列表发生变更等事件,B节点则会收到A节点改变的通知,接着完成其他额外事情。
  • 实现原理:
    • 思想是当某个线程要对方法加锁时,首先会在zookeeper中创建一个与当前方法对应的父节点
    • 接着每个要获取当前方法的锁的线程,都会在父节点下创建一个临时有序节点,因为节点序号是递增的,所以后续要获取锁的线程在zookeeper中的序号也是逐次递增的。
    • 根据这个特性,当前序号最小的节点一定是首先要获取锁的线程,因此可以规定序号最小的节点获得锁。所以,每个线程再要获取锁时,可以判断自己的节点序号是否是最小的,如果是则获取到锁。当释放锁时,只需将自己的临时有序节点删除即可。
    • 在并发下,每个线程都会在对应方法节点下创建属于自己的临时节点,且每个节点都是临时且有序的。每当添加一个新的临时节点时,其都会基于watcher机制监听着它本身的前一个节点等待前一个节点的通知,当前一个节点删除时,就轮到它来持有锁了。然后依次类推。

原理剖析

低效实现

  • 流程:开始事务–>获取锁–>创建锁节点类型:临时节点–>创建成功获得锁,不成功锁节点已经存在,监听锁节点删除
  • 羊群效应:低效点–只有一个锁节点,其他线程都会监听同一个锁节点,一旦锁节点释放后,其他线程都会收到通知,然后竞争获取锁节点。这种大量的通知操作会严重降低zookeeper性能,对于这种由于一个被watch的znode节点的变化,而造成大量的通知操作,叫做羊群效应。

高效实现

让获取锁的线程产生排队,后一个监听前一个,依次排序。推荐使用这种方式实现分布式锁。

流程:开始事务–>获取锁–>判断父节点是否存在–>不存在创建父节点–>创建临时有序节点–>获取锁判断自身是否为序号最小节点—>是最小节点,获取锁成功,—>不是最小节点,监控比本节点序号-1的节点,阻塞等待节点删除通知 -->收到前置节点删除通知–>回到获取锁判断是不是为序号最小的节点

结果:会在根节点下为每一个等待获取锁的线程创建一个对应的临时有序节点,序号最小的节点会持有锁,并且后一个节点只监听其前面的一个节点,从而可以让获取锁的过程有序且高效。

3.redis分布式锁

3.1单节点Redis实现分布式锁

核心API:

  • setnx():向redis中存key-value,只有当key不存在时才会设置成功,否则返回0,体现互斥性
  • expire():设置key的过期时间,用于避免死锁出现
  • delete():删除key,用于释放锁

问题:

  • 编写工具类注意释放锁的原子性
  • 锁续期:在创建锁的同时创建一个守护线程,同时定义一个定时任务每隔一段时间去为未释放的锁增加过期时间,当业务执行完,释放锁后,再关闭守护线程,这种实现思想可以解决锁续期(业务没执行完,锁过期)
  • 服务单点&集群问题:单点redis可以完成锁操作,可一旦redis服务节点挂掉了,则无法提供锁操作
    • 生产环境,为保证redis高可用,采用异步复制方法进行主从部署,主节点写入数据异步复制给从节点,当主节点宕机,从节点升级为主节点。

3.2 redission实现分布式锁

  • 单机实现:getLock,lock,unlock
    • 多线程并发获取锁时,当一个线程获取到锁,其他线程则获取不到,并内部会不断尝试获取锁,当持有锁的线程将锁释放后,其他线程则会继续去竞争锁。
  • 看门狗
    • 不需要对锁key设置过期时间,当过期时间为-1时,会启动一个定时任务,在业务释放锁前,会一直不停的增加这个锁的有效时间,从而保证在业务执行完毕之前,这把锁不会被提前释放掉
    • 实现:将lock改为tryLock。 在lock源码中,如果没有设置锁超时,默认过期时间是30秒,即watchdog每隔30秒来进行一次续期,值可修改
  • 红锁
    • 考虑将redis配置为主从结构,在主从结构中,数据复制是异步实现的。假设在主从结构中,master会异步将数据复制到slave中,一旦某个线程持有了锁,在还没有将数据复制到slave时,master宕机。则slave会被提升为master,但被提升为slave的master中并没有之前线程的锁信息,那么其他线程则又可以重新加锁。
    • redlock:基于多节点redis实现分布式锁的算法,可以有效解决redis单点故障的问题
    • 实现过程:
      • 记录获取锁前的当前时间
      • 使用相同的key,value获取所有redis实例中的锁,并且设置获取锁的时间要远远小于锁自动释放的时间。假设锁自动释放时间是10秒,则获取时间应在5-50毫秒之间。通过这种方式避免客户端长时间等待一个已经关闭的实例,如果一个实例不可用了,则尝试获取下一个实例。
      • 客户端通过获取所有实例的锁后的时间减去第一步的时间,得到的差值要小于锁自动释放时间,避免拿到一个已经过期的锁。并且要有超过半数的redis实例成功获取到锁,才算最终获取锁成功。如果不是超过半数,有可能出现多个客户端重复获取到锁,导致锁失效。
      • 当已经获取到锁,那么它的真正失效时间应该为:过期时间-第三步的差值。
      • 如果客户端获取锁失败,则在所有redis实例中释放掉锁。为了保证更高效的获取锁,还可以设置重试策略,在一定时间后重新尝试获取锁,但不能是无休止的,要设置重试次数。
redis和zookeeper分布式锁对比

redis缺点

  • 采用抢占式方式进行锁的获取,需要不断的在用户态进行CAS尝试获取锁,对CPU占用率高。
  • redis本身并不是CP模型,即便采用了redlock算法,但仍然无法保证百分百不会出现问题,如持久化问题。对于redis分布式锁的使用,在企业中是非常常见的,绝大多数情况不会出现极端情况。

zookeeper实现分布式的优点在于其是强一致性的,采用排队监听的方式获取锁,不会像redis那样不断进行轮询尝试,对性能消耗较小。其缺点则是如果频繁的加锁和解锁,对zk服务器压力较大。

当进行技术选型时,应该对其优缺点结合公司当前情况进行考虑。 如果公司有条件使用zk集群,更推荐使用zk的分布式锁,因为redis实现分布式锁有可能出现数据不正确的情况,但如果公司没有zk集群,使用redis集群完成分布式锁也无可厚非。

参考:

https://blog.csdn.net/poizxc2014/article/details/123963250

https://blog.csdn.net/q66562636/article/details/124862795

你可能感兴趣的:(分布式,分布式)