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.mzph.cn/news/597377.shtml

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

相关文章

05-微服务-RabbitMQ-概述

RabbitMQ 1.初识MQ 1.1.同步和异步通讯 微服务间通讯有同步和异步两种方式&#xff1a; 同步通讯&#xff1a;就像打电话&#xff0c;需要实时响应。 异步通讯&#xff1a;就像发邮件&#xff0c;不需要马上回复。 两种方式各有优劣&#xff0c;打电话可以立即得到响应&am…

canvas设置文字阴影

查看专栏目录 canvas示例教程100专栏&#xff0c;提供canvas的基础知识&#xff0c;高级动画&#xff0c;相关应用扩展等信息。canvas作为html的一部分&#xff0c;是图像图标地图可视化的一个重要的基础&#xff0c;学好了canvas&#xff0c;在其他的一些应用上将会起到非常重…

MySQL之CRUD、常见函数及union查询

目录 一. CRUD 1.1 什么是crud 1.2 SELECT(查询) 1.3 INSERT(新增) 1.4 UPDATE(修改) 1.5 DELETE(删除) 二. 函数 2.1 常见函数 2.2 流程控制函数 2.3 聚合函数 三. union与union all 3.1 union 3.2 union all 3.3 具体不同 3.4 结论 四. 思维导图 一. CRUD 1.1 什么是crud…

【愚公系列】2023年12月 HarmonyOS应用开发者高级认证(完美答案)

&#x1f3c6; 作者简介&#xff0c;愚公搬代码 &#x1f3c6;《头衔》&#xff1a;华为云特约编辑&#xff0c;华为云云享专家&#xff0c;华为开发者专家&#xff0c;华为产品云测专家&#xff0c;CSDN博客专家&#xff0c;CSDN商业化专家&#xff0c;阿里云专家博主&#xf…

6. Mybatis 缓存

6. Mybatis 缓存 MyBatis 包含一个非常强大的查询缓存特性,它可以非常方便地配置和定制。缓存可以极大的提升查询效率MyBatis系统中默认定义了两级缓存 一级缓存二级缓存 默认情况下&#xff0c;只有一级缓存&#xff08;SqlSession级别的缓存&#xff0c;也称为本地缓存&…

Transforer逐模块讲解

本文将按照transformer的结构图依次对各个模块进行讲解&#xff1a; 可以看一下模型的大致结构&#xff1a;主要有encode和decode两大部分组成&#xff0c;数据经过词embedding以及位置embedding得到encode的时输入数据 输入部分 embedding就是从原始数据中提取出单词或位置&…

ubuntu22.04配置双网卡绑定提升带宽

这里写自定义目录标题 Bonding简介配置验证参考链接 Bonding简介 bonding(绑定)是一种linux系统下的网卡绑定技术&#xff0c;可以把服务器上n个物理网卡在系统内部抽象(绑定)成一个逻辑上的网卡&#xff0c;能够提升网络吞吐量、实现网络冗余、负载均衡等功能&#xff0c;有很…

2023年工作初体验

23年终于正式入职&#xff0c;参与了正式上线的电商平台、crm平台等项目的研发&#xff0c;公司规模较小&#xff0c;气氛融洽&#xff0c;没有任何勾心斗角、末位淘汰&#xff0c;几乎没什么压力。虽然是我的第一家公司&#xff0c;但实际是个适合养老的公司&#xff08;笑 总…

双击shutdown.bat关闭Tomcat报错:未设置关闭端口~

你们好&#xff0c;我是金金金。 场景 当我startup.bat启动tomcat之后&#xff0c;然后双击shutdown.bat关闭&#xff0c;结果报错了~ 排查 看报错信息很明显了&#xff0c;未配置关闭端口&#xff0c;突然想起来了我在安装的时候都选的是默认的配置&#xff0c;我还记得有这…

快速批量运行命令

Ansible 是 redhat 提供的自动化运维工具&#xff0c;它是 Python编写&#xff0c;可以通过 pip 安装。 pip install ansible 它通过任务(task)、角色(role)、剧本(playbook) 组织工作项目&#xff0c;适用于批量化系统配置、软件部署等需要复杂操作的工作。 但对于批量运行命…

宝塔部署nuxt3项目问题解决

使用宝塔部署nuxt3项目一直没成功&#xff0c;网站502&#xff0c;要不就是资源加载不出来 测试使用宝塔版本8.0.4 添加node项目方式失败&#xff0c;项目更目录设置到server,无法设置运行目录为public, 导致网站资源加载不出来&#xff0c;设置到.output目录&#xff0c;会提…

继电器组开发控制

也是通过树莓派IO口的控制来实现继电器组的开发 继电器组有四根信号线&#xff0c;2根电源线。 通过gpio readall 查看树莓派各个端口的信息选择26 27 28 29 作为信号端口 编程可能会遇到的一些问题 1、通过键盘输入指令的时候&#xff0c;如果用scanf 会有bug&#xff0c;导…

JavaScript高级程序设计读书记录(一):语言基础,语法,变量,数据类型

1. 语法 很大程度上借鉴了 C 语言和其他类 C 语言&#xff0c;如 Java 和 Perl. 1.1 区分大小写 1.2 标识符 第一个字符必须是一个字母、下划线&#xff08;_&#xff09;或美元符号&#xff08;$&#xff09;&#xff1b; 剩下的其他字符可以是字母、下划线、美元符号或数…

排除启动类故障----三大实验

目录 一、模拟破坏mbr和分区表然后修复 二、修复grub引导故障 三、遗忘root用户密码 一、模拟破坏mbr和分区表然后修复 1、mbr处于第一块磁盘的第一个物理扇区&#xff0c;总共512个字节&#xff0c;前446个字节是grub程序&#xff0c;后面64个字节是分区表 2、故障原因&a…

Linux 进程(九) 进程等待

子进程退出&#xff0c;父进程如果不管不顾&#xff0c;就可能造成‘僵尸进程’的问题&#xff0c;进而造成内存泄漏&#xff0c;所以父进程回收子进程是必然要做的。 另外&#xff0c;进程一旦变成僵尸状态&#xff0c;那就刀枪不入&#xff0c;“杀人不眨眼”的kill …

googlecode.log4jdbc慢sql日志,格式化sql

前言 无论使用原生JDBC、mybatis还是hibernate&#xff0c;使用log4j等日志框架可以看到生成的SQL&#xff0c;但是占位符和参数总是分开打印的&#xff0c;不便于分析&#xff0c;显示如下的效果: googlecode Log4jdbc 是一个开源 SQL 日志组件&#xff0c;它使用代理模式实…

day06、SQL语言之概述

SQl 语言之概述 6.1 SQL语言概述6.2 SQL语言之DDL定义数据库6.3 SQL语言之DML操纵数据库 6.1 SQL语言概述 6.2 SQL语言之DDL定义数据库 6.3 SQL语言之DML操纵数据库

BLE Mesh蓝牙组网技术详细解析之Foundation Model Layer基础模型层(七)

目录 一、什么是BLE Mesh Foundation Model Layer基础模型层&#xff1f; 二、模型 2.1 配置模型 2.2 健康模型 三、状态 3.1 Composition Data 四、资料获取 一、什么是BLE Mesh Foundation Model Layer基础模型层&#xff1f; BLE Mesh Foundation model Layer是蓝牙…

redis服务迁移数据工具--RDM

一、背景&#xff1a; 在日常的运维工作经常遇见各种数据迁移工作&#xff0c;例如mysql数据库迁移、redis数据库迁移、minio数据迁移等等工作。这里介绍一下redis数据库的迁移过程。 二、迁移思路&#xff1a; redis服务/集群的数据迁移思路是需要新建一个配置、密码一样的re…

【Leetcode】2487. 从链表中移除节点

文章目录 题目思路代码 题目 2487. 从链表中移除节点 思路 1、递归移除节点&#xff1a; 如果头节点为空&#xff0c;直接返回空。递归调用函数处理下一个节点 head->next。在递归返回后&#xff0c;判断当前节点的值是否小于之前记录的最大值 maxVal。如果小于 maxVal…