文章目录
前言
有种场景,方法上即便是加了 @Transactional(rollbackFor = Exception.class) 也会出现分布式事务的问题。
例如:public void fun(){
update1();
MQsend();
update2();
}
update2();出现异常,update1();可以回滚,但是MQ消息已经发出去了,撤不回来了。
针对与这种情况,RocketMQ 实现了事务消息。
一、RocketMQ 事务消息是什么?
1.本地方法执行发送MQ事务消息,这个消息实际被标注了"不可投递"
2.本地方法执行完成后会发送"二次确认:提交\回滚",此时MQ消息就知道这个消息是要投递还是要丢弃了。
3.如果MQ没有收到"二次确认",也就是断网或者异常了,则MQ会进行"回查"检查本地事务状态,根据回查的结果进行投递或者丢弃。
二、使用步骤
例如:文章审核功能,审核员在看完一篇文章之后点击审核通过,传给后台论文id和审核信息。
1.生产者
private final RocketMQTemplate rocketMQTemplate;
public Share auditById(Integer id, ShareAuditDTO auditDTO) {
Share share = this.shareMapper.selectByPrimaryKey(id);
if (share == null) {
throw new IllegalArgumentException("参数非法!该分享不存在!");
}
if (!Objects.equals("NOT_YET", share.getAuditStatus())) {
throw new IllegalArgumentException("参数非法!该分享已审核通过或审核不通过!");
}
//如果是PASS,那么发送消息给rocketmq,让用户中心去消费,并为发布人添加积分
if (AuditStatusEnum.PASS.equals(auditDTO.getAuditStatusEnum())) {
//把需要与MQ消息做一致事务的业务逻辑 挪到MQ的TransactionListener里面
String uuid = UUID.randomUUID().toString();
//sendMessageInTransaction(txGroup,Topic,Message,Ojb args)
//txGroup:要与TransactionListener的@RocketMQTransactionListener的txProducerGroup一致
//Topic要与消费者的Listener的@RocketMQMessageListener的topic一致
//Message 以及 Message的Header 和args 都是用来传参数的,以变执行业务逻辑
this.rocketMQTemplate.sendMessageInTransaction(
"tx_add_bonus_group",
"add_bouns",
MessageBuilder.withPayload(UserAddBonusMsgDTO.builder().userId(share.getUserId()).bonus(50).build()).setHeader(RocketMQHeaders.TRANSACTION_ID,uuid).setHeader("share_id",id).build(),
auditDTO
);
//TODO 其他业务逻辑
}else{
this.auditByIdInDB(id, auditDTO);
}
return share;
}
@Transactional(rollbackFor = Exception.class)
public void auditByIdInDB(Integer id, ShareAuditDTO auditDTO) {
Share share = Share.builder()
.id(id)
.auditStatus(auditDTO.getAuditStatusEnum().toString())
.reason(auditDTO.getReason())
.build();
this.shareMapper.updateByPrimaryKeySelective(share);
}
@Transactional(rollbackFor = Exception.class)
public void auditByIdWithRocketMqLog(Integer id, ShareAuditDTO auditDTO, String transactionId) {
this.auditByIdInDB(id, auditDTO);
this.rocketmqTransactionLogMapper.insertSelective(
RocketmqTransactionLog.builder()
.transactionId(transactionId)
.log("审核分享...")
.build()
);
}
2.生产者TransactionListener
import com.itmuch.contentcenter.dao.RocketmqTransactionLog.RocketmqTransactionLogMapper;
import com.itmuch.contentcenter.domain.dto.content.ShareAuditDTO;
import com.itmuch.contentcenter.domain.entity.RocketmqTransactionLog.RocketmqTransactionLog;
import com.itmuch.contentcenter.service.content.ShareService;
import lombok.RequiredArgsConstructor;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
@RocketMQTransactionListener(txProducerGroup = "tx_add_bonus_group")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddBonusTransactionListener implements RocketMQLocalTransactionListener {
private final ShareService shareService;
private final RocketmqTransactionLogMapper rocketmqTransactionLogMapper;
@Override
//执行本地事务
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object args) {
MessageHeaders headers = message.getHeaders();
String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
Integer shareId = Integer.valueOf((String) headers.get("share_id"));
ShareAuditDTO auditDTO = (ShareAuditDTO)args;
try{
//执行本地的业务逻辑,并存MQ消息到LOG表
//存LOG表是为了实现'本地事务的检查接口',遇到长时间不给MQ发二次确认消息的本地事务,MQ会通过检查接口去查LOG表,查到了数据则返回COMMIT,没有查到返回ROLLBACK
this.shareService.auditByIdWithRocketMqLog(shareId,auditDTO,transactionId);
return RocketMQLocalTransactionState.COMMIT;
}catch (Exception e){
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
//本地事务的检查接口
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
MessageHeaders headers = message.getHeaders();
String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
RocketmqTransactionLog rocketmqTransactionLog = this.rocketmqTransactionLogMapper.selectOne(RocketmqTransactionLog.builder().transactionId(transactionId).build());
if(rocketmqTransactionLog !=null){
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
}
3.消费者
import com.itmuch.usercenter.dao.user.BonusEventLogMapper;
import com.itmuch.usercenter.dao.user.UserMapper;
import com.itmuch.usercenter.domain.entity.dto.massaging.UserAddBonusMsgDTO;
import com.itmuch.usercenter.domain.entity.user.BonusEventLog;
import com.itmuch.usercenter.domain.entity.user.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Date;
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumer-group",topic = "add_bouns")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
@Service
public class AddBonusListener implements RocketMQListener<UserAddBonusMsgDTO> {
private final UserMapper userMapper;
private final BonusEventLogMapper bonusEventLogMapper;
@Override
public void onMessage(UserAddBonusMsgDTO userAddBonusMsgDTO) {
User user = userMapper.selectByPrimaryKey(userAddBonusMsgDTO.getUserId());
user.setBonus(user.getBonus()+userAddBonusMsgDTO.getBonus());
userMapper.updateByPrimaryKey(user);
bonusEventLogMapper.insert(BonusEventLog.builder().userId(user.getId()).event("UPDATE").createTime(new Date()).description("加积分").value(userAddBonusMsgDTO.getBonus()).build());
}
}
三. 基于Spring Cloud Stream + RocketMQ实现分布式事务
1.生产者、消费者 加依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>
2. 生产者加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876
bindings:
output:
producer:
transational: true
group: tx_add_bonus_group #source.output().send 的group
bindings:
output: #也叫channel
destination: stream-topic #指定 source.output().send的topic
3.生产者加注解
启动类上加注解
@EnableBinding(Source.class)
4.生产者
private final Source source;
public Share auditById(Integer id, ShareAuditDTO auditDTO) {
Share share = this.shareMapper.selectByPrimaryKey(id);
if (share == null) {
throw new IllegalArgumentException("参数非法!该分享不存在!");
}
if (!Objects.equals("NOT_YET", share.getAuditStatus())) {
throw new IllegalArgumentException("参数非法!该分享已审核通过或审核不通过!");
}
//如果是PASS,那么发送消息给rocketmq,让用户中心去消费,并为发布人添加积分
if (AuditStatusEnum.PASS.equals(auditDTO.getAuditStatusEnum())) {
//把需要与MQ消息做一致事务的业务逻辑 挪到MQ的TransactionListener里面
String uuid = UUID.randomUUID().toString();
//Message 以及 Message的Header 都是用来传参数的,以变执行业务逻辑
//source的send(),只有一个Message参数,没有args,所以原来args的参数需要放到header里
//send的topic和group都在配置文件里指定了
this.source.output().send(
MessageBuilder.withPayload(
UserAddBonusMsgDTO.builder().userId(share.getUserId()).bonus(50).build()).setHeader(RocketMQHeaders.TRANSACTION_ID,uuid
).setHeader("share_id",id).setHeader("dto",JSON.toJSONString(auditDTO)).build());
//TODO 其他业务逻辑
}else{
this.auditByIdInDB(id, auditDTO);
}
return share;
}
5.生产者TransactionListener
import com.alibaba.fastjson.JSON;
import com.itmuch.contentcenter.dao.RocketmqTransactionLog.RocketmqTransactionLogMapper;
import com.itmuch.contentcenter.domain.dto.content.ShareAuditDTO;
import com.itmuch.contentcenter.domain.entity.RocketmqTransactionLog.RocketmqTransactionLog;
import com.itmuch.contentcenter.service.content.ShareService;
import lombok.RequiredArgsConstructor;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
@RocketMQTransactionListener(txProducerGroup = "tx_add_bonus_group")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddBonusTransactionListener implements RocketMQLocalTransactionListener {
private final ShareService shareService;
private final RocketmqTransactionLogMapper rocketmqTransactionLogMapper;
@Override
//执行本地事务
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object args) {
MessageHeaders headers = message.getHeaders();
String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
Integer shareId = Integer.valueOf((String) headers.get("share_id"));
//从headers里取到的都是String
String dtoString = (String) headers.get("dto");
ShareAuditDTO auditDTO = JSON.parseObject(dtoString, ShareAuditDTO.class);
try{
//执行本地的业务逻辑,并存MQ消息到LOG表
//存LOG表是为了实现'本地事务的检查接口',遇到长时间不给MQ发二次确认消息的本地事务,MQ会通过检查接口去查LOG表,查到了数据则返回COMMIT,没有查到返回ROLLBACK
this.shareService.auditByIdWithRocketMqLog(shareId,auditDTO,transactionId);
return RocketMQLocalTransactionState.COMMIT;
}catch (Exception e){
return RocketMQLocalTransactionState.ROLLBACK;
}
}
@Override
//本地事务的检查接口
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
MessageHeaders headers = message.getHeaders();
String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
RocketmqTransactionLog rocketmqTransactionLog = this.rocketmqTransactionLogMapper.selectOne(RocketmqTransactionLog.builder().transactionId(transactionId).build());
if(rocketmqTransactionLog !=null){
return RocketMQLocalTransactionState.COMMIT;
}
return RocketMQLocalTransactionState.ROLLBACK;
}
}
7.消费者加配置
spring:
cloud:
stream:
rocketmq:
binder:
name-server: localhost:9876
bindings:
input:
destination: stream-topic #消费哪个topic
group: stream-group #一定要指定,否则启动不了
8.消费者加注解
@EnableBinding(Sink.class)
9.消费者
import com.itmuch.usercenter.domain.entity.dto.massaging.UserAddBonusMsgDTO;
import com.itmuch.usercenter.service.user.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.stereotype.Service;
@Service
@Slf4j
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class AddBonusStreamListener {
private final UserService userService;
@StreamListener(Sink.INPUT)
public void receive(UserAddBonusMsgDTO userAddBonusMsgDTO){
log.info("AddBonusStreamListener.receive.userAddBonusMsgDTO:"+userAddBonusMsgDTO.toString());
//业务逻辑
userService.addBonus(userAddBonusMsgDTO);
}
@StreamListener("errorChannel")
public void error(Message<?> message){
ErrorMessage errorMessage = (ErrorMessage)message;
log.warn("出现异常:{}",errorMessage);
}
}