一文看分布式锁

news2024/12/24 8:56:56

为什么会存在分布式锁?

经典场景-扣库存,多人去同时购买一件商品,首先会查询判断是否有剩余,如果有进行购买并扣减库存,没有提示库存不足。假如现在仅存有一件商品,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

后续学到,继续更新

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

The import xxx.xxx.xxxx is never used

CTRL SHIFT O 就完成了&#xff0c;懒人&#xff0c;代码没洁癖啊&#xff0c;几千上万的代码没用的。

Vue 路由缓存 防止路由切换数据丢失

在切换路由的时候&#xff0c;如果写好了一丢数据在去切换路由在回到写好的数据的路由去将会丢失&#xff0c;这时可以使用路由缓存技术进行保存&#xff0c;这样两个界面来回换数据也不会丢失 在 < router-view >展示的内容都不会被销毁&#xff0c;路由来回切换数据也…

HP惠普暗影精灵9笔记本电脑OMEN by HP Transcend 16英寸游戏本16-u0000原厂Windows11系统

惠普暗影9恢复出厂开箱状态&#xff0c;原装出厂Win11-22H2系统ISO镜像 下载链接&#xff1a;https://pan.baidu.com/s/17ftbBHEMFSEOw22tnYvPog?pwd91p1 提取码&#xff1a;91p1 适用型号&#xff1a;16-u0006TX、16-u0007TX、16-u0008TX、16-u0009TX、16-u0017TX 原厂系…

【AI视野·今日CV 计算机视觉论文速览 第279期】Tue, 31 Oct 2023

AI视野今日CS.CV 计算机视觉论文速览 Tue, 31 Oct 2023 Totally 165 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computer Vision Papers SimMMDG: A Simple and Effective Framework for Multi-modal Domain Generalization Authors Hao Dong, Ismail Nejjar, …

【力扣】从零开始的动态规划

【力扣】从零开始的动态规划 文章目录 【力扣】从零开始的动态规划开头139. 单词拆分解题思路 45. 跳跃游戏 II解题思路 5. 最长回文子串解题思路 1143. 最长公共子序列解题思路 931. 下降路径最小和解题思路 开头 本力扣题解用5题来引出动态规划的解题步骤&#xff0c;用于本…

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

数据渗透&#xff0c;例如黑客攻击和“渗透”&#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…

C语言变量与常量

跟着肯哥&#xff08;不是我&#xff09;学C语言的变量和常量、跨文件访问、栈空间 栈空间还不清楚&#xff0c;期待明天的课程内容 C变量 变量&#xff08;Variable&#xff09;是用于存储和表示数据值的名称。 主要包括四个环节&#xff1a;定义、初始化、声明、使用 在我刚…

gorm的简单操作

1. 什么是orm ORM全称是&#xff1a;Object Relational Mapping(对象关系映射)&#xff0c;其主要作用是在编程中&#xff0c;把面向对象的概念跟数据库中表的概念对应起来。举例来说就是&#xff0c;我定义一个对象&#xff0c;那就对应着一张表&#xff0c;这个对象的实例&a…

LabVIEW编程开发NI-USRP

LabVIEW编程开发NI-USRP 可编程性是SDR的关键特性&#xff0c;它使人们能够将无线电外围设备转换为先进的无线系统。USRP是市场上最开放、最通用的SDR&#xff0c;可帮助工程师在主机和FPGA上使用各种软件开发工具构建系统。 有多种选项可用于对基于SDR的系统的主机进行编程。…

wpf devexpress Property Gird管理集合属性

Property Grid允许你添加&#xff0c;浏览和编辑集合属性

【2022改良版】学法减分助手PRO小程序源码

【2022改良版】学法减分助手PRO小程序源码 &#xff0c;交管推出个学法减分&#xff0c;每个驾驶员可以把被扣的6分&#xff0c;以看视频答题的形式学习回来&#xff0c;然后答题这个一共二十道题每道题60秒&#xff0c; 有好多人不会&#xff0c;用咱们的小程序就可以模拟练习…

鸿蒙开发|鸿蒙系统项目开发前的准备工作

文章目录 鸿蒙项目开发的基本流程介绍鸿蒙项目开发和其他项目有什么不同成为华为开发者-注册和实名认证1.登录官方网站 鸿蒙项目开发的基本流程介绍 直接上图&#xff0c;简单易懂&#xff01; 整个项目的开发通过4个模块进行&#xff1a;开发准备、开发应用、运行调试测试和发…

Typora for Mac:打造全新文本编辑体验

Typora for Mac是一款与众不同的文本编辑器&#xff0c;它不仅拥有直观易用的界面&#xff0c;还融合了Markdown语法和富文本编辑的功能&#xff0c;为用户带来了前所未有的写作和编辑体验。 一、简洁明了的界面设计 Typora for Mac的界面简洁明了&#xff0c;让用户可以专注…

解决更换NodeJs版本后npm -v返回空白

一、问题描述 win11电脑上输入cmd进入控制台&#xff0c;输入 node --version 有正常返回安装的nodejs的版本号 再输入 npm -v 返回空白。正常情况应该是要返回版本号。 二、问题背景 最近准备学习vue&#xff0c;在不久前已经安装了NodeJs和python。运行了好几个开源项…

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

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

vim模式用法总结

0.前言 我们用gcc编译文件的时候&#xff0c;如果发生了下面的错误&#xff0c;那么如何用vim打开的时候就定位到&#xff1f; 我们可以知道&#xff0c;这是第6行出现了错误&#xff1b; 所以我们使用vim打开的时候多输入个这个&#xff0c;我们就可以快速定位了 vim test.c 6…

centos7 killall命令安装、使用

安装 在线安装命 输入下面命令 yum install psmisc -y Psmisc软件包包含三个帮助管理/proc目录的程序。 安装下列程序: fuser, killall,pstree和pstree.x11(到pstree的链接) fuser #显示使用指定文件或者文件系统的进程的PID。 killall #杀死某个名字的进程&#xff0c;它…

【咖啡品牌分析】Google Maps数据采集咖啡市场数据分析区域分析热度分布分析数据抓取瑞幸星巴克

引言 咖啡作为一种受欢迎的饮品&#xff0c;已经成为我们生活中不可或缺的一部分。随着国内外咖啡品牌的涌入&#xff0c;新加坡咖啡市场愈加多元化和竞争激烈。 本文对新加坡咖啡市场进行了全面的品牌门店数占比分析&#xff0c;聚焦于热门品牌的地理分布、投资价值等。通过…

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

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