一文看分布式锁

为什么会存在分布式锁?

经典场景-扣库存,多人去同时购买一件商品,首先会查询判断是否有剩余,如果有进行购买并扣减库存,没有提示库存不足。假如现在仅存有一件商品,3人同时购买,三个线程同时执行方法,第一人通过了库存>0的校验,此时线程阻塞,第二人通过校验后抢先购买,此时库存已经为0,第一人继续执行扣减,库存就为-1,也就是【超卖】。

首先会想到加锁的方式解决,使用synchronized或者ReentrantLock解决,但如果多个服务器同时对一个共享资源操作时,多个服务器的内存是不共享的,加锁只能锁住当前服务器的进程。但在分布式系统下,例如使用nginx负载均衡,请求会发送到不同的tomcat容器。

1、基于关系型数据库

(1)lock表

基于主键或者唯一索引,一个线程尝试获取锁时往表中插入一条数据,插入成功则表示获取锁成功,获取锁依赖于数据库,架构简单,但是一旦出现宕机等问题,锁就无法释放,需要设置一个定时任务清理数据。并且获取不到锁的线程,需要一直等待,轮询查询什么时候可获取锁。

(2)悲观版本

锁数据提前初始化,抢锁时,使用select ...... for update; 数据库的悲观锁可以自动阻塞其他等待锁的线程,实现锁等待的功能,如果持有锁的节点宕机,数据库事务会自动回滚,锁自然释放。

这种锁可能存在两个问题:

第一个是MySQL可能会对查询做优化,对于小表可能采用表锁代替行锁,表中不同的锁之间就变成串行互斥的关系;

第二个是使用悲观锁需要开启事务,需要一直占用数据库的连接,如果锁过多,则对数据库造成压力。

2、基于Redis

(1)setnx+uuid+finally


setnx命令,只有key不存在时,才能添加成功,达到加锁的目的

void reduceStock() {
    //uid
    String uuid = UUID.randomUUID().toString();
    // 获取锁
    ///Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, "1");
    // 设置锁过期时间
    //redisTemplate.expire("lock",30, TimeUnit.SECONDS);
    // 解决原子性问题
   boolean result = redisTemplate.opsForValue().setIfAbsent(lock, uuid, 10, TimeUnit.SECONDS);


    if (lock) {
        try{
            int num = stockMapper.selectNum();
            if(num > 0) {
                //扣减库存
                stockMapper.reduceStock();
            }else{
                log.info("库存不足");
            }
        } finally {
            //校验是否是当前线程的锁
            String lockValue = redisTemplate.opsForValue().get("lock");
            if(lockValue.equals(uuid)) {
                redisTemplate.delete("lock");   
            }        
        }
    } else {
        log.info("未获取锁");
    }
}
  • 在线程A获取锁之后,如果出现宕机或者代码报错异常,锁不会释放,需要在finally中释放锁
  • 仍存在一个问题,如果在执行释放锁逻辑的时候服务器宕机,释放锁失败,在重启服务器后,加锁的数据又被恢复,就出现死锁的问题。需要再redis中添加锁的过期时间
  • 那么又会出现,在获取锁和设置时间之间,如果存在宕机问题,时间会失败,也就是不满足原子性。通过setnx可以同时满足。(Lua脚本,后续阐述)
  • 此时又会出现,在高并发的情况下,如果锁设置30s,线程A获取到锁,但方法执行了35s,线程A的锁已经过期,线程B重新获取锁并开始执行方法执行到5s时,线程A执行finally释放锁,导致线程A将线程B的锁释放。可以设置一个uuid表示当前线程的锁。

(2)redisson

void reduceStock() {
    RLock lock = redissonClient.getLock("lock");
    if (lock.tryLock()) {
        try {
            int stockNum = stockMapper.selectNum();
            if (stockNum > 0) {
                stockMapper.reduceStock();
            } else {
                log.info("库存不足");
            }
        } finally {
            lock.unlock();
        }
    } else {
        log.info("未获取锁");
    }
}
  • 使用:通过getLock方法获取到RLock对象,tryLock或lock()来进行加锁(Lua脚本);
  • 底层reids的key是锁的名称,这个key对应的值是一个HASH结构,HASH的key是持有锁的客户端ID+线程ID,value初始化为1表示锁的重入次数,当其他线程去获取锁时,使用redis提的的subcribe特性等待redis的key的变动,方式轮询造成的系统消耗资源。
  • 看门狗:当前持有锁的线程在redission客户端初始化一个watch dog线程,定时刷新key的过期时间,默认是30s,监听主线程是否还在执行,如果还在执行通过LUA脚本每10s给锁续期30秒。这个值可以config自定义。
  • 【注意】加锁时,如果使用 tryLock(long t1,long t2, TimeUnit unit) lock(long t1,long t2, TimeUnit unit) 第二个参数不是-1,则看门狗无法生效;

LUA脚本为什么能保证原子性呢?

因为redis是单线程的,当redis执行lua脚本时,lua脚本将一系列操作封装成一个命令,redis会把lua作为一个整体任务,加入到一个队列,单线程执行任务会按照队列的顺序依次执行,在执行时lua是不会被其他线程请求打断的,从而保证原子性。

优点:

减少网络开销,一次请求和接受一次响应;直接在redis上执行无需解析和转换;

缺点:

新的语言;需要单独存储和管理,需要维护;占用redis资源和时间;

源码解析

首先找到这个方法的实现

    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        //【核心方法】,尝试去获取锁,返回null时表示获取成功
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        if (ttl == null) {
            return;
        }

        //通过线程id去订阅锁
        CompletableFuture<RedissonLockEntry> future = subscribe(threadId);
        pubSub.timeout(future);
        RedissonLockEntry entry;
        if (interruptibly) {
            entry = commandExecutor.getInterrupted(future);
        } else {
            entry = commandExecutor.get(future);
        }

        try {
            //锁自旋
            while (true) {
                //尝试获取锁,获取成功后,break跳出循环
                ttl = tryAcquire(-1, leaseTime, unit, threadId);
                if (ttl == null) {
                    break;
                }

                //获取锁成功,并且获取到锁的线程需要等待一段时间ttl
                if (ttl >= 0) {
                    try {
                        entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        //如果发生中断,根据 interruptibly 参数判断是否重新尝试获取许可
                        entry.getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    //获取锁成功,但获取到锁的线程无需等待,可以直接执行后续操作
                    if (interruptibly) {
                        entry.getLatch().acquire();
                    } else {
                        entry.getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
            //取消订阅
            unsubscribe(entry, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

首先看核心代码:

Lua脚本分析:

刷新时间:

这里为什么要使用ConCurrentHashMap呢?

锁的过期时间刷新任务需要在不同的节点之间共享,确保在一个节点续约锁的时候,其他节点也能够知道锁的状态,让不同的节点能够访问和更新相同的数据结构,保证一致性。其中putIfAbsent 也是原子的,防止并发问题。

看门狗:

(3) RedLock

redission解决了锁续期的问题,但是在redis集群中,主从复制是有延时性的。例如,当线程A获取锁成功后,主节点保存了锁信息,当主节点还未同步到从节点锁信息时,主节点宕机,从节点切换为主节点后,线程的锁就丢失了。

而使用RedLock解决这个问题,就是认为每台redis都是独立的主节点,在加锁时,会记录开始加速的时间,以及加锁成功后的时间。

例如:5台redis服务器

  • 客户端获取当前毫秒级的时间戳,设置超时时间ttl=5
  • 向5个redis服务发起请求,保证全局唯一的value请求key锁
  • 如果存在3个(一半以上)那么就是获取锁成功,否则失败
  • 如果失败,或者超过ttl超过5,则向所有的redis服务发出解锁请求
  • 获取锁失败后,在 随机时间后重试获取锁,同时重试需要限制次数(随机时间是防止过多的客户端尝试去获取,但孩子有一台能获取到,导致大批出现失败的问题)

【注意】向redis服务建立网络连接时,要设置一个超时时间,避免redis服务宕机,客户端仍在等待,官方建议5-50毫秒之间。

如果所有节点同时宕机,怎么办?参考

​​

延迟重启:reids同步到磁盘方式默认1次/s,在redis崩溃后,等待ttl之后再重启。

ttl时间后全部锁都过期,不会对现有的锁造成影响,但在ttl时间内是宕机状态,影响性能和使用。

(4)基于zookeeper

后续学到,继续更新

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/159220.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Apache DolphinScheduler在通信行业的多集群统一建设与管理实践

背景介绍 为什么我们考虑构建统一的调度平台&#xff1f; 主要原因是&#xff1a;我们公司的大数据中心目前拥有七个大数据集群&#xff0c;这些集群分布在不同的机房&#xff0c;例如内蒙、南京、苏州和广州。而且&#xff0c;这些机房之间的网络并不互通。如果每个集群都独立…

基于SSM的项目管理系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

4核8G服务器价格选择轻量还是CVM合适?

腾讯云服务器4核8G配置优惠价格表&#xff0c;轻量应用服务器和CVM云服务器均有活动&#xff0c;云服务器CVM标准型S5实例4核8G配置价格15个月1437.3元&#xff0c;5年6490.44元&#xff0c;轻量应用服务器4核8G12M带宽一年446元、529元15个月&#xff0c;腾讯云百科txybk.com分…

各机构如何加强网络渗透、“渗透”防御

数据渗透&#xff0c;例如黑客攻击和“渗透”&#xff0c;或未经授权的信息传输。 联邦调查局、国家安全局以及网络安全和基础设施安全局最近的联合报告证明&#xff0c;网络安全仍然是当今国防部门面临的两个最大的网络威胁。 所谓的零日攻击尤其有害&#xff0c;因为组织在…

DMA原理和应用

目录 1.什么是DMA 2.DMA的意义 3.DMA搬运的数据和方式 4.DMA 控制器和通道 5.DMA通道的优先级 6.DMA传输方式 7.DMA应用 实验一: 内存到内存搬运 CubeMX配置&#xff1a; ​编辑用到的库函数&#xff1a; 代码实现思路&#xff1a; 实验二: 内存到外设搬运 CubeMX…

VR智慧景区:VR赋能文旅产业,激活消费潜能

随着国家数字化战略的不断深入实施&#xff0c;文旅产业数字化转型的步伐也在逐渐加快&#xff0c;以VR技术赋能文旅产业&#xff0c;让文旅景区线上线下双渠道融合&#xff0c;进一步呈现文化底蕴、激活消费潜能。 VR智慧景区以沉浸式、互动式、科技感的方式&#xff0c;将景区…

Vellum —— Constraint 约束

目录 Stretch Bend Pin Drag 解算器对DOP外节点的约束属性&#xff0c;只会读取起始帧的值&#xff1b; Stretch 保持点间的初始距离&#xff1b; Stiffness 越高的stiffness&#xff0c;就需要越多的迭代来收敛&#xff0c;如constraint iterations或substeps(子步会更好)…

Codeforces Round 909 (Div. 3)(A~G)(启发式合并)

1899A - Game with Integers 题意&#xff1a;给定一个数 , 两个人玩游戏&#xff0c;每人能够执行 操作&#xff0c;若操作完是3的倍数则获胜&#xff0c;问先手的人能否获胜&#xff08;若无限循环则先手的人输&#xff09;。 思路&#xff1a;假如一个数模3余1或者2&#…

Java编程中,异步操作流程中,最终一致性以及重试补偿的设计与实现

一、背景 微服务设计中&#xff0c;跨服务的调用&#xff0c;由于网络或程序故障等各种原因&#xff0c;经常会出现调用失败而需要重试。另外&#xff0c;在异步操作中&#xff0c;我们提供接口让外部服务回调。回调过程中&#xff0c;也可能出现故障。 这就要求我们主动向外…

金融业务系统: Service Mesh用于安全微服务集成

随着云计算的不断演进&#xff0c;微服务架构变得日益复杂。为了有效地管理这种复杂性&#xff0c;人们开始采用服务网格。在本文中&#xff0c;我们将解释什么是Service Mesh&#xff0c;为什么它对现代云架构至关重要&#xff0c;以及它是如何解决开发人员今天面临的一些最紧…

基于SSM的智能仓储系统研究与设计

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

RMI协议详解

前言特点应用示例存在的问题应用场景拓展 前言 RMI&#xff08;Remote Method Invocation&#xff0c;远程方法调用&#xff09;是Java中的一种远程通信协议&#xff0c;用于实现跨网络的对象方法调用。RMI协议基于Java的分布式计算&#xff0c;可以让客户端程序调用远程服务器…

如何用继承和多态来打印个人信息

1 问题 在python中的数据类型中&#xff0c;我们常常运用继承和多态。合理地使用继承和多态可以增强程序的可扩展性使代码更简洁。那么如何使用继承和多态来打印个人信息&#xff1f; 2 方法 打印基本信息添加子类&#xff0c;再定义一个class&#xff0c;可以直接从Person类继…

mac苹果笔记本应用程序在哪?有什么快捷方式吗?

苹果笔记本电脑一直以来都被广泛使用&#xff0c;而苹果的操作系统 macOS 也非常受欢迎。一台好的笔记本电脑不仅仅依赖于硬件配置&#xff0c;还需要丰富多样的应用程序来满足用户的需求。苹果笔记本应用程序在哪&#xff0c;不少mac新手用户会有这个疑问。在这篇文章中&#…

react antd下拉选择框选项内容换行

下拉框选项字太多&#xff0c;默认样式是超出就省略号&#xff0c;需求要换行全展示&#xff0c;选完在选择框里还是要省略的 .less: .aaaDropdown {:global {.ant-select-dropdown-menu-item {white-space: pre-line !important;word-break: break-all !important;}} } html…

MAC电脑连接外接显示屏,颜色显示有问题,又粉、紫色蒙版,问题处理(1)

问题描述 买了一个显示器&#xff0c;想给mac做分屏使用&#xff0c;结果连上之后发现&#xff0c;整个屏幕像是被蒙上了一层紫色的蒙版。 就像下面展示的一样&#xff1a; 解决 将显示器颜色空间改为RGB颜色空间即可。 打开显示器菜单&#xff0c;找到颜色空间选项&#…

PCL_点云分割_基于法线微分分割

一、概述 PCL_点云分割_基于法线微分分割_点云法向量微分-CSDN博客 利用不同的半径&#xff08;大的半径、小半径&#xff09;来计算同一个点的法向量差值P。判断P的范围&#xff0c;从而进行分割。 看图理解&#xff1a; 二、计算流程 1、计算P点小半径的法向量Ns 2、计…

Spring Boot - devtools 热部署

spring-boot-devtools是Spring Boot提供的一组开发工具&#xff0c;它旨在提高开发体验。这些工具包括应用程序的自动重新启动、自动刷新和远程调试等功能。下面是将spring-boot-devtools整合到Spring Boot应用程序中的步骤&#xff1a; 0、启用"Build project automatic…

nodejs+vue面向中小学课堂教学辅助软件系统的设计与实现-微信小程序-安卓-python-PHP-计算机毕业设计

主要功能有&#xff0c;管理员通过后台会对此教学辅助进行审核&#xff0c;管理员在还可以进行首页、个人中心、学生管理、教师管理、班级信息管理、科目名称管理、课程信息管理、教学资料管理、作业信息管理、作业提交管理、作业成绩管理、在线考试管理、试题管理、考试管理、…

2023年11月15号期中测验判断题(Java)

1-1 局部变量可以与成员变量重名。 正确答案&#xff1a;T 解释&#xff1a; 局部变量可以和成员变量重名&#xff0c;通常&#xff0c;为了区分局部变量和成员变量&#xff0c;会使用this关键字&#xff08;C称this指针&#xff0c;python是self关键字&#xff09;来特别声…