背景

最近项目有这么一个问题:页面有一个“分单排定”的功能,针对选择的订单记录可以调用后台接口对这条订单进行“分单”操作,即对相关业务表的insert、update(在同一个事务中),然后控制流程的往下流转(未加入到事务)。但有反馈有同一条订单被“分单”多次的现象,于是让前端检查了防止多次触发提交以及提交遮罩层的功能。可是用户仍然可能点击提交之后刷新页面重复提交(提交接口处理时间长)

解决方法

为了防止用户点击提交后刷新页面重复提交,一方面优化后台接口效率;另一方面在加了事务的业务方法入口增加以下逻辑:

  • 如果该订单ID没有缓存到redis中:那么将该订单ID缓存到redis中,代表该订单正在进行一系列的业务操作事务中,并且发布一个事件,然后正常进行后续的业务逻辑;
  • 如果该订单ID在redis中存在,说明该订单正在业务逻辑事务中,不能重复运行后续的业务逻辑;
  • 同时使用 @TransactionalEventListener注解实现事务事件监听。当事务事件完成后,将监听到的事件(订单ID)从redis中删掉;

具体实现

业务类

	@Transactional(rollbackFor = Exception.class)
	@Override
	public R<RollplanReleaseLaunchVO> savePrepareOrder(PrepareOrderSaveVO prepareOrderSaveVO) {
		// 从redis中检查当前订单是否正在处理中; 如果是,提示稍等;如果否,将订单号缓存到redis中。(当此事务完成后触发事务事件从redis中删掉该订单号)
		boolean orderIdSaveCheck = redisTemplate.boundSetOps(ProductionConstant.PREPARE_ORDER_SET).isMember(prepareOrderSaveVO.getOrderNum());
		if (orderIdSaveCheck) {
			return R.failed("订单正在分单排定中,请稍后查询");
		} else {
			redisTemplate.boundSetOps(ProductionConstant.PREPARE_ORDER_SET).add(prepareOrderSaveVO.getOrderNum());
			//发布事件
						SpringContextHolder.getApplicationContext().publishEvent(prepareOrderSaveVO.getOrderNum());
		}
		//TODO 以下为正常业务逻辑
	}

监听器类

import com.jic.xxx.XXX.api.constant.ProductionConstant;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

/**
 * @author Lu#####
 * @Title 分单排定事务事件监听器
 * @date 2022/3/22 9:18
 */
@Slf4j
@Component
public class PrepareOrderTransactionalEventListener {

	@Autowired
	private RedisTemplate redisTemplate;

	/**
	 * 分单排定事务完成后:
	 * 		从redis中删掉该订单正在分单排定的记录
	 * @param orderId 分单排定完成的订单号
	 */
	@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
	public void prepareOrderCompletion(String orderId) {
		log.info("prepareOrderCompletion:" + orderId);
		if (orderId != null) {
			redisTemplate.boundSetOps(ProductionConstant.PREPARE_ORDER_SET).remove(orderId);
		}
	}
}

拓展

使用@EventListener注解来注册常规事件监听器
Spring还提供了专门针对事务的的事件监听器,事务监听器可以绑定到事务的某一个阶段。比如事务提交成功或者回滚后才触发事件,其中实现事务监听的一种方式就是使用@TransactionalEventListener注解
事务监听器专门用于监听事务中发布的事件,@TransactionalEventListener注解包装了@EventListener注解,是普通监听器的加强,但是监听器方法是通过回调触发的,即在事务进行gcommit或者rollback的时候会回调监听器方法进行处理。而其他的,事务事件的发布方式和普通事件的发布方式是一样的,只不过事务事件必须在事务中发布。如果发布了“事务事件”,并且事件类型和某些普通监听器监听的事件类型一致,那么普通监听器也会被触发!
@TransactionalEventListener和@EventListener一样,都是同步处理,即处理事件和发布事件的线程是同一个,因此仍然可能会阻塞线程,但是可以使用@Async进行异步任务处理!

@TransactionalEventListener可以通过phase属性指定触发阶段,其中有四个枚举:

package org.springframework.transaction.event;

import java.util.function.Consumer;
import org.springframework.transaction.support.TransactionSynchronization;

public enum TransactionPhase {

	/**
	 * 在事务提交之前触发事件。
	 */
	BEFORE_COMMIT,

	/**
	 * 在成功完成提交后触发事件。这是默认的触发阶段。
	 */
	AFTER_COMMIT,

	/**
	 * 如果事务已回滚,则触发事件。
	 */
	AFTER_ROLLBACK,

	/**
	 * 事务完成之后(无论是提交还是回滚)进行触发。
	 * 如果同时注册AFTER_COMPLETION和AFTER_ROLLBACK/AFTER_COMMIT事件,
	 * 那么触发的先后顺序是不固定的,但是可以使用@Order注解指定先后顺序。
	 */
	AFTER_COMPLETION

}

@TransactionalEventListener标注的监听器方法,在默认情况下仅仅会被事务中发布的事件触发,如果需要在没有事务也能当作普通时间监听器触发,那么需要将fallbackExecution属性设置为true。

感谢

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐