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;@Autowiredpublic 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方法会返回trueif (!lockUtil.tryLock("ORDER_APPROVE:" + orderDetail.getOrderId, 3L, 5, 1L)) {return AjaxResult.error("点击过于频繁,请稍后再操作");}try {// 处理业务逻辑} finally {lockUtil.unlock();}
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++知识切片①:运算符重载之前置递增和后置递增

文章目录 前置递增的实现1.先写好main函数及头文件2.自定义MyInteger类3.重定义cout4.在类内实现前置递增 后置递增的实现完整代码 在进行运算符重载之前&#xff0c;不妨先看看常规的前置递增和后置递增的区别&#xff1a; 前置递增如a所示&#xff0c;a是先进行递增计算&…

大数据开发个人简历范本(2024最新版-附模板)

大数据开发工程师个人简历范本> 男 22 本科 张三 计算机科学与技术 1234567890 个人概述 具备深入的Hadoop大数据运维工程师背景&#xff0c;熟悉相关技术和工具 具备良好的团队合作能力&#xff0c;善于沟通和协作 具有快速学习新知识和解决问题的能力 对于数据科学…

mac远程ssh免密登录

服务器部署经常会登录到远程服务&#xff0c;为方便操作&#xff0c;提高效率对运维人员来说设置免密登录还是很有必要的。其实也是很简单&#xff0c;安以下操作步骤即可。 1、进入到&#xff5e;/.ssh目录下&#xff0c;确认已经生成有公钥与私钥。如果没有请执行发下命令 …

使用STM32和ESP8266构建智能家居网络

本文将介绍如何使用STM32微控制器和ESP8266 WiFi模块构建一个智能家居网络。我们将讨论智能家居网络的整体设计思路、硬件连接和软件开发。通过本文的指导和示例代码&#xff0c;读者将能够搭建一个智能家居系统&#xff0c;实现远程控制和数据监测。 一、智能家居网络的整体设…

Windows 安装配置 Anaconda、CUDA、cuDNN、pytorch-cuda全流程

Windows 安装配置 Anaconda、CUDA、cuDNN、pytorch-cuda全流程 1. 安装Anaconda 网址&#xff1a;https://repo.anaconda.com/archive/ 选择第一个下载即可 双击exe文件&#xff0c;按安装向导安装即可&#xff08;除安装路径自己选择外&#xff0c;其余均可按默认选项&#x…

微众区块链观察节点的架构和原理 | 科普时间

践行区块链公共精神&#xff0c;实现更好的公众开放与监督&#xff01;2023年12月&#xff0c;微众区块链观察节点正式面向公众开放接入功能。从开放日起&#xff0c;陆续有多个观察节点在各地运行&#xff0c;同步区块链数据&#xff0c;运行区块链浏览器观察检视数据&#xf…

计算机毕业设计 基于Java的供应商管理系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…

Spark内核解析-整体概述1(六)

1、Spark整体概述 1.1整体概念 Apache Spark是一个开源的通用集群计算系统&#xff0c;它提供了High-level编程API&#xff0c;支持Scala、Java和Python三种编程语言。Spark内核使用Scala语言编写&#xff0c;通过基于Scala的函数式编程特性&#xff0c;在不同的计算层面进行…

MySQL:约束主键唯一键

表的约束&#xff1a;表中一定有约束&#xff0c;通过约束让插入表中的数据是符号预期的 约束的本质是通过技术手段&#xff0c;倒逼程序员插入正确的数据 Null约束 这里的Null表示在插入的时候&#xff0c;该属性能否为空&#xff0c;如果是NO&#xff0c;则插入时候必须有数…

鸿蒙应用中图片的显示(Image组件)

目录 1、加载图片资源 1.1、存档图类型数据源 a.本地资源 b.网络资源 c.Resource资源 d.媒体库file://data/storage e.base64 1.2、多媒体像素图片 2、显示矢量图 3、添加属性 3.1、设置图片缩放类型 3.2、设置图片重复样式 3.3、设置图片渲染模式 3.4、设置图…

基于SSM的班级事务管理系统设计与实现

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

初识对抗生成网络(GAN)

在研究语义通信的时候&#xff0c;发现解码端很多都是用GAN或基于GAN来完成的。带着对GAN的好奇&#xff0c;对GAN进行了一个初步学习。这篇文章介绍一下和GAN相关的一些常识吧~   本文围绕以下几个内容展开&#xff1a;     1.什么是GAN&#xff1f;     2.为什么要…