Java架构直通车——基于数据库for update实现分布式锁

2023-11-11

解决库存超卖问题,可以另扣库存不在程序中运行,而是通过数据库。向数据库传递库存增量,扣件N个库存,增量为-N。也就是在数据库update语句计算库存,通过update行锁解决并发。
在高并发的情况下对数据库压力较大,所以很少使用。

使用数据库解决超卖问题(非分布式)

这里我们从原始方法开始,一步一步修改代码来达到防止超卖的目的。

分为三步:原始方法

我们一步一步的分析,先来看下面的更新库存代码(创建订单的代码不做展示),我们实现了下面的三个步骤:

  1. 获取当前库存量selectByPrimaryKey()
  2. 在程序中计算还剩下多少库存leftCount
  3. 将leftCount更新至数据库updateByPrimaryKeySelective()
    在这里插入图片描述
    我们使用一个test多线程来查看这个程序的结果:
@RunWith(SpringRunner.class)
@SpringBootTest
public class DistributeDemoApplicationTests {
    @Autowired
    private OrderService orderService;

    @Test
    public void concurrentOrder() throws InterruptedException {
        CountDownLatch cdl = new CountDownLatch(5);
        CyclicBarrier cyclicBarrier = new CyclicBarrier(5);

        ExecutorService es = Executors.newFixedThreadPool(5);
        for (int i =0;i<5;i++){
            es.execute(()->{
                try {
                    cyclicBarrier.await();
                    Integer orderId = orderService.createOrder();
                    System.out.println("订单id:"+orderId);
                } catch (Exception e) {
                    e.printStackTrace();
                }finally {
                    cdl.countDown();
                }
            });
        }
        cdl.await();
        es.shutdown();
    }
}

该程序运行的结果如下:
在数据库中,product商品数据库的数目count从1减少到了0。
在这里插入图片描述
而订单表多了5个订单:
在这里插入图片描述
很明显库存只剩下1个,但是重复下单了5次,出现了超卖

合并二、三步:使用update行锁使操作下沉到数据库

所以,我们想使用update行锁来把代码执行的逻辑下降到数据库中去执行。
在Mapper中:

int updateProductCount(@Param("purchaseProductNum") int purchaseProductNum,
                           @Param("updateUser") String xxx, @Param("updateTime") Date date,
                           @Param("id") Integer id);
    <update id="updateProductCount">
      update product
      set count = count - #{purchaseProductNum,jdbcType=INTEGER},
      update_user = #{updateUser,jdbcType=VARCHAR},
       update_time = #{updateTime,jdbcType=TIME}
      where id = #{id,jdbcType=INTEGER}
    </update>

在原来的代码中,将计算剩余库存和更新数据库的操作合并为一步:
在这里插入图片描述

因为update操作有行锁,所以5个线程同时执行一条语句的话,只会有一个线程执行成功,另外四个线程回去等它运行完成后再争抢这个锁。
这样运行的结果如下:
在数据库中,product商品数据库的数目count从1减少到了-4。
在这里插入图片描述
同样也是产生了5个订单:
在这里插入图片描述

很明显结果不正确,所以代码还需要改进。

合并一、二、三步:使用方法锁

我们可以让校验库存和减扣库存成为原子性的操作。也就是把1,2,3步合并成为一步。

这样,并发的时候,只有获得锁的线程才能进行校验和扣减。
在这里插入图片描述
我们运行的结果如下:
在这里插入图片描述
在这里插入图片描述

直接看结果,发现这个test跑出错了,理想情况下只可能产生一个订单,而截图很明显产生了两次订单。
我们再看数据库:
在这里插入图片描述

怎么回事?我们只能通过打印log来看看到底出了什么问题。
在这里插入图片描述

再次运行:
在这里插入图片描述
我们发现线程3和4读到的库存都是1。为什么会有两个线程读到的库存都是1呢?

这是因为我们使用了事务:
在这里插入图片描述
前一个线程还没有提交事务,后一个线程就开始执行了。
所以我们需要把事务一块锁起来,这需要我们手动的去控制事务了。

首先需要手动的注入这些类。

import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;

    @Autowired
    private PlatformTransactionManager platformTransactionManager;
    @Autowired
    private TransactionDefinition transactionDefinition;

在方法开始我们创建事务,方法的结束提交事务:

//    @Transactional(rollbackFor = Exception.class)
    public Integer createOrder() throws Exception{
		TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);
		...
		校验库存
		减扣库存
		新建订单
		...
		 platformTransactionManager.commit(transaction1);
		 return order.getId();
	}

其中校验库存、减扣库存、新建订单的三个操作中,如果出现了异常,需要执行事务回滚的相关操作:platformTransactionManager.rollback(transaction1);

这次运行的结果:
数据库扣库存成功,订单也只创建了一个。

优化:使用块锁

当然可以使用synchronized(this){}的块锁。
也可以使用ReentrantLock并发包提供的可重入锁。

这里给出后者的完整代码:


    private Lock lock = new ReentrantLock();


//    @Transactional(rollbackFor = Exception.class)
    public Integer createOrder() throws Exception{
        Product product = null;

        lock.lock();
        try {
            TransactionStatus transaction1 = platformTransactionManager.getTransaction(transactionDefinition);
            product = productMapper.selectByPrimaryKey(purchaseProductId);
            if (product==null){
                platformTransactionManager.rollback(transaction1);
                throw new Exception("购买商品:"+purchaseProductId+"不存在");
            }

            //商品当前库存
            Integer currentCount = product.getCount();
            System.out.println(Thread.currentThread().getName()+"库存数:"+currentCount);
            //校验库存
            if (purchaseProductNum > currentCount){
                platformTransactionManager.rollback(transaction1);
                throw
                        new Exception("商品"+purchaseProductId+"仅剩"+currentCount+"件,无法购买");
            }

            productMapper.updateProductCount(purchaseProductNum,"xxx",new Date(),product.getId());
            platformTransactionManager.commit(transaction1);
        }finally {
            lock.unlock();
        }

        TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
        Order order = new Order();
        ...
        orderMapper.insertSelective(order);

        OrderItem orderItem = new OrderItem();
        ...
        orderItemMapper.insertSelective(orderItem);
        platformTransactionManager.commit(transaction);
        return order.getId();
    }

使用数据库解决分布式超卖问题

我们知道,单体锁是不会跨JVM的,所以需要第三方来解决分布式的问题。

主要原理

这里我们使用数据库来解决,主要是通过select ... for update来解决,这句话的意思是for update的数据,我们就给他加上了一把锁,其他的人是不能修改这个数据的,也不能给这条数据再加锁。

我们首先新增一个表distribute_lock
在这里插入图片描述

  • id,自增id。
  • bussiness_code,业务代码用来区分不同业务使用的锁,比如订单使用一把锁,商品的其他某些步骤的并发又使用其他的锁。
  • bussiness_name,业务名称用来标注code是什么意思,做一个注释的作用。

我们简单的放入数据:
在这里插入图片描述
我们查看自动提交是否置为0:
在这里插入图片描述
没有,则置为0:
在这里插入图片描述
现在我们运行:
在这里插入图片描述
现在这条语句已经检索出来了,并且加上了锁(由于我们把autocommit设置为了不自动提交,所以这条语句现在还没有提交)。

我们再创建一个窗口,仍然是同样的select ... for update语句查询:
在这里插入图片描述
可以看到直接运行是检索不出来的。

我们在前一个查询上加上commit操作,直接运行commit。
在这里插入图片描述
这样第二个窗口的查询也在之后立即执行完毕,然后我们也要再执行commit。

解决方案

首先生成Springboot程序。
在这里插入图片描述
这里的controller,我们使用一个可重用锁来锁住,我们运行一下,并且使用postman来验证锁。
请求1:
在这里插入图片描述
请求2(与请求1相同):
在这里插入图片描述

我们看下命令行输出:
在这里插入图片描述
可以发现,单JVM的情况下,同时请求,只有一个获取到了锁。
这里我们再试试多JVM,也就是新增一个port。
一个设置8080端口,另一个设置8081端口。
在这里插入图片描述
启动:
在这里插入图片描述
我们使用postman,一个请求8080一个请求8081:
在这里插入图片描述
在这里插入图片描述

可以看出,锁只在同一个JVM里才有效。

现在我们使用第三方数据库来实现分布式锁,首先用mybatis-generator插件生成:

		<plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.7</version>
                <dependencies>
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.17</version>
                    </dependency>
                </dependencies>
            </plugin>

我们在生成的map里新加入一个方法:

DistributeLock selectDistributeLock(@Param("bussinessCode")String bussinessCode);

实现如下:
在这里插入图片描述

修改controller:
在这里插入图片描述
按照之前的postman的方法同样测试两个不同的端口:

在这里插入图片描述
在这里插入图片描述
为什么会出现这种情况呢?因为我们没有加入事务,sql语句已经自动提交了。
加上事务即可。
在这里插入图片描述
运行结果:
在这里插入图片描述
在这里插入图片描述
运行成功。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Java架构直通车——基于数据库for update实现分布式锁 的相关文章

  • vue2 ajax异步请求,数据嵌套层数过多,导致页面无法正常通过数据驱动渲染

    数据层数过多的小坑 初入门vue2 在开发项目过程中因为用到了vue echarts v3 涉及图表的数据 难免数据就有过多的层数 导致出现了这么一个坑 其实归根结底是自己没有按照vue2官方的方法进行对象数据修改 首先 数据结构大致是这样
  • 精美简历生成器(Nice_Resume_Builder)

    文章目录 前言 功能演示 后记 前言 写简历有时候是个比较麻烦的事情 不管是用Word还是用别的设计工具 如果内容经常需要修改的话 那么修改后通常有需要花时间去调整格式排版 这个过程令我烦躁 毫无意义的浪费时间 所以稍微花点时间弄了这个东西

随机推荐

  • 超过最大更新深度。当组件在 componentWillUpdate 或 componentDidUpdate 中重复调用 setState 时,可能会发生这种情况。React 限制了嵌套更新的数量以防

    超过最大更新深度 当组件在 componentWillUpdate 或 componentDidUpdate 中重复调用 setState 时 可能会发生这种情况 React 限制了嵌套更新的数量以防止无限循环 有没用像我这样报错的 这个报
  • 每天Leetcode 刷题 初级算法篇-设计问题-最小栈

    题目要求 力扣题解 代码 program mydemo description 设计问题 最小栈 author Mr zeng create 2021 02 19 09 49 public class MinStack private St
  • linux获取主板温度电压_穷人省钱技巧揭秘!200元技嘉主板竟可满血英特尔I9处理器?...

    其实 这是一个意外 有一个粉丝 手里置闲了一块技嘉Z270芯片组的主板 本来他以为的型号是GA Z270X UD3 结果 发过来的却是GA Z370 HD3 本来UD3的供电就够乞丐的了 HD3则更加低端 更要命的是 粉丝想搭配的处理器 竟
  • java--基础--26--模块化

    java 基础 26 模块化 代码 https gitee com DanShenGuiZu learnDemo tree mysql mybaties DB java model learn 1 模块化概述 无论是运行一个大型的软件系统
  • MOS管和三极管区别-对比很显然

    在电路设计当中假设我们想要对电流中止控制 那就少不了三极管的帮助 我们俗称的三极管其全称为半导体三极管 它的主要作用就是将微小的信号中止放大 MOS管与三极管有着许多相近的地方 这就使得一些新手不断无法明白两者之间的区别 这里就将为大家引见
  • uniapp 各种兼容,优化等问题记录

    对于ios自带的上下拉 进行禁用 橡皮筋回弹 1 pages json中加入如下配置 path pages my my style navigationBarTitleText 个人中心 disableScroll true 禁止滑动 en
  • xss-labs/level5

    输入 查看回显 如下所示 能够发现script被恶意替换为scr ipt 查看源代码 第一个输出点被转义了 所以没有利用价值了 第二个输出点如同刚才所言被进行了关键字的恶意替换操作 那没办法 我们只能继续尝试一下在标签内部构造一个新属性然后
  • opencv CvSolve函数深度解析

    Opencv CvSolve函数主要是用来求解线性系统Ax b的方程 X的解 solve函数跟它的算法是一样的 也是用来求解线性系统 设方程Ax b 根据有效的方程个数和未知数的个数 可以分为以下3种情况 1 rank A lt n 也就是
  • mysql故障切换 java_java.sql.SQLException: Value'0000-00-00'异常解决办法及mysql的url设置...

    一 0000 00 00错误及解决方案 在使用MySql 时 数据库中的字段类型是timestamp的 默认为0000 00 00 会发生异常 java sql SQLException Value 0000 00 00 can not b
  • 音频服务器运行失败怎么办,音频服务错误1068怎么解决 音频服务未运行win10解决方法...

    win10音频服务无法启动 提示错误1068 依赖服务或组无法启动是什么情况 怎么解决 在这里就跟着小编一起来了解一下吧 让大家解决电脑不能够出声的问题 音频服务未运行win10问题排除 一 电脑没了声音 小喇叭上有个红叉 鼠标放上去显 音
  • 【Python学习笔记2】Python网络爬虫的异常处理

    本文信息主要来源于韦玮老师的 精通python网络爬虫 仅作为个人学习笔记 通过python库函数urllib request去爬取网页时 遇到的异常主要时URL类和网页类的错误 python已提供URLError类和HTTPError类来
  • Ado.Net总结

    ADO NET总结 ADO NET 是在 NET 平台上访问数据库的组件 它是以 ODBC Open Database Connectivity 技术的方式来访问数据库的一种技术 ADO NET常用命名空间 命名空间 数据提供程序 Syst
  • 多输入多输出

    多输入多输出 MATLAB实现GRU门控循环单元多输入多输出 目录 多输入多输出 MATLAB实现GRU门控循环单元多输入多输出 预测效果 基本介绍 程序设计 往期精彩 参考资料 预测效果 基本介绍 MATLAB实现GRU门控循环单元多输入
  • spark SQL配置连接Hive Metastore 3.1.2

    Hive Metastore作为元数据管理中心 支持多种计算引擎的读取操作 例如Flink Presto Spark等 本文讲述通过spark SQL配置连接Hive Metastore 并以3 1 2版本为例 通过Spark连接Hive
  • HTML <progress> 标签

    实例 正在进行的下载
  • kali 破解压缩包密码

    一 提取加密压缩包的哈希值 zip2john 文件名 gt hash txt zip2john 文件名 能提取出文件的哈希值 然后我们通过重定向 将它写入一个文档中 二 利用工具对提取到的哈希值进行破解 john hash txt 然后就能
  • windows11安装微软商店里的ubuntu报错,已解决

    1 问题情况 安装微软商店的Ubuntu系统 安装完成后打开开始菜单的Ubuntu图标 初始化过程中卡住 点击界面下方的展开按钮有文字提示报错 报错提示内容为 Installing this may take a few minutes W
  • 京东抢购服务高并发实践

    声明 本位来自京东张开涛的微信公众号 kaitao 1234567 授权CSDN转载 如需转载请联系作者 作者 张子良 京东高级开发工程师 在京东负责抢购后端服务系统架构和开发工作 责编 钱曙光 关注架构和算法领域 寻求报道或者投稿请发邮件
  • eclipse与Mysql数据库是否连接的检验

    先去下载与Mysql数据库版本匹配的架包 然后导入到项目中 在这里我用的是Mysql8 0 Mysql8 0的驱动类名是 com mysql cj jdbc Driver 首先新建一个Demo类 然后进行数据库的连接即检验 代码如下 pac
  • Java架构直通车——基于数据库for update实现分布式锁

    文章目录 使用数据库解决超卖问题 非分布式 分为三步 原始方法 合并二 三步 使用update行锁使操作下沉到数据库 合并一 二 三步 使用方法锁 优化 使用块锁 使用数据库解决分布式超卖问题 主要原理 解决方案 解决库存超卖问题 可以另扣