由于业务需要,需要Spring Boot集成RocketMq,进行事务消息的发送。相信很多道友在网上搜的话,应该有很多案例。但是大部分只是说到了单条事务消息的发送。而应用中,不同业务。可能会有多个事务消息的发送。查看网上很多资料,说的都不太正确。因此记录一下,我在业务中的正确处理方式。希望能帮到更多的道友来尽快摆脱多事务消息的困扰。
版本说明:
SpringBoot 2.2.2.RELEASE
rocketmq maven:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
一、首先定义controller
@RestController
@RequestMapping("/zhmy/mqTrans/")
@Slf4j
public class BookController {
private final BookServiceImpl bookService;
@Resource
private OrderMessageSender orderMessageSender;
@Resource
private ReduceStockMsgSender reduceStockMsgSender;
@PostMapping("/bookTransSave")
public ResultModel addBook(@RequestBody BookVO vo){
bookService.addBookTrans(vo);
orderMessageSender.sendCreateOrderMsg(vo);
return ResultModel.success();
}
@PostMapping("/bookTransSave2")
public ResultModel addBook2(@RequestBody BookVO vo){
bookService.addBookTrans(vo);
reduceStockMsgSender.sendCreateOrderMsg(vo);
return ResultModel.success();
}
}
二、从controller中,大家可看到我定义了两个mq生产者。具体代码如下
@Slf4j
@Component
public class OrderMessageSender {
@Resource(name ="rocketMQTemplate" )
private RocketMQTemplate rocketMQTemplate;
private String TAG = "cancelOrder";
private String TXTAG = "trans";
private String ORDERTAG = "create-order";
/**
* 使用事务消息机制发送订单
* @return
*/
public boolean sendCreateOrderMsg(BookVO bookVO){
String destination = RocketMqConstant.ORDER_ASYNC_TOPIC+":"+ORDERTAG;
Message<BookVO> build = MessageBuilder.withPayload(bookVO).build();
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(destination,build,bookVO.getId());
log.info("发送的rocketMQTemplate事务消息信息:{}", JSONUtil.toJsonStr(sendResult));
return SendStatus.SEND_OK == sendResult.getSendStatus();
}
}
@Component
@Slf4j
public class ReduceStockMsgSender {
@Autowired
private ZhmyRocketMQTemplate zhmyRocketMQTemplate;
private String TAG = "cancelOrder";
private String TXTAG = "trans";
private String ORDERTAG = "create-order2";
/**
*
* @param bookVO
* @return
*/
public boolean sendCreateOrderMsg(BookVO bookVO){
String destination = RocketMqConstant.ORDER_ASYNC_TOPIC2+":"+ORDERTAG;
Message<BookVO> build = MessageBuilder.withPayload(bookVO).build();
TransactionSendResult sendResult = zhmyRocketMQTemplate.sendMessageInTransaction(destination,build,bookVO.getId());
log.info("发送的extRocketMQTemplate事务消息信息:{}", JSONUtil.toJsonStr(sendResult));
return SendStatus.SEND_OK == sendResult.getSendStatus();
}
}
三、从生产者中,可以看到定义了两个RocketMQTemplate,一个是默认的,一个是咱们自定义的。
默认的无需处理了,看下自定义的是如何定义的。且生产组一定要和其他生产组区分出来
//一定要加上生产组,否则多事务消息的监听器都会监听到
@ExtRocketMQTemplateConfiguration(group = "zhmy-Test2")
public class ZhmyRocketMQTemplate extends RocketMQTemplate {
}
四、在定义两个监听器
@Slf4j
@RocketMQTransactionListener
public class TransactionListenerImpl2 implements RocketMQLocalTransactionListener {
private static int maxTryMums = 5;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private BookMapper bookMapper;
@Resource
private BookServiceImpl bookService;
private String ORDERTAG = "create-order";
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object object) {
try{
String transId = (String)msg.getHeaders().get(RocketMQHeaders.PREFIX + RocketMQHeaders.TRANSACTION_ID);
log.info("------------RocketMQ执行本地订单创建 transId: "+transId+"-------------");
/**
* 订单已提前生成,这里就不用记录本地订单了。
* */
log.info("object信息为:{}", JSONUtil.toJsonStr(object));
log.info("msg信息为:{}", JSONUtil.toJsonStr(msg));
//RocketMQUtil.convertToRocketMessage(new StringMessageConverter(), "UTF-8", destination, msg);
String destination = RocketMqConstant.ORDER_ASYNC_TOPIC2+":"+ORDERTAG;
org.apache.rocketmq.common.message.Message message =
RocketMQUtil.convertToRocketMessage(new StringMessageConverter(), "UTF-8", destination, msg);
log.info("输出msg信息:{}",JSONUtil.toJsonStr(msg));
BookVO vo = getBusinessMessage(msg);
log.info("vo信息为:{}", JSONUtil.toJsonStr(vo));
redisTemplate.opsForHash().put(RocketMqConstant.REDIS_CREATE_ORDER,vo.getId().toString(),0);
return RocketMQLocalTransactionState.UNKNOWN;
}catch (Exception e){
log.warn("-----创建RocketMQ下单事务消息失败, ---",e);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
try{
String transId = (String)msg.getHeaders().get(RocketMQHeaders.PREFIX + RocketMQHeaders.TRANSACTION_ID);
log.info("------------RocketMQ回查订单状态 transId: "+transId+"-------------");
BookVO vo = getBusinessMessage(msg);
// String message = new String((byte[]) msg.getPayload());
if(ObjectUtil.isNotNull(vo) && vo.getAuthor().contains("author") ){
Object redisRecord = redisTemplate.opsForHash().get(RocketMqConstant.REDIS_CREATE_ORDER, vo.getId());
if(null == redisRecord){
log.warn("-----RocketMQ订单事务回查失败, 没有Redis记录");
return RocketMQLocalTransactionState.ROLLBACK;
}
int retryTimes = (int)redisRecord;
//1、超过最大检查次数,表示订单超时未支付,关闭订单
if(retryTimes >= maxTryMums){
Book book = bookService.queryBook(vo.getId());
book.setTitle("超出最大次数,关闭了");
bookMapper.updateById(book);
redisTemplate.opsForHash().delete(RocketMqConstant.REDIS_CREATE_ORDER,vo.getId());
log.info("--- 订单下单事务消息 transID: "+transId+"支付超时,发送消息,释放锁定的库存");
return RocketMQLocalTransactionState.COMMIT;
}else{
//2、查询支付宝订单支付状态
//2.1 如果支付宝支付成功,这个方法中会更新订单paytype和status,并完成扣减库存
Book book = bookService.queryBook(vo.getId());
if(book.getPayStatus() == 1){
// msg.getHeaders().remove("CHECK_TIME");
// localTrans.remove(transId);
log.info("--- 订单下单事务消息 transID: "+transId+";订单号:"+vo.getId()+"已经完成支付,回滚消息");
return RocketMQLocalTransactionState.ROLLBACK;
}else{
log.info("--- 订单下单事务消息 transID: "+transId+";订单号:"+vo.getId()+"未支付,已回查"+retryTimes+"次,等待下次回查");
//记录回查次数,等待下次回查。
redisTemplate.opsForHash().increment(RocketMqConstant.REDIS_CREATE_ORDER,vo.getId().toString(),1);
// msg.getHeaders().put("CHECK_TIME",retryTimes+1);
return RocketMQLocalTransactionState.UNKNOWN;
}
}
}else{
log.info("----RocketMQ订单消息格式不对,丢弃消息。");
return RocketMQLocalTransactionState.ROLLBACK;
}
}catch (Exception e){
log.warn("-----RcoketMQ下单事务消息状态回查失败, ---",e);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* Get event instance from message payload.
*/
private BookVO getBusinessMessage(Message msg) throws JsonProcessingException {
final String json = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
BookVO bookVO = JSONUtil.toBean(json, BookVO.class);
return bookVO;
}
}
@Slf4j
@RocketMQTransactionListener(rocketMQTemplateBeanName="zhmyRocketMQTemplate") //一个事物监听器对应一个事物流程
public class ReduceStockMsgListener implements RocketMQLocalTransactionListener {
private static int maxTryMums = 5;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private BookMapper bookMapper;
@Resource
private BookServiceImpl bookService;
private String ORDERTAG = "create-order";
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object object) {
try{
String destination = RocketMqConstant.ORDER_ASYNC_TOPIC2+":"+ORDERTAG;
org.apache.rocketmq.common.message.Message message =
RocketMQUtil.convertToRocketMessage(new StringMessageConverter(), "UTF-8", destination, msg);
log.info("输出msg信息:{}",JSONUtil.toJsonStr(msg));
Object rocketmqTags = msg.getHeaders().get("rocketmq_TAGS");
log.info("tags信息为{}",rocketmqTags.toString());
String transId = (String)msg.getHeaders().get(RocketMQHeaders.PREFIX + RocketMQHeaders.TRANSACTION_ID);
log.info("-----extRocketMQTemplate-------RocketMQ执行本地订单创建 transId: "+transId+"-------------");
/**
* 订单已提前生成,这里就不用记录本地订单了。
* */
log.info("extRocketMQTemplate object信息为:{}", JSONUtil.toJsonStr(object));
log.info("extRocketMQTemplate msg信息为:{}", JSONUtil.toJsonStr(msg));
BookVO vo = getBusinessMessage(msg);
log.info("extRocketMQTemplate vo信息为:{}", JSONUtil.toJsonStr(vo));
redisTemplate.opsForHash().put(RocketMqConstant.REDIS_CREATE_ORDER,vo.getId().toString(),0);
return RocketMQLocalTransactionState.UNKNOWN;
}catch (Exception e){
log.warn("--extRocketMQTemplate---创建RocketMQ下单事务消息失败, ---",e);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
try{
String transId = (String)msg.getHeaders().get(RocketMQHeaders.PREFIX + RocketMQHeaders.TRANSACTION_ID);
log.info("-----extRocketMQTemplate-------RocketMQ回查订单状态 transId: "+transId+"-------------");
BookVO vo = getBusinessMessage(msg);
// String message = new String((byte[]) msg.getPayload());
if(ObjectUtil.isNotNull(vo) && vo.getAuthor().contains("author") ){
Object redisRecord = redisTemplate.opsForHash().get(RocketMqConstant.REDIS_CREATE_ORDER, vo.getId());
if(null == redisRecord){
log.warn("-extRocketMQTemplate----RocketMQ订单事务回查失败, 没有Redis记录");
return RocketMQLocalTransactionState.ROLLBACK;
}
int retryTimes = (int)redisRecord;
//1、超过最大检查次数,表示订单超时未支付,关闭订单
if(retryTimes >= maxTryMums){
Book book = bookService.queryBook(vo.getId());
book.setTitle("extRocketMQTemplate超出最大次数,关闭了");
bookMapper.updateById(book);
redisTemplate.opsForHash().delete(RocketMqConstant.REDIS_CREATE_ORDER,vo.getId());
log.info("--extRocketMQTemplate- 订单下单事务消息 transID: "+transId+"支付超时,发送消息,释放锁定的库存");
return RocketMQLocalTransactionState.COMMIT;
}else{
//2、查询支付宝订单支付状态
//2.1 如果支付宝支付成功,这个方法中会更新订单paytype和status,并完成扣减库存
Book book = bookService.queryBook(vo.getId());
if(book.getPayStatus() == 1){
// msg.getHeaders().remove("CHECK_TIME");
// localTrans.remove(transId);
log.info("--- 订单extRocketMQTemplate下单事务消息 transID: "+transId+";订单号:"+vo.getId()+"已经完成支付,回滚消息");
return RocketMQLocalTransactionState.ROLLBACK;
}else{
log.info("--- 订单extRocketMQTemplate下单事务消息 transID: "+transId+";订单号:"+vo.getId()+"未支付,已回查"+retryTimes+"次,等待下次回查");
//2.2 记录回查次数,等待下次回查。
redisTemplate.opsForHash().increment(RocketMqConstant.REDIS_CREATE_ORDER,vo.getId().toString(),1);
//消息的header不可修改。可以思考下为什么?
// msg.getHeaders().put("CHECK_TIME",retryTimes+1);
return RocketMQLocalTransactionState.UNKNOWN;
}
}
}else{
log.info("--extRocketMQTemplate--RocketMQ订单消息格式不对,丢弃消息。");
return RocketMQLocalTransactionState.ROLLBACK;
}
}catch (Exception e){
log.warn("--extRocketMQTemplate---RcoketMQ下单事务消息状态回查失败, ---",e);
return RocketMQLocalTransactionState.ROLLBACK;
}
}
/**
* Get event instance from message payload.
*/
private BookVO getBusinessMessage(Message msg) throws JsonProcessingException {
final String json = new String((byte[]) msg.getPayload(), StandardCharsets.UTF_8);
BookVO bookVO = JSONUtil.toBean(json, BookVO.class);
return bookVO;
}
}
notes:
@RocketMQTransactionListener(rocketMQTemplateBeanName="zhmyRocketMQTemplate") //一个事物监听器对应一个事物流程
五、定义两个消费者:
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "CancelOrderGroup2", topic = RocketMqConstant.ORDER_ASYNC_TOPIC2)
public class RocketMqCancelOrderReceiver implements RocketMQListener<BookVO> {
@Autowired
private BookServiceImpl bookService;
@Override
public void onMessage(BookVO bookVO) {
log.info("监控到订单支付超时事件,取消超时订单:BookVO:{} ", JSONUtil.toJsonStr(bookVO));
if(ObjectUtil.isNull(bookVO)){
return;
}
try {
//取消的订单,释放锁定的库存
bookService.delBook(bookVO);
} catch (Exception e) {
log.error("订单取消异常 : 还原库存失败,please check:{}",e.getMessage(),e.getCause());
throw new RuntimeException();//抛异常出去,rocketmq会重新投递
}
}
}
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "CancelOrderGroup", topic = RocketMqConstant.ORDER_ASYNC_TOPIC)
public class RocketMqCancelOrderReceiver2 implements RocketMQListener<BookVO> {
@Autowired
private BookServiceImpl bookService;
@Override
public void onMessage(BookVO bookVO) {
log.info("监控到订单支付超时事件,取消超时订单:BookVO:{} ", JSONUtil.toJsonStr(bookVO));
if(ObjectUtil.isNull(bookVO)){
return;
}
try {
bookService.delBook(bookVO);
} catch (Exception e) {
log.error("订单取消异常 : 还原库存失败,please check:{}",e.getMessage(),e.getCause());
throw new RuntimeException();//抛异常出去,rocketmq会重新投递
}
}
}
到此基本结束,开始进行实验:
controller调用:
服务端查看日志:
至此大家即可看到,事务多消息只被其对应的监听器监听处理了。