Java业务功能并发问题处理

业务场景:

笔者负责的功能需要调用其他系统的进行审批,而接口的调用过程耗时有点长(可能长达10秒),一个订单能被多个人提交审批,当订单已提交后会更改为审批中,不能再次审批(下游系统每调用一次会产生一次笔新数据)。前端在点击编辑前会进行一次查询,处于审批中的订单无法点击提交审批。问题代码如下所示

// 在数据加载时, 用户点击编辑详情时会请求当前方法, 方法会校验订单状态决定是否允许用户获取订单详情
public AjaxResult selectOrderDetailToEdit(Long orderId) {
	OrderDetail orderDetail =  OrderMapper.selectOrderId(orderId);
    if ("PROCESS".equals(orderDetail.getDealStatus()) {
        return AjaxResult.error("审批中的订单不能编辑");
    }
	return AjaxResult.success(orderDetail);
}

/**
 * 提交审批的逻辑
 */
@Transactional
public AjaxResult submitOrderApprove(OrderDetail orderDetail, boolean isProcess) {
    // 省略其他处理逻辑...
	// 调用审批接口
	if (isProcess) {
		OrderDetail toApproveOrder =  OrderMapper.selectOrderId(orderId);
		AjaxResult approvedRs = approveOrderProcess(toApproveOrder);
		// 接口调用成功, 将接口返回的编码和处理中状态写入数据库
		if (approvedRs.isSuccess()) {
			toApproveOrder.setProcessCode(approvedRs.getProcessCode());
 			toApproveOrder.setDealStatus("PROCESS");
			updateOrderDealStatus(toApproveOrder);
		}
	}
	return AjaxResult.success(orderDetail);
}

原因分析:

出现的问题就是当A和B两个用户同时在订单未审批状态时进入了订单的编辑状态,然后A用户进行了提交,订单状态实际已经是审批中,B用户由于页面上订单状态未更新,也显示的是未审批,也可以提交审批,B用户点击提交后,就导致接口调用了两次。上面的描述可能不太直观,可以看下面的时序图。

在这里插入图片描述

解决方案:

多个用户处理一个单据的情况在业务中时常见的,针对这种问题,单机服务和分布式服务的应用采用的解决方案也不相同,下面也记录下不同部署方式的解决方案以供参考。

单机应用处理方式

synchronize代码块

锁粒度比较大,不能控制到按订单加锁,当不同人操作不同的订单也需要等待其他订单处理完成后才能继续处理下一个订单。

public AjaxResult submitOrderApprove(OrderDetail orderDetail, boolean isProcess) {
    synchronize(当前类名.class) {
        if ("PROCESS".equals(orderDetail.getDealStatus())) {
            return AjaxResult("订单已处理");
        }
    }
}
ConcurrentHashMap

使用ConcurrentHashMap时需要注意使用完后要remove掉,避免出现其他线程获取不到锁甚至内存溢出的问题
其中使用了putIfAbsent方法保证原子操作,下面直接给出代码示例

// 全局静态的ConcurrentHashMap
private static ConcurrentHashMap<Long, String> orderLockMap = new ConcurrentHashMap<>();

public void submitOrderApprove(OrderDetail orderDetail) {
    long orderId = orderDetail.getOrderId();
    // map中的值是当前线程名称,remove时需要判断等于当前线程时才移除,避免移除了其他线程的锁值
    String threadName = Thread.currentThread().getName();
    try {
    	/* map中的值是当前线程名称,用于在remove时判断当前线程,避免移除其他线程的锁值
           使用ConcurrentHashMap的putIfAbsent方法, 如果put成功返回null, 键已存在则返回已存在键的值
         */
        if (OrderLock.orderLockMap.putIfAbsent(orderId, threadName) != null) {
            System.out.println(orderId + "订单正在处理中,请稍后");
        } else {
            System.out.println("加锁成功, 当前订单ID:" + orderId);
        }
        // 模拟其他业务处理逻辑
        Thread.sleep(5000L);
    } finally {
        if (threadName.equals(orderLockMap.get(orderId))) {
            orderLockMap.remove(orderId);
        }
    }
}

分布式服务处理方式

通过数据库锁限制

在提交逻辑中查询单据状态并增加查询行锁进行判断,这样另外一个线程查询时也会等待锁执行完成后才查询返回,for update是关键

-- 假设是写在mybatis的mapper中的queryOrderProcessStatus()方法的查询sql
select order_id, deal_status from order_detail where order_id = #{orderId} for update;
@Transaction
public void submitOrderApprove(OrderDetail orderDetail) {
    OrderDetail orderDetail = orderMapper.queryOrderProcessStatus(orderId);
    if ("PROCESS".equals(orderDetail.getDealStatus())) {
        return AjaxResult("订单已处理");
    }
    // 业务处理逻辑
}

当然在数据库中建立一张表作为事务处理表亦可,因篇幅所限,此处不展示这种处理方法。

通过Redis分布式锁限制

现有的Redis在java方面的api很多,我们实现起来也很方便快捷了,下面使用RedisTemplate实现Redis加锁逻辑。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

@Component
public class LockUtil {

    /**
     * lua脚本 释放锁,因为有多步操作,需要保证原子性使用lua脚本
     */
    private static final String REDIS_DEL_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

    /**
     * redis锁分类目录
     */
    private static final String LOCK_PREFIX = "LOCK:";
    /**
     * redis操作服务
     */
    protected RedisTemplate<String, String> redisTemplate;

    @Autowired
    public LockUtil(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * @param lockKey         锁的唯一key
     * @param lockTime        锁超时时间(毫秒)
     * @param reqNum          请求锁的次数
     * @param reqWaitLockTime 每次请求锁的间隔时间(毫秒)
     * @return
     * @throws InterruptedException
     */
    public Boolean tryLock(String lockKey, Long lockTime, Integer reqNum, Long reqWaitLockTime) {
        Boolean isSuccessLock = false;
        String redisKey = LOCK_PREFIX + lockKey;
        for (int count = 1; count <= reqNum; count++) {
            isSuccessLock = redisTemplate.opsForValue().setIfAbsent(redisKey, Thread.currentThread().getName(), lockTime, TimeUnit.MILLISECONDS);
            if (Boolean.TRUE.equals(isSuccessLock)) {
                return true;
            }
            try {
                Thread.sleep(reqWaitLockTime);
            } catch (InterruptedException e) {
                unLock(lockKey);
                throw new RuntimeException("加锁失败,锁ID【" + lockKey + "】");
            }
        }
        return isSuccessLock;
    }

    /**
     * 释放锁
     *
     * @param lockKey 锁的唯一key
     */
    public void unLock(String lockKey) {
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(REDIS_DEL_LOCK_SCRIPT, Long.class);
        redisTemplate.execute(redisScript, new ArrayList<>(Collections.singleton(LOCK_PREFIX + lockKey)), Thread.currentThread().getName());
    }
}
使用示例

@Autowired
private LockUtil lockUtil;

public void submitOrderApprove(OrderDetail orderDetail) {
    // 如果加锁成功, tryLock方法会返回true
    if (!lockUtil.tryLock("ORDER_APPROVE:" + orderDetail.getOrderId, 3L, 5, 1L)) {
        return AjaxResult.error("点击过于频繁,请稍后再操作");
    }
    try {
        // 处理业务逻辑
    } finally {
        lockUtil.unlock();
    }
}

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

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

相关文章

js逆向第11例:猿人学第4题雪碧图、样式干扰

任务4:采集这5页的全部数字,计算加和并提交结果 打开控制台查看请求地址https://match.yuanrenxue.cn/api/match/4,返回的是一段html网页代码 复制出来格式化后,查看具体内容如下: <td><img src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABUAAA…

mysql与其他数据库有何区别?

随着信息技术的不断发展&#xff0c;数据库系统在各行各业中得到了广泛的应用。其中&#xff0c;MySQL作为一种流行的关系型数据库管理系统&#xff0c;与其他数据库系统存在一些明显的区别。本文将就MySQL与其他数据库的区别进行深入探讨。 1、更低的成本 MySQL是一个开源的关…

小兔鲜儿 uniapp - 项目打包

目录 微信小程序端​ 核心步骤​ 步骤图示​ 条件编译​ 条件编译语法​ 打包为 H5 端​ 核心步骤​ 路由基础路径​ 打包为 APP 端​ 微信小程序端​ 把当前 uni-app 项目打包成微信小程序端&#xff0c;并发布上线。 核心步骤​ 运行打包命令 pnpm build:mp-weix…

Mybatis系列课程-一对一

目标 学会使用 assocation的select 与column 属性 select:设置分步查询的唯一标识 column:将查询出的某个字段作为分步查询的下一个查询的sql条件 步骤 第一步:修改Student.java 增加 private Grade grade; // 如果之前已经增加过, 跳过这步 第二步:修改StudentMapper.java…

YOLOv8改进 | 2023Neck篇 | 利用Gold-YOLO改进YOLOv8对小目标检测

一、本文介绍 本文给大家带来的改进机制是Gold-YOLO利用其Neck改进v8的Neck,GoLd-YOLO引入了一种新的机制——信息聚集-分发(Gather-and-Distribute, GD)。这个机制通过全局融合不同层次的特征并将融合后的全局信息注入到各个层级中,从而实现更高效的信息交互和融合。这种…

【PX4-AutoPilot教程-TIPS】Ubuntu中安装指定版本的gcc-arm-none-eabi

Ubuntu中安装指定版本的gcc-arm-none-eabi 在 Ubuntu 中开发基于 ARM 架构的 STM32 芯片&#xff0c;需要安装交叉编译器 gcc-arm-none-eabi编译代码&#xff0c;那么什么是交叉编译器呢&#xff1f; Ubuntu 自带的 gcc 编译器是针对 X86 架构的&#xff01;而我们现在要编译…

Leetcode2962. 统计最大元素出现至少 K 次的子数组

Every day a Leetcode 题目来源&#xff1a;2962. 统计最大元素出现至少 K 次的子数组 解法1&#xff1a;滑动窗口 算法如下&#xff1a; 设 mx max⁡(nums)。右端点 right 从左到右遍历 nums。遍历到元素 xnums[right] 时&#xff0c;如果 xmx&#xff0c;就把计数器 co…

【springboot+vue项目(零)】开发项目经验积累(处理问题)

一、VUEElement UI &#xff08;一&#xff09;elementui下拉框默认值不是对应中文问题 v-model绑定的值必须是字符串&#xff0c;才会显示默认选中对应中文&#xff0c;如果是数字&#xff0c;则显示数字&#xff0c;修改为&#xff1a; handleOpenAddDialog() {this.dialogT…

JVS规则引擎和智能BI(自助式数据分析)1.3新增功能说明

规则引擎更新功能 新增: 1、数据源新增Excel数据源&#xff1b; Excel数据源功能允许用户将Excel文件作为数据源导入&#xff0c;并进行数据清洗、转换和处理&#xff0c;以实现数据的集成、可视化和深度分析&#xff0c;为决策提供强大支持&#xff0c;同时保持良好的交互性…

html+css 有关于less的使用和全面解释

目录 less 注释 运算 嵌套 变量 导入 导出 禁止导出 less Less是一个CSS预处理器, Less文件后缀是.less。扩充了 CSS 语言, 使 CSS 具备一定的逻辑性、计算能力 注意&#xff1a;浏览器不识别 Less 代码&#xff0c;目前阶段&#xff0c;网页要引入对应的 CSS 文件 V…

ClickHouse数据库详解和应用实践

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 概述1.适用场景2.不适用场景 一、核心特性1.完备的DBMS功能2.列式存储与数据压缩 二、安装部署1.在线安装2.离线安装 三、jdbc访问总结 概述 ClickHouse 是一个用于…

Linux 系统磁盘空间扩容

根据提示可以看到此系统的磁盘是 50G 的&#xff0c;但是实际适用有28G左右可以扩容20G 1、磁盘分区 m 查看帮助信息 n&#xff08;表示增加分区&#xff09; p&#xff08;创建主分区&#xff09; partition number 输入3&#xff08;因为上面已经有两个分区 sda1 和 sda2&…

Qt中图片旋转缩放操作

在我们开发过程中&#xff0c;难免会遇到加载图片的问题&#xff0c;在上一个开发项目里我就遇到了图片缩放的问题&#xff0c;所以&#xff0c;我决定将这一部分好好研究&#xff0c;记录下来&#xff0c;希望对大家有帮助哟~ 在讲解之前&#xff0c;我们先看一看具体的展示效…

react antd,echarts全景视图

1.公告滚动&#xff0c;40s更新一次 2.echarts图标 左右轮播 60s更新一次 3.table 表格 import { useState, useEffect } from react;import Slider from react-slick; import slick-carousel/slick/slick-theme.css; import slick-carousel/slick/slick.css;import Layout fro…

MongoDB批量写入操作

一、概述 MongoDB为客户端提供了批量执行写入操作的能力。批量写入操作影响单个集合。MongoDB允许应用程序确定批量写入操作所需的可接受确认级别。 db.collection.bulkWrite&#xff08;&#xff09;方法提供了执行批量插入、更新和删除操作的能力。 MongoDB还支持通过db.col…

使用Apache POI将数据写入Excel文件

首先导入依赖 <dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>3.16</version> </dependency> <dependency><groupId>org.apache.poi</groupId><artifactId>po…

Spring Cloud Gateway 缓存区异常

目录 1、问题背景 2、分析源码过程 3、解决办法 最近在测试环境spring cloud gateway突然出现了异常&#xff0c;在这里记录一下&#xff0c;直接上干货 1、问题背景 测试环境spring cloud gateway遇到以下异常 DataBufferLimitException: Exceeded limit on max bytes t…

Spring 面试题学习笔记整理

Spring 面试题学习笔记整理 Spring的理解IOC读取 xml注入 配置过程解析注解注入过程 高频 &#xff1a;IOC 理解 及原理 底层实现IoC的底层实现高频&#xff1a;Bean的生命周期&#xff08;图解&#xff09;高频&#xff1a;Bean的生命周期&#xff08;文解&#xff09;扩展知识…

STM32和ESP8266的WiFi模块控制与数据传输

基于STM32和ESP8266 WiFi模块的控制与数据传输是一种常见的嵌入式系统应用。在这种应用中&#xff0c;STM32作为主控制器负责控制和与外部传感器交互&#xff0c;而ESP8266 WiFi模块则用于实现无线通信和数据传输。本文将介绍如何在STM32上控制ESP8266模块&#xff0c;建立WiFi…

【React系列】React生命周期、setState深入理解、 shouldComponentUpdate和PureComponent性能优化、脚手架

本文来自#React系列教程&#xff1a;https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. 生命周期 1.1. 认识生命周期 很多的事物都有从创建到销毁的整个过程&#xff0c;这个过程称之为是生命周期&…