从零到一编码实现Redis分布式锁

分布式锁-这一篇全了解(Redis实现分布式锁完美方案) 前言 在某些场景中,多个进程必须以互斥的方式独占共享资源,这时用分布式锁是最直接有效的。 随着技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。在单机环境中,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 JAV... 阅读详情

有的同学,就是这么尿性。也能理解,不自己弄一下,怎么能理解透彻,那就一起来搞一下呗!

使用场景和选型

分布式多节点的部署方式,使得共享变量有可能被同时操作,遇到有数据一致性要求的情况,就需要采取全局锁定的措施来保障并发操作下的一致性要求,如,库存扣减操作、同一个商品的上下架和更新操作等等。

常见的,分布式锁采用Zookeeper和Redis来实现。怎么取舍呢?

生产环境下,性能往往被优先考虑,相比较各自的优缺点,综合考虑,我们一般更倾向于redis。

从0到1 实现分布式锁

step1: 加锁 和 解锁的基础能力构建

Jedis.set(key, value, params) 👏🏻
这个2.6之后新增的加强版set命令是真不错,解决了加锁时设置锁超时时间的原子诉求,防止服务宕机导致的死锁~

(1) 一个具有加锁解锁功能的分布式锁对象,最少要有 jedis客户端 、 对应的redis key 、 锁超时时间 :

//构建分布式锁对象
public class DistributedLock {
    private Jedis jedis;
    private String lockName;
    private long lockExpireSecond;

    public DistributedLock(Jedis jedis, String lockName, long lockExpireSecond) {
        this.jedis = jedis;
        this.lockName = lockName;
        this.lockExpireSecond = lockExpireSecond;
    }
}

(2) 利用jedis提供的SetParams ,对NX , PX 在jedis.set操作中一次性的原子的完成设置:

public void lock() throws BizException {
    String lockResult = null;
    try {
       //设置 NX PX 参数
       SetParams params = new SetParams();
       params.nx();
       params.px(TimeUnit.SECONDS.toMillis(lockExpireSecond));
       //执行加锁 , value 暂定 为固定字符串
       lockResult = this.jedis.set(this.lockName, "lockValue", params);

   } catch (Exception e) {
       LOG.error("lock error",e);
   }

   if ("OK".equals(lockResult)) {
       LOG.debug("locked success,lockName:{}",lockName);
   } else {
      throw new BizException("Get lock failed.");
   }
}

(3) 用jedis.del命令完成解锁:

 public boolean unlock() {
    boolean unlockResult=false;

   try {
       this.jedis.del(this.lockName);
       unlockResult=true;
   }catch (Exception e){
      LOG.error("unLock error",e);
   }
    return unlockResult;
}

step2: 加锁失败直接结束? 希望多试几次

从上面的构造函数和lock()实现,发现当前实现属于一锤子买卖,不成功便成仁。这其实不太满足我们的生产需求,很多场景下,业务执行速度是很快的,只要稍微等一等,就可以。那怎么办?

自定义重试次数和等待间隔,有限重试等待

//新增重试间隔属性
private long retryIntervalTime; 

//通过构造方法初始化重试间隔
public DistributedLock(Jedis jedis, String lockName, long lockExpireSecond, long retryIntervalTime) {
   ...略
   this.retryIntervalTime = retryIntervalTime;
}

//新增入参,加锁超时时间
public void lock(long timeout,TimeUnit unit) throws TimeoutException {
   String lockResult = null;
   try {
       //设置 NX PX 参数
       SetParams params = new SetParams();
       params.nx();
       params.px(TimeUnit.SECONDS.toMillis(lockExpireSecond));
            
      //加锁开始时间
      long startTime=System.nanoTime();
            
      //循环有限等待
      while (!"OK".equals(lockResult=this.jedis.set(this.lockName, "lockValue", params))&&!isTimeout(startTime,unit.toNanos(timeout))){
           Thread.sleep(retryIntervalTime);
     }

  } catch (Exception e) {
      LOG.error("lock error",e);
  }
        
  //修改抛出异常类型为超时异常
  if ("OK".equals(lockResult)) {
       LOG.debug("locked success,lockName:{}",lockName);
  } else {
      throw new TimeoutException("Get lock failed because of timeout.");
  }
}

step3: 只能解自己加的锁,别人的锁不能乱动

考虑一个问题:我们为了防止加锁后机器宕机的情况,给锁设置了过期时间,以此来保障锁可以在服务节点宕机不能解锁时,也可以给后续业务提供锁操作。

上图中,因为业务执行时间的不可控(或者遇到GC等不可预期的停顿),给分布式锁带来了使用问题。

我们先看问题一:用户线程1 把 线程2的锁释放了!怎么办呢?

加锁保存线程标识,解锁校验,非自己的锁不释放

//其他属性略,新增lockOwner标识
private String lockOwner;

//通过构造函数初始化lockOwner标识 
public DistributedLock(Jedis jedis, String lockName, String lockOwner, long lockExpireSecond, long retryIntervalTime) {
    ...略
    this.lockOwner = lockOwner;
}

public void lock(long timeout,TimeUnit unit) throws TimeoutException {
   String lockResult = null;
   try {
      //设置 NX PX 参数
      SetParams params = new SetParams();
      params.nx();
      params.px(TimeUnit.SECONDS.toMillis(lockExpireSecond));
            
      //加锁开始时间
      long startTime=System.nanoTime();
            
      // set时的value 改为 lockOwner
     while (!"OK".equals(lockResult=this.jedis.set(this.lockName, this.lockOwner, params))&&!isTimeout(startTime,unit.toNanos(timeout))){
         Thread.sleep(retryIntervalTime);
      }
   } catch (Exception e) {
       LOG.error("lock error",e);
   }
   ...略
}
    
public boolean unlock() {
    boolean unlockResult=false;
    try {
       // 先getValue ,并和当前lockOwner匹配,匹配上才去解锁
        if (this.lockOwner.equals(this.jedis.get(this.lockName))) {
           this.jedis.del(this.lockName);
           unlockResult = true;
       }
   }catch (Exception e){
        LOG.error("unLock error",e);
  }
    return unlockResult;
}

有的同学说,这个解锁的地方,需要用lua包成原子操作。单从功能上来讲,上面的实现也是OK的,因为只有get到的结果和本身匹配,才会进行下述操作。包成lua脚本的目的,应该主要是为了减少一次传输,提高执行效率。

step4: expire时间不够产生并发冲突

也就是之前的图中的问题二: 线程1 还在执行中,锁就过期释放了,导致线程2也加锁成功,这直接导致了线程间的业务冲突。 怎么办呢?

锁持有期内,根据需要,动态延长锁的过期时间

触发锁延期的方案选型,也是个大事,jdk原生timer、调度线程池、netty的Timer都可以实现,选哪个好?

综合对比精度、资源消耗等方面,Netty中采用时间轮算法的Timer应该是首选,都能管理成千上万的连接、调度心跳检测,拿来搞个锁延期还不是手拿把掐?

•首先,需要构建一个全局的Timer来存储和调度任务•其次,在加锁成功之后添加定时触发任务•再次,延期操作时,需要校验当前线程是否还持有锁•最后,在解锁时,取消定时任务•注意点,任务需要循环注册 , 考虑线程被中断的情况

构建分布式锁上下文,用于存储全局时间轮调度器:

public class LockContext {

    private HashedWheelTimer timer;

    private LockContext(){
        //时间轮参数可以从业务自己的配置获取
        // long tickDuration=(Long) config.get("tickDuration");
        // int tickPerWheel=(int) config.get("tickPerWheel"); //默认1024
        // boolean leakDetection=(Boolean)config.get("leakDetection");
        timer = new HashedWheelTimer(new DefaultThreadFactory("distributedLock-timer",true), 10, TimeUnit.MILLISECONDS, 1024, false);
    }

通过构造函数,将上下文及调度器传入分布式锁对象:

public class DistributedLock {
    //上下文
    private LockContext context;
    //当前持有的 Timer 调度对象
    private volatile Timeout  lockTimeout;

    public DistributedLock(Jedis jedis, String lockName, String lockOwner, long lockExpireSecond, long retryIntervalTime, LockContext context) {
         ...其他属性略
        this.context = context;
    }

加锁成功之后,执行调度器注册操作:

public void lock(long timeout, TimeUnit unit) throws TimeoutException {
    //...加锁 略
    
   if ("OK".equals(lockResult)) {
       LOGGER.info("locked success,lockName:{}",lockName);
       try {
           //注册循环延期事件
           registerLoopReExpire();
       }finally {
           if (Thread.currentThread().isInterrupted()&&this.lockTimeout!=null){
               LOGGER.warn("线程中断,定时任务取消");
               this.lockTimeout.cancel();
           }
       }
   } else {
       throw new TimeoutException("Get lock failed because of timeout.");
    }
}

方法registerLoopReExpire()中是实际的任务注册和延期操作:

private void registerLoopReExpire() {
    LOGGER.info("分布式锁延期任务注册");

    //每次注册,都把timeout赋给当前锁对象,用于后续解锁时取消
    this.lockTimeout = context.getTimer().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
        
            //校验是否还在持有锁,并延长过期时间
            boolean isReExpired=reExpireLock(lockName,lockOwner);
        
            if (isReExpired) {
                //自己调自己,循环注册
                registerLoopReExpire();
            }else {
                lockTimeout.cancel();
            }
        }
    }, TimeUnit.SECONDS.toMillis( lockExpireSecond)/2, TimeUnit.MILLISECONDS);

    LOGGER.info("分布式锁延期任务注册完成");
}

这里有几个点需要重点关注:

•newTimeout()操作会返回一个Timeout实体,我们需要依赖该实体来对当前任务进行管理,所以需要赋值给锁内部对象。•锁延期,需要根据lockOwner 和 lockName来判断,持有锁才加锁,需要使用lua方式来保证判断和执行的原子性。•执行完延期操作之后,需要根据结果进行后续处理,成功则继续注册,失败则取消当前任务。•定时任务的执行时间,应该要小于锁的过期时间,取过期时间的1/2或1/3或自定义传入。

来验证一下,我们设置 锁的过期时间为3秒,业务执行时间为10秒 ,执行:

可以看到,定时任务一共延期了6次,最后一次注册成功了,但是业务执行完随着解锁任务取消了。

总结和回顾

本文,我们从0到1的对分布式锁进行了编码实现。从基本能力,最后到生产环境的各种诉求基本都进行了填充和完善。

值得一提的是,除了延期功能,上述大部分能力都是经历过生产环境考验的。大家如果发现了延期功能的实现有什么问题,欢迎留言纠正,一起讨论进步。

当然,上述内容还有缺失,比如jedis 操作lua脚本的延期实现,可重入锁的改造,由于篇幅原因就不都贴出来了,有兴趣的同学也可以按上述思路继续完善。

另外,我们的上述实现都是基于主从架构,因此,分布式锁有可能会在主从切换或者其他宕机场景出现异常,但是个人认为,用牺牲效率来保障稳定的redLock,在大部分场景下其实没有什么必要。关于这部分,其实有几个号主讲述的非常到位,可以搜来看看。

最后,当我们对照redisson的分布式锁实现,来回看我们自己的实现,其实就会发现,在主逻辑上的实现,其实是大同小异的,只是redission的在可重入、效率兼顾(netty架构的运用)等方面要更加完备。

纸上得来终觉浅~ 共勉~
dd

uniapp - 实现PDA扫码枪RFID批量扫描读取示例代码,uni-app APP识别RFID(射频识别)结合硬件设备进行扫描,uniApp调用RFID读写通用方案适配各种设备+完整实例源码教程! uni-app PDA 读取RFID,基于uniapp开发PDA扫码枪APP:实现RFID批量扫描读取功能,UniApp中的RFID实现,手持机 RFID插件(uni-app集成手持设备rfid插件),uniapp自动识别RFID,uni-app调RFID模块,uni-app RFID识别插件,uniapp读取rfid,uniapp手持设备扫描rfid,uniapp超高频rfid识别,rfid标签识别,uniapp调用RFID读写,uniapp rfid扫码枪,uniapp中如何使用rfid插件,打包上线后 阅读详情

相关推荐

SAP 接口测试操作-业务顾问

业务顾问测试接口操作

weixin_43657667的博客 5163

分布式锁实现生成唯一订单编号

前言 一:发展由来 大多数互联网系统都是分布式部署的,分布式部署确实能带来性能和效率上的提升,但为此,我们就需要多解决一个分布式环境下,数据一致性的问题。 当某个资源在多系统之间,具有共享性的时候,为了保证大家访问这个资源数据是一致的,那么就必须要求在同一时刻只能被一个客户端处理,不能并发的执行,否者就会出现同一时刻有人写有人读,大家访问到的数据就不一致了。 二:我们为什么需要分布式锁? 在单机时...

qq_36801998的博客 4023

关于Nuttx操作系统

NuttX是一个成熟的实时操作系统

qq_28576837的博客 4092

Redis实现分布式锁

在之前并发系列的文章中,我们介绍了JVM中的锁。但是无论是synchronized还是Lock,都运行在线程级别上,必须运行在同一个JVM中。如果竞争资源的进程不在同一个JVM中时,这样线程锁就无法起到作用,必须使用分布式锁来控制多个进程对资源的访问。 分布式锁的实现一般有三种方式,使用MySql数据库行锁,基于Redis的分布式锁,以及基于Zookeeper的分布式锁。本文中我们重点看一下Redis如何实现分布式锁。 首先,看一下用于实现分布式锁的两个Redis基础命令: setnx key va

955

产品经理必备知识——API接口

在古代,我们的传输信息的方式有很多,比如写信、飞鸽传书,以及在战争中使用的烽烟,才有了著名的烽火戏诸侯,但这些方式传输信息的效率终究还是无法满足高速发展的社会需要。如果你选择允许,app就会根据你的当前位置为你提供更加个性化的一些服务,当然也有一些app拿到你的位置信息做出损害用户利益的行为,所以还是需要谨慎授权。首先,看接口文档的场景一般是自身业务模块需要获取外部的数据,但又不想自己重新开发,寻找外部已经实现的有开放能力的供应商(包含企业内部的)。通过发送请求和响应消息的方式实现程序间的通信。

WBKJ_Noah_的博客 3457

滑模控制理论(SMC)概述

滑模控制理论(SMC)概述一、背景二、数学理论三、高维拓展四、分析 一、背景 滑模控制理论(Sliding Mode Control, SMC)是一种建立在现代控制理论基础上的控制理论,主要数学核心为李雅普诺夫函数。 滑模控制的核心思想是建立一个滑模面,将被控系统拉取到滑模面上来,使系统沿着滑模面运动。滑模控制的一个优势是无视外部扰动和不确定参数,采用一种比较“暴力”的方式达到控制的目的。 其思想和反步法有些类似,但数学实现起来更加方便而不抽象。相比反步法,滑模控制没有复杂的数学计算,也不会出现“微分爆炸”

高能阿博特的博客 6万+

分布式锁之Redis实现

加锁的过程很简单,就是通过SET指令来设置值,成功则返回;否则就循环等待,在timeout时间内仍未获取到锁,则获取失败。 @Service public class RedisLock { Logger logger = LoggerFactory.getLogger(this.getClass()); private String lock_key = "redis_lock"; //锁键 protected long internalLockLeaseTime = .

Ztw_bb的博客 251

手把手教你为FPGA项目集成HyperRAM IP核:从AXI接口封装到上板调试全流程

本文详细介绍了如何为FPGA项目集成HyperRAM IP核,涵盖从AXI接口封装到上板调试的全流程。通过对比DDR3L、PSRAM和HyperRAM的性能指标,突出HyperRAM在低功耗和小封装尺寸方面的优势,并提供硬件连接、AXI4-Lite接口设计、状态机优化及调试技巧等实用内容,帮助开发者高效完成FPGA项目中的内存扩展。

weixin_32103009的博客 287

【8.3】功能增强

可参考https://cloud.tencent.com/developer/article/1918116?from=15425。见博客https://gitee.com/guchengwuyue/yshopmall/pulls/3。对于较为复杂的数据@ResponseBody并不能正确的得到处理,前端传来的json数据。

m0_46661713的博客 285

【Office激活】学校正版Office已激活成功仍弹出激活窗口

学校正版Office激活问题:已激活成功仍弹出激活窗口

ESCM_的博客 1万+

MCP开发实战:基于openEuler系统MCP插件的开发、测试与发布全流程

MCP开发实战:基于openEuler系统MCP插件的开发、测试与发布全流程

jks212454的博客 555

redis 分布式锁实现

分布式锁

weixin_44550507的博客 2070

Vue3 - 详细实现流程图编辑器可视化平台示例代码,打造前端web网页流程图在线编辑绘制操作展示,vue3自定义绘制流程图在页面上画流程图功能(提供各种操作按钮及节点控件,界面美观交互式流程图插件)

vue3,绘制流程图,在线流程图制作,在vue3中实现流程图,vue3做一个制作流程图的功能,vue3流程图组件,vue3流程图,流程图绘制展示,vue3流程图可视化在线编辑源码,基于VUE3的在线绘制流程图工具,vue3+logicflow流程图,绘制动态流程图并自定义节点和线,vue使用logicFlow绘制流程图,生成各种流程图图形可连接点移除,基于Vue的流程图组件,工作流程图,流程编辑器,vue3流程组件,vue-flow绘制流程图,绘制流程图示例demo,vue3 logicflow流程图使用教

🏆 前端领域优质创作者 9969

Redis分布式锁-这一篇全了解(Redisson实现分布式锁完美方案)

前言 在某些场景中,多个进程必须以互斥的方式独占共享资源,这时用分布式锁是最直接有效的。 随着技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。在单机环境中,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurrent

Coder 16万+

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版

Part-E1-SDIO-Specification-Ver4.00-Final-120220.pdf:最新的sdio 4.0 spec 协议标准 带书签 高清完整PDF版,包括SDIO 接口的外设,SDIO 接口官方协议等。

redis实现分布式锁

通过唯一标识值和原子操作,保证了锁的基本原子性和持有者的唯一性。Watchdog 自动续期确保任务执行超时时不会因锁过期被其他线程抢占。Redisson 提供了更高的易用性和可靠性,非常适合在 Spring Boot 中集成。

qq_35861084的博客 1526

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

Redis分布式锁的实现方式、实现原理

分布式锁是分布式系统中的关键概念,用于解决多个节点同时访问共享资源可能引发的并发问题。

学Java,找哪吒 907

redis分布式锁实现

当一个客户端执行完setnx这段代码后,正要执行expire这段代码时,该客户端所在的机器宕机了,key超时没设置上,该客户端又没有删除key,所以key会一直存在,这样的话,后续其他客户端执行setnx失败,这些客户端无法获取锁,导致下单失败。如果不加锁,就会出现问题,假设某个商品库存只剩一件了,两个买家同时抢购这个商品,同时去读取库存,买家A读取库存是1,可以下单,买家B读取库存也是1,可以下单,就。,如果key不存在,设置value,表示加锁成功,如果key已经存在,则无法设置,表示加锁失败。

weixin_69381390的博客 1403

AOP+自定义注解+Redis实现分布式锁

AOP+自定义注解+Redis实现分布式锁

小学生波波 3518

Redis应用详解(一)分布式锁

1. 前言 在某些场景中,多个进程必须以互斥的方式独占共享资源,这时用分布式锁是最直接有效的。 随着技术快速发展,数据规模增大,分布式系统越来越普及,一个应用往往会部署在多台机器上(多节点),在有些场景中,为了保证数据不重复,要求在同一时刻,同一任务只在一个节点上运行,即保证某一方法同一时刻只能被一个线程执行。在单机环境中,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 JAVA 提供的 volatile、ReentrantLock、synchronized 以及 concurre

傅红雪的专栏 4649

Redisson实现分布式锁

一、jedis/luttuce/redisson关系 在redis官网推荐的三大框架就是:jedis、lettuce,redission。 1、jedis jedis使用阻塞的I/O,是同步的,即当jedis与redis数据库建立连接后,只有当连接释放后才允许下一次的连接 jedis客户端实例API非线程安全,需要通过连接池来使用jedis jedis是redis的java实现的客户端,,其API提供了比较全面的redis命令的支持,jedis的每个方法底层都是对...

hehe的博客 3162

聊聊分布式锁——Redis和Redisson的方式

分布式~~锁,要这么念,首先得是『分布式』,然后才是『锁』分布式:这里的分布式指的是分布式系统,涉及到好多技术和理论,包括CAP 理论、分布式存储、分布式事务、分布式锁...分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。锁:对对,就是你想的那个,Javer 学的第一个锁应该就是Java 初级面试问题,来拼写下赛克瑞纳挨日的线程锁。

20年码农 4294

Redis分布式锁详解

一、分布式简介 什么是分布式,用最简单的话来说,就是为了较低单个服务器的压力,将功能分布在不同的机器上面;就比如: 本来一个程序员可以完成一个项目:需求->设计->编码->测试 但是项目多的时候,一个人也扛不住,这就需要不同的人进行分工合作了 这就是一个简单的分布式协同工作了; 二、分布式锁 首先看一个问题,如果说某个环节被终止或者别侵占,就会发生不可知的事情 这就会出现,设计好的或者设计的半成品会被破坏,导致后面环节出错; 这时候,我们就需要引入分布式锁的概念; 何为分布式锁?

Apollo的博客 1万+
上一篇: ElasticSearch 中需要掌握的七个概念
下一篇: 对标 VS Code,JetBrains 的下一代 IDE:Fleet
Java技术那些事儿
博客等级 码龄5年 2306粉丝 2584原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值