Java业务功能并发问题处理

业务场景:

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

java 复制代码
// 在数据加载时, 用户点击编辑详情时会请求当前方法, 方法会校验订单状态决定是否允许用户获取订单详情
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代码块

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

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

使用ConcurrentHashMap时需要注意使用完后要remove掉,避免出现其他线程获取不到锁甚至内存溢出的问题

其中使用了putIfAbsent方法保证原子操作,下面直接给出代码示例

java 复制代码
// 全局静态的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是关键

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

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

通过Redis分布式锁限制

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

java 复制代码
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());
    }
}
使用示例
java 复制代码
@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();
    }
}
相关推荐
福建佰胜张工28 分钟前
SMC 薄型气爪 MHF2-16D1 深度解析:平行开闭、低剖面结构在自动化装配中的选型与落地
java·服务器·安全·自动化
catoop31 分钟前
API Key 设计与安全生成方案:结构拆解、数据库设计与 Java 实现
java·数据库·安全
system_error_x1 小时前
SpringBoot 集成 Thymeleaf 简洁入门
java·spring boot·后端
驭渊的小故事1 小时前
Spring AI Alibaba 实战:从maven依赖治理到声明式工具调用
java·人工智能·spring
Elastic 中国社区官方博客2 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn1882 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn3 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su3 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
我命由我123454 小时前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
苜蓿muxu5 小时前
Java 字符串拼接:+、StringBuilder、StringBuffer的区别
java