Spring MVC控制器同步方法优化:基于交易ID的并发控制咨询
针对同一交易ID同步、不同交易并行的Spring MVC解决方案
首先得说,你原来的代码问题出在锁的范围太大——用synchronized(this)锁的是整个控制器实例,不管交易ID是什么,所有请求都得排队串行处理,这自然就没法让不同交易并行运行了。
你修改后用String.valueOf(tranID).intern()作为锁对象的思路是对的,核心逻辑就是让同一交易ID的请求共用同一把锁,不同交易ID用不同的锁,这样不同交易就能并行处理,同一交易的重复请求会串行。不过这个方案有几个坑得注意:
用intern()的潜在问题
- 内存泄漏风险:
intern()后的字符串会被存入字符串常量池,这个池里的对象一般不会被GC回收。如果你的系统有大量不同的交易ID,时间长了常量池会越来越大,可能导致内存溢出。 - NullPointerException风险:如果
tranID是null,String.valueOf(tranID)会得到"null"字符串,但如果直接用tranID.intern()就会报错,你用String.valueOf()处理是对的,但最好再显式兜底处理null场景。 - 恶意请求攻击:如果有人故意用大量不同的随机tranID发起请求,会快速填满字符串常量池,直接搞垮系统。
更安全的替代方案:自定义锁对象缓存
推荐用ConcurrentHashMap来维护每个交易ID对应的锁对象,既避免了字符串常量池的问题,又能保证锁的唯一性:
import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @Controller public class PayController { // 用ConcurrentHashMap存储锁对象,天然线程安全 private static final ConcurrentHashMap<String, Object> TRANSACTION_LOCKS = new ConcurrentHashMap<>(); @RequestMapping(value="/pay", method=RequestMethod.POST) public String payAck(HttpServletRequest httpRequest, HttpServletResponse httpResponse, HttpSession session) { String tranID = httpRequest.getParameter("tranID"); // 处理tranID为null的情况,避免空指针和异常 String safeTranID = Objects.requireNonNullElse(tranID, "DEFAULT_TRAN_ID"); // 原子性获取或创建锁对象,确保同一tranID只会有一个锁实例 Object lock = TRANSACTION_LOCKS.computeIfAbsent(safeTranID, key -> new Object()); try { synchronized (lock) { return processPayAck(httpRequest, httpResponse, session); } } finally { // 处理完成后移除锁,避免内存泄漏 // 不用担心并发问题:就算刚移除,新的请求进来会重新创建锁,不影响业务逻辑 TRANSACTION_LOCKS.remove(safeTranID); } } // 这里不需要再用synchronized修饰了,锁已经在外面按交易ID精准控制 public String processPayAck(HttpServletRequest httpRequest, HttpServletResponse httpResponse, HttpSession session) { // 你的支付确认逻辑:先检查交易是否已处理 if (isTransactionNotProcessed(httpRequest)) { callWS(); // 执行核心业务逻辑 return "success_url"; } else { // 重复请求直接返回结果,不用再走业务流程 return "duplicate_url"; } } // 模拟检查交易是否已处理的方法(实际需从数据库/缓存读取状态) private boolean isTransactionNotProcessed(HttpServletRequest httpRequest) { String tranID = httpRequest.getParameter("tranID"); // 这里写实际的校验逻辑:比如查数据库看该tranID是否已标记为处理完成 return true; } }
这个方案的优点:
- 锁粒度精准:每个交易ID对应独立的锁,不同交易完全并行,同一交易的重复请求串行。
- 避免内存泄漏:请求处理完就把锁对象从Map中移除,不会像
intern()那样一直占用内存。 - 线程安全:
ConcurrentHashMap的computeIfAbsent方法是原子操作,不会出现同一tranID创建多个锁的情况。
额外注意事项
- 去掉
processPayAck的synchronized修饰:你原来的方法加了synchronized,会锁整个控制器实例,和我们的细粒度锁冲突,必须去掉。 - 分布式场景换分布式锁:如果你的系统是多实例部署的,本地锁就没用了——不同服务器的控制器实例是独立的,同一交易ID的请求可能落到不同节点,这时候得用Redis分布式锁(比如Redisson)来跨节点控制同步。
- 交易状态必须持久化:不管用哪种锁,都要在业务逻辑里把交易状态存入数据库/缓存,因为锁只能保证内存中的同步,万一服务器重启,锁失效了,重复请求还是得靠持久化的状态来判断是否已处理。
内容的提问来源于stack exchange,提问作者deadend
相关产品推荐
相关产品推荐

