分布式锁实现方法

2023-09-21 20:34:12

分布式锁

什么时候需要加锁

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

场景:

电商系统,下单流程:用户下单–>秒杀系统检查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

更多推荐

清华博士面试的准备(已通过)

内修(30%)不管如何任何人都不能影响你的心态。因为冷静、理性,才能处理好95%以上的问题。剩下的5%我可以不拥有。不能既要、又要、还要。尊重客观规律。放下我执。价值导向、解决问题为导向。允许一切事情的发生,是我们最大的底牌。我是谁不重要,你是谁也不重要,重要的是大家一起做了什么事情。见天地、见众生、见自己每个人都了不

vue-h5:移动Web单击事件和延迟300ms的问题

在PC端的网页,大部分的交互是通过click事件来实现的,然而在移动端,则是通过touch事件来实现触摸交互。单击或者点击事件,指的是鼠标按下并且在短时间内放开【一般是小于300ms】。那么移动端,也是类似,在手指触摸到屏幕开始计算时间,并且在300ms内离开屏幕。这就是移动端的单击事件,手指触摸成为touch。tou

数据组合利器:从入门到精通Python中的zip()函数应用

介绍zip()函数是Python内置的一个非常有用的函数,它可以将多个可迭代对象打包成一个元组构成的新的可迭代对象。本文将深入探讨zip()函数的用法,从入门到精通。目录zip()函数的基本用法使用zip()函数合并列表使用zip()函数进行解压缩zip()函数在循环中的应用不等长可迭代对象的处理zip()函数与*操作

轻松搞定Spring集成缓存,让你的应用程序飞起来!

Spring集成缓存缓存接口开启注解缓存注解使用@Cacheable@CachePut@CacheEvict@Caching@CacheConfig缓存存储使用ConcurrentHashMap作为缓存使用Ehcache作为缓存使用Caffeine作为缓存主页传送门:📀传送Spring提供了对缓存的支持,允许你将数据

网络安全(黑客)自学

前言:想自学网络安全(黑客技术)首先你得了解什么是网络安全!什么是黑客网络安全可以基于攻击和防御视角来分类,我们经常听到的“红队”、“渗透测试”等就是研究攻击技术,而“蓝队”、“安全运营”、“安全运维”则研究防御技术。无论网络、Web、移动、桌面、云等哪个领域,都有攻与防两面性,例如Web安全技术,既有Web渗透,也有

Palantir的“英伟达时刻”即将到来

来源:猛兽财经作者:猛兽财经总结(1)由于投资者对生成式人工智能的兴趣持续增加,Palantir的股价一直在上涨。(2)Palantir已经连续三个季度实现了GAAP盈利,并将很快有资格被纳入标普500指数。(3)Palantir拥有非常健康的资产负债表,并授权了一项股票回购计划。(4)虽然市场已经消化了很多乐观情绪,

RocketMQ高性能核心原理与源码架构剖析

文章目录1、源码环境搭建1.1、主要功能模块1.2、源码启动服务1.2.1、启动nameServer1.2.2、启动Broker1.2.3、发送消息1.2.4、消费消息1、源码环境搭建1.1、主要功能模块​RocketMQ的官方Git仓库地址:https://github.com/apache/rocketmq可以用g

【数据结构初阶】三、 线性表里的链表(无头+单向+非循环链表)

=========================================================================相关代码gitee自取:C语言学习日记:加油努力(gitee.com)====================================================

读高性能MySQL(第4版)笔记10_查询性能优化(上)

1.三管齐下1.1.不做、少做、快速地做1.2.如果查询太大,服务端会拒绝接收更多的数据并抛出相应错误1.3.如果查询写得很糟糕,即使库表结构再合理、索引再合适,也无法实现高性能1.4.查询优化、索引优化、库表结构优化需要齐头并进,一个不落1.5.PerconaToolkit中的pt-archiver工具2.响应时间2

【vivo秋招0912】三、最少开发工时总和 <模拟>

三、最少开发工时总和某开发小组近期承接了多个研发项目,作为组长的你需要为员工分配工作任务。具体要求如下:项目划分到的任务工时用二维数组tasks表示,其中tasks[i][j]表示的是第i个项目中第j个任务的开发工时;现在组内员工有n个,每个工作任务只能分配给一位员工,一位员工可以被分配多个任务,一个任务完成才能进行下

【用unity实现100个游戏之12】unity制作一个俯视角2DRPG《类星露谷物语》资源收集游戏demo

文章目录前言加快编辑器运行速度素材(1)场景人物(2)工具一、人物移动和动画切换二、走路灰尘粒子效果探究实现三、树木排序设计方法一方法二四、绘制拿工具的角色动画五、砍树实现六、存储拾取物品引入Unity的可序列化字典类七、实现靠近收获物品自动吸附八、树木被砍掉的粒子效果九、新增更多可收集物十、更多工具切换十一、扩展源码

热文推荐