从零到一编码实现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架构的运用)等方面要更加完备。

纸上得来终觉浅~ 共勉~

立创星火计划:基于STM32H743VIH6的Maverick_H743_MK1.0高性能开源飞控硬件全解析 本文详细解析了基于STM32H743VIH6核心的Maverick_H743_MK1.0高性能开源飞控硬件设计。重点阐述了采用双IMU冗余设计、高精度气压计、多路独立电源管理以及丰富接口(如CAN总线)的考量,旨在为穿越机、科研无人机等平台提供强大、可靠的飞行控制解决方案。 阅读详情

相关推荐

【网络】Ikuai中如何添加/更换虚拟机(图文)

Ikuai 添加虚拟机教程

qq_20623849的博客 6533

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

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

qq_36801998的博客 4021

Cadence 17.4 PSpice仿真555定时器输出方波

这里利用555定时器组成多谐振荡器产生1Hz的方波并用PSpice仿真其输出的方波波形。555定时器是一种广泛应用的中规模集成电路。根据其内部组成的不同,可分为双极型(555B)和CMOS型(555C)两类。两种类型的定时器各有所长。双极型定时器具有较大的驱动能力,其输出电流可达200mA,可直接驱动发光二级管、扬声器、继电器等负载。而CMOS型定时器的输入阻抗高、功耗低。

tj_nonstoper的博客 1万+

Redis实现分布式锁

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

955

STM32L476低功耗实战:LPTIM精准定时唤醒STOP2模式与系统级功耗优化

本文深入探讨了STM32L476低功耗实战技巧,重点解析了LPTIM精准定时唤醒STOP2模式的配置与优化。通过详细的步骤指南和实战经验分享,帮助开发者实现系统级功耗优化,特别适合电池供电设备开发。文章涵盖LPTIM1定时器配置、STOP2模式进入流程及常见问题排查,助力将整机功耗降至8.2uA级别。

weixin_33686714的博客 435

伺服驱动器启用后对传感器线信号干扰的解决方案

在一些Arduino项目中,研究者发现,当伺服电机启动时,传感器信号会出现显著噪声。例如,在一个假肢项目中,研究者使用光电隔离器成功隔离了伺服驱动器对EMG传感器的干扰。然而,当启用伺服驱动器时,传感器线信号常常受到干扰,导致系统性能下降。传感器线信号是传感器输出的电信号,用于测量物理量,如温度、光线或位置。例如,在一些案例中,伺服驱动器的开关操作可能导致传感器信号出现尖峰或波动,特别是在模拟传感器上更为明显。描述: 使用光电隔离或其他隔离技术,分离伺服驱动器和传感器的电气路径,防止直接干扰。

有用好玩有趣的“电”知识 1466

【8.3】功能增强

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

m0_46661713的博客 285

龙腾CMS downloadFile接口任意文件读取漏洞复现 [附POC]

大连网龙网络科技有限公司专注于在信息科技领域中向客户提供商业解决方案,是专业的互联网服务解决方案提供商和网络营销服务专家,为用户提供一体化的网络营销整体解决方案,龙腾CMS downloadFile接口存在任意文件读取漏洞。

薛定谔嘚喵博客 720

分布式锁之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

手把手教你学Simulink实例:基于Simulink的锂电池管理系统(BMS)仿真

是确保锂电池安全、高效运行的关键组件。BMS 主要功能包括监控电池的状态(如电压、电流、温度)、估算荷电状态(State of Charge, SOC)、健康状态(State of Health, SOH),以及实现电池均衡和保护机制等。Simulink 提供了一个强大的仿真环境,可以用来设计、分析和验证锂电池管理系统的控制策略。常见的均衡方法包括被动均衡(通过电阻放电)和主动均衡(通过开关电容或变压器)。通过本教程,我们详细介绍了如何使用 Simulink 进行锂电池管理系统的仿真实例。

MHD0815的博客 538

西门子TP900精智系列触摸屏和三菱Q系列PLC以太网通信的组态步骤

西门子TP900精智系列触摸屏和三菱Q系列PLC以太网通信的组态步骤

Robot_PLC_HMI_Automation 6097

redis 分布式锁实现

分布式锁

weixin_44550507的博客 2040

第二章:创建交互式地图

那么您应该已经有了一个安装了这些包的 conda 环境。否则,您需要创建一个新的 conda 环境并使用以下命令安装。

韩天放 4215

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

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

Coder 16万+

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

redis实现分布式锁

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

qq_35861084的博客 1525

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

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

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

学Java,找哪吒 906

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

redis分布式锁实现

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

weixin_69381390的博客 1403

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

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

小学生波波 3516

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年码农 4289

Redis分布式锁详解

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

Apollo的博客 1万+
上一篇: IntelliJ IDEA 2021.3 来了,新功能很强
下一篇: 面试官:Redis新版本开始引入多线程,谈谈你的看法?
Java技术那些事儿
博客等级 码龄5年 2306粉丝 2584原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值