全面理解并发编程之分布式架构下Redis、ZK分布式锁的前世今生

本文为博客 VIP 文章,开通 VIP 后可阅读全文

开通 VIP

引言

我们反复围绕着线程安全相关问题在对Java的并发编程进行阐述,但前叙的都是基于单体架构的Java程序进行分析的,而如今单体的程序远不足以满足日益渐增的用户需求,所以一般目前Java程序都是通过多机器、分布式的架构模式进行部署。那么在多部署环境下,之前我们分析的CAS无锁、隐式锁、显式锁等方案是否还有效呢?答案是无效。

一、单体架构下的锁迁移分布式架构分析

这两种都是属于可以解决线程安全问题的锁,一种是Java原生的关键字,通过Java对象进行加锁的隐式锁,另一种则是JUC提供的显式锁方案。在开发过程中使用它们都能够保证多线程的安全问题。但把它们放在多机器/多应用部署的环境下,也能保证相同的效果吗?先来看一个案例:

两个服务:[订单服务:8000端口]、[商品服务:8001、8002端口]
数据库:订单库[db_order]、商品库[db_shopping]
订单服务中提供了一个下单接口,用户下单后会调用它,调用下单接口后,会通过restTemplate进行RPC调用商品服务的扣库存接口,实现库存扣减下单操作。
源码如下:

// 订单服务
@RestController
@RequestMapping("/order")
public class OrderApi{
    // 库存服务的RPC前缀
    private static final String REST_URL_PREFIX =
        "http://SHOPPING/inventory";
    
    @Autowired
    private OrderService orderService;
    @Autowired
    private RestTemplate restTemplate;
    
    // 下单接口
    @RequestMapping("/placeAnOrder")
    public String placeAnOrder(){
        // 模拟商品ID
        String inventoryId = "82391173-9dbc-49b6-821b-746a11dbbe5e";
        // 生成一个订单ID(分布式架构中要使用分布式ID生成策略,此处是模拟)
        String orderId = UUID.randomUUID().toString();
        // 模拟生成订单记录
        Order order = new
            Order(orderId,"黄金竹子","88888.88",inventoryId);
        
        // RPC调用库存接口
        String responseResult = restTemplate.getForObject(
            REST_URL_PREFIX + "/minusInventory?inventoryId="
                + inventoryId, String.class);
        System.out.println("调用后库存接口结果:" + responseResult);
        
        Integer n = orderService.insertSelective(order);
        
        if (n > 0) 
            return "下单成功....";
        return "下单失败....";
    }
}

// 库存服务
@RestController
@RequestMapping("/inventory")
public class InventoryApi{
    @Autowired
    private InventoryService inventoryService;
    @Value("${server.port}")
    private String port;
    
    // 扣库存接口
    @RequestMapping("/minusInventory")
    public String minusInventory(Inventory inventory) {
        // 查询库存信息
        Inventory inventoryResult =
            inventoryService.selectByPrimaryKey(inventory.getInventoryId());
        
        if (inventoryResult.getShopCount() <= 0) {
            return "库存不足,请联系卖家....";
        }
        
        // 扣减库存
        inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
        int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
        
        if (n > 0)
            return "端口-" + port + ",库存扣减成功!!!";
        return "端口-" + port + ",库存扣减失败!!!";
    }
}
复制代码

观察上述源码,存在什么问题?线程安全问题。按照之前的做法我们会对扣库存的接口使用Synchronized或ReetrantLock加锁,如下:

// 库存服务 → 扣库存接口
@RequestMapping("/minusInventory")
public String minusInventory(Inventory inventory) {
    int n;
    synchronized(InventoryApi.class){
        // 查询库存信息
        Inventory inventoryResult =
            inventoryService.selectByPrimaryKey(inventory.getInventoryId());
        
        if (inventoryResult.getShopCount() <= 0) {
            return "库存不足,请联系卖家....";
        }
        
        // 扣减库存
        inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
        n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
        System.out.println("库存信息-剩余库存数量:" +
                inventoryResult.getShopCount());
    }
    if (n > 0)
        return "端口:" + port + ",库存扣减成功!!!";
    return "端口:" + port + ",库存扣减失败!!!";
}
复制代码

是不是感觉没问题了?看测试:

通过JMeter压测工具,1秒内对下单接口进行八百次调用,此时出现了一个比较有意思的现象,测试结果如下:

订单服务控制台日志:[端口:8000]
    ......
    调用后库存接口结果:端口-8001,库存扣减成功!!!
    调用后库存接口结果:端口-8002,库存扣减成功!!!
    ......
    调用后库存接口结果:端口-8001,库存扣减成功!!!
    调用后库存接口结果:端口-8001,库存扣减成功!!!
    调用后库存接口结果:端口-8002,库存扣减成功!!!
    ......

商品服务控制台日志:[端口:8001]
    ......
    库存信息-剩余库存数量:999
    ......
    库存信息-剩余库存数量:788
    库存信息-剩余库存数量:787
    .....

商品服务控制台日志:[端口:8002]
    ......
    库存信息-剩余库存数量:998
    库存信息-剩余库存数量:996
    库存信息-剩余库存数量:993
    ......
    库存信息-剩余库存数量:788
    .....
复制代码

注意观察如上日志,在两个商品服务[8001/8002]中都出现了库存信息-剩余库存数量:788这么一条日志记录,这代表第799个商品被卖了两次,还是出现了线程安全问题,导致了库存超卖。可我们不是已经通过Class对象加锁了吗?为什么还会有这样的问题出现呢?下面我们来依次分析。

问题分析

在关于Synchronized关键字原理剖析的文章中已经得知:Synchronized关键字是依赖于对象做为锁资源进行加锁操作的,每个对象都会存在一个伴生的Monitor监视器,Synchronized则是通过它进行上锁,加锁过程如下:

OK~,有了上述基础知识之后,再接着分析前面的问题。为什么会出现线程安全问题?

实际上这个问题也不难理解,前面的案例中,我们是通过InventoryApi.class类对象进行上锁的,如果在单体程序中确实没有任何问题,因为Class对象是唯一的,当多条线程抢占一把Class锁时,同一时刻只会有一条线程获取锁成功,这样自然而然也不存在线程安全问题了。但目前的问题就出在这里,InventoryApi.class对象在单体程序中确实只存在一个,但目前商品服务是属于多应用/多机器部署的,目前商品服务有两个进程,那这就代表着存在两个不同的Java堆,在两个不同的堆空间中,都存在各自的InventoryApi.class对象,也就是代表着“此时Class对象并不是唯一的,此刻出现了两把锁”。而正是因为这个问题才最终导致了线程安全问题的出现。如下图:

 

还是回到最开始叙述线程安全的起点,同一时刻满足三要素:“多线程”、“共享资源”以及“非原子性操作”便会产生线程安全问题,而Synchronized或ReetrantLock都是从破坏了第一个要素的角度出发,以此解决了线程安全问题。但目前在分布式架构中,Synchronized或ReetrantLock因为多进程导致的多个Java空间堆出现,所以不管是Synchronized还是ReetrantLock都已经无法保证同一时刻只有一条线程对共享资源进行非原子性操作,最终线程安全问题的出现已经无法避免。

二、分布式锁思想及其实现过程推导

经过前面的分析,已经得知了分布式架构下的线程安全问题产生的根本原因,那目前又该如何解决这个问题呢?可能学习过分布式锁的小伙伴会直接回答:分布式锁。但此刻请你抛下之前的记忆,我们完全从零开始对分布式锁进行推导。

大家可以先代入一个角色:假设市面上还没有成熟的解决方案,你是第一位遇到这个问题的人,那么你又该如何去解决这类问题呢?OK~,接下来我们一起逐步推导。

先思考思考前面的Synchronized、ReetrantLock是如何解决单体程序的线程安全问题的呢?

  • Synchronized: 依赖堆中对象的Monitor监视器 通过操作监视器中的_count字段实现互斥
  • ReetrantLock: 依赖于AQS同步器 通过操作AQS中volatile修饰的state成员实现互斥

它们有什么共同点呢?都存在一个互斥量,并且互斥量都是所有线程可见的。

OK~,明白了这两点之后,再反过来思考一下,分布式环境中又是因为什么原因导致的安全问题复发了呢?答案是:互斥量所在的区域对于其他进程中的线程来说是不可见的。比如Synchronized关键字通过某个Class对象作为锁对象,一个堆空间中的Class对象对于当前进程中的所有线程来说是可见的,但是对于其他进程的线程是不可见的。ReetrantLock也是同理,volatile修饰的state变量,对于当前进程中的所有线程可见,但对于另外进程中的线程是不可见的。

那么此时想要解决分布式情况下的线程安全问题的思路是不是明了啦?

我们只需要找一个多个进程之间所有线程可见的区域实现这个互斥量即可。
比如:在一台服务器的同一路径下创建一个文件夹。获取锁操作则是创建文件夹,反之,释放锁的逻辑则是删除文件夹,这样可以很好的实现一把分布式锁,因为OS特性规定,在同一路径下,相同名字的文件夹只能创建一个。所以当两条线程同时执行获取锁逻辑时,永远只会有一条线程创建成功,成功创建文件夹的那条线程则代表获取锁成功,那么可以去执行业务逻辑。当这条线程执行完业务后,再删除掉文件夹,代表释放锁,以便于其他线程可以再次获取锁。

上述的这种方式确实可以实现一把最基本的分布式锁,但问题在于:这样实现的话一方面性能会比较差,第二个也不能具备锁重入的功能,第三方面也没有具备合理的锁失效机制以及阻塞机制。而一个优秀的分布式锁的实现方案应该满足如下几个特性:

  • ①在分布式环境中,可以保证不同进程之间的线程互斥
  • ②在同一时刻,同时只允许一条线程成功获取到锁资源
  • ③保存互斥量的地方需要保证高可用性
  • ④要保证可以高性能的获取锁与释放锁
  • ⑤可以支持同一线程的锁重入性
  • ⑥具备合理的阻塞机制,竞争锁失败的线程也有处理方案
  • ⑦支持非阻塞式获取锁,获取锁失败的线程可以直接返回
  • ⑧具备合理的锁失效机制,如超时失效等,可以确保避免死锁情况出现

那么目前市面上对于分布式锁的成熟方案有哪些呢?

  • ①基于DB实现
  • ②基
MySQL和Redis实现分布式锁 MySQL和Redis实现分布式锁1. 为何需要分布式锁2. 分布式锁的一些特点3. 常见的分布式锁4. Mysql实现分布式锁4.1 lock()4.2 tryLock()和tryLock(long timeout)4.3 unlock()4.4 锁超时4.5 MySQL实现方式小结4.6 乐观锁5. Redis实现分布式锁5.1 使用setnx实现5.2 使用Redission来实现5.3 R... 阅读详情

相关推荐

阿里架构师分享分布式架构笔记文档:Nginx+Redis+ZK+Kafka+MQ等

市面上真正适合学习的Nginx资料太少,有的书或资料虽然讲得比较深入,但是语言晦涩难懂,大多数人看完这些书基本都是从入门到放弃。学透Nginx难道就真的就没有一种适合大多数同学的方法吗?

SharingOfficer的博客 1297

(十三)全面理解并发编程分布式架构RedisZK分布式锁前世今生

引言 在前面的大部分文章中,我们反复围绕着线程安全相关问题在对Java的并发编程进行阐述,但前叙的文章中都是基于单体架构的Java程序进行分析的,而如今单体的程序远不足以满足日益渐增的用户需求,所以一般目前Java程序都是通过多机器、分布式架构模式进行部署。那么在多部署环境下,之前我们分析的CAS无锁、隐式锁、显式锁等方案是否还有效呢?答案是无效。 一、单体架构下的锁迁移分布式架构分析 在前面关于Synchronized关键字原理剖析以及AQS与ReetrantLock原理分析两篇文章中,曾得知这两种都是

weixin_45101064的博客 2444

SPARTAN6 + Verilog - FPGA step by step 从0开始学习(1)

SPARTAN6 + Verilog - FPGA step by step 从0开始学习(1)Step 1 软件安装Step 2 硬件获取Step 3 开始从头到脚建一个工程Step 3.1 新建一个工程 从0开始学习FPGA,最好的办法就是先建一个最简单的模块,并且跑起来,然后再慢慢展开。下面这个例子是要按照一定的周期点亮核心板上的一个led。 Step 1 软件安装 这个从0开始学习是用的xilinx的SPARTAN6。SPARTAN6集成开发环境用ISE,不能用Vivado,各自对应的版本不同。安装

xfacexx点点记录 5446

分布式锁Redis锁和ZK

分布式锁 分布式系统中,常见的分布式锁有两种,一种是基于Redis实现的分布式锁,一种是基于ZooKeeper锁。本篇文章简要介绍下其原理及方案。 Redisredis锁简单版本 上锁 先说上锁的命令,上锁的命令是:set {lockName} {randomVal} nx px 3000...

chuobenggu7592的博客 669

zookeeper和redis实现的分布式锁比较

基于zookeeper和redis两种方式实现的分布式锁相比较

qq_39350797的博客 778

分布式锁Redis 还是 Zookeeper

Curator是一个zookeeper的开源客户端,也提供了分布式锁的实现。try {= null ) {// 获取当前所有节点排序后的集合// 获取当前节点的名称// 判断当前节点是否是最小的节点// 获取到锁} else {// 没获取到锁,对当前节点的上一个节点注册一个监听器if ( stat!throw e;} finally{

GitHub质检员 290

redis分布式锁zk分布式锁的对比

·

qq_27877781的博客 4337

rediszk分布式锁的区别

Redis 在性能、实现复杂度、功能特性和扩展性等方面均优于 Zookeeper 作为分布式锁的选择。特别是对于需要高并发、低延迟的应用场景(如电商秒杀、抢购等),Redis 是更好的选择。然而,在某些特定场景下(如需要复杂的分布式协调功能),Zookeeper 仍然有其独特的优势。因此,在选择分布式锁方案时,应根据具体业务需求和技术栈进行权衡。

weixin_47713590的博客 1140

一文看尽所有分布式锁:MySQL,RedisZK

再有人问你分布式锁,这篇文章扔给他 1.背景 对于锁大家肯定不会陌生,在Java中synchronized关键字和ReentrantLock可重入锁在我们的代码中是经常见的,一般我们用其在多线程环境中控制对资源的并发访问,但是随着分布式的快速发展,本地的加锁往往不能满足我们的需要,在我们的分布式环境中上面加锁的方法就会失去作用。于是人们为了在分布式环境中也能实现本地锁的效果,也是纷纷各出其招,今天让我们来聊一聊一般分布式锁实现的套路。 2.分布式锁 2.1为何需要分布式锁 Martin Klepp

Jaylon Wang的专栏 884

JAVA的分布式锁

Java的分布式锁的实现与原理#什么是分布式锁为什么需要用分布式锁功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入 #什...

小林哥的博客 8004

zk——你知道的zk是这样的吗

你平常使用zookeeper做什么?是分布式协调服务、共享变量、协调锁资源、还是提供命名空间? 好了,接下来我们以提问的形式来打开话题: 你知道zk能用来做什么? 你知道zk的数据模型吗? 你知道zk的数据结构吗? 你会zk操作基本命令吗? 这些命令是如何事件通知? zk是如何保证一致性的? 你会用zk做什么? zk数据模型 了解一门技术,先知道它大致长得啥样,这才好的...

在此立一个IT职业上的flag:https://github.com/Zeb-D/my-review 2万+

分布式下必备神器之分布式锁

今天这篇文章我们来聊聊在分布式环境下的一个神兵利器——分布式锁!在看这篇文章的时候,默认大家对锁已经了解了,如果不了解的朋友可以去翻翻公号前面的文章,有很多篇详细介绍了锁...

纯洁的微笑 559

redis实现分布式锁

通过流程图也可以看到,若要使用redis实现分布式锁,会存在一些性能问题,因为在等待锁的过程中会一直自旋,周而复始的去判断是否有锁资源,如果要用高性能的分布式锁,还是需要用zookeeper,因为zookeeper天生就是用来做分布式锁的;

叶新东老师的博客 831

分布式系列之分布式锁

分布式锁的概念、应用场景、实现方案;数据库(基于数据库表、基于数据库排他锁、4个问题和解决方案、缺点分析)、Zookeeper(原理、Curator实现方案)、Consul(原理、命令)、缓存(Redis、Redlock、Redisson);问题、分布式锁分类、ReentrantLock能不能用于实现分布式锁、select for update排他锁,遇到数据库宕机是否会释放、

lonelymanontheway的博客 1481

Redis 分布式客户端 Redisson 分布式锁快速入门

Redisson 概述 Redisson 分布式锁快速入门

蚩尤后裔-汪茂雄 6200

createmutex创建的锁需要手动关闭句柄吗_分布式下必备神器之分布式锁

今天这篇文章我们来聊聊在分布式环境下的一个神兵利器——分布式锁!在看这篇文章的时候,默认大家对锁已经了解了,如果不了解的朋友可以去翻翻公号前面的文章,有很多篇详细介绍了锁的一些知识。写这篇文章的主要原因是之前星球中有朋友说面试中被问的频率有点高,虽然知道分布式锁是什么,但是还是不能很好的说出来,这篇文章就是帮助大家好好梳理一下分布式锁的原理,希望对大家有帮助。另外欢迎到 Java 极客技术知识星球...

weixin_29159127的博客 153

HPC软件使用之ANSYS Fluent

本文介绍了如何在超级计算机上使用ANSYS Fluent进行科学计算和工程仿真。文章首先对Fluent软件进行了简要介绍,强调了其在计算流体力学领域的广泛应用和强大功能。接着,详细说明了如何编写jou脚本文件和slurm脚本文件,以控制Fluent的计算流程和作业提交。文章还提供了作业提交和查看的具体命令,并通过一个鼓风机仿真的案例演示了从网格模型准备、脚本编写、作业提交到结果查看的完整流程。最后,展示了使用Fluent进行结果可视化的方法。

欢迎各位大佬光临“寒舍”!请多多指点,共同进步! 2125

操作系统课程设计 模拟FAT文件系统的设计与实现(基于Java实现)

1.研究FAT文件系统的物理布局。2.掌握FAT文件系统中目录的结构与目录项定义。3.掌握文件操作如建立目录,建立文件,删除文件,复制文件时,对FAT和目录的操作步骤。4.合理设计文件系统布局与数据结构(直接用数组模拟磁盘布局或建立一个文件模拟磁盘布局)。5.编制程序模拟FAT文件系统,加深理解文件系统的功能及实现机理。实现功能显示目录内容dir <路径名> 路径下包含的所有目录与文件(除被隐藏的文件外)创建目录md <路径/目录名>成功或失败提示删除空目录rd <路径/目录名>成功或失败提示改变当前目录cd <路径/目录名>改变命令提示符前显示的当前路径创建文件new <路径/文件名 文件内容>成功或失败提示删除文件del <路径/文件名>成功或失败提示编辑文件edit <路径/文件名 编辑内容>成功或失败提示查看文件type <路径/文件名>指定文件的内容复制文件copy <路径/文件名> <路径/文件名>成功或失败提示设置文件属性attr <文件名> +r/-r/+h/ -h成功或失败提示退出系统exit

上一篇: JVM成神路之剖析Java类加载子系统、双亲委派机制及线程类加载器
下一篇: 聊透Spring循环依赖
头顶假发
博客等级 码龄4年 92粉丝 · 574原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值