RocketMQ事务消息用来解决本地事务和消息提交的状态同步。意思就是:本地事务成功,消息提交成功;本地事务失败,消息提交失败。否则会出现数据不一致的情况。本地事务提交并且创建成功,再保证消费者能成功消费消息,分布式事务就可以实现,如果一次业务操作需要关联很多微服务,各服务只要保证自己事务和消息的一致性就可以实现整个分布式事务的一致性,只不过模式为最终一致。
RocketMQ针对事务消息采用的机制是,创建半消息(预处理消息 RMQ_SYS_TRANS_HALF_TOPIC),不会被消费,保存在特殊队列中,监听器返回结果后更改状态,如果长时间没有返回结果,则定时查询。类似我们对接第三方支付接口一样,调用后等待结果,如果获取结果异常,通过查询接口获取最终结果。
异常情况
消息消费异常,需要人工干预
本地事务超时、异常,通过定时查询获取结果,用来提交或回滚消息
生产者配置
生产者核心Service类图
1. pom.xml引入依赖包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
2. yml配置
server:
port: 8081
spring:
application:
name: consumer
datasource:
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8&serverTimezone=GMT%2B8
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
cloud:
nacos:
config:
server-addr: localhost:8848
file-extension: yaml
discovery:
server-addr: localhost:8848
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: Producer-Group
3. 定义事务消息必须实现的接口
public interface TransactionalMQService {
/**
* 本地使用事务执行
* @param msg
* @return
*/
boolean process(String msg);
/**
* 查询本地事务结果
* @param msg
* @return
*/
boolean checkSuccess(String msg);
}
4. 抽象类AbstractRocketMQListener
实现TransactionListener接口,并实现对应方法;实现ApplicationContextAware接口用来获取执行对象的代理对象。
此处必须使用TransactionalMQService接口获取,否则有可能获取不到代理对象,导致事务失效。
@Component
public abstract class AbstractRocketMQListener implements TransactionListener, ApplicationContextAware {
@Value("${rocketmq.producer.group}")
private String producerGroupName;
private static final String TOPIC = "TOPIC_A";
private static ApplicationContext APPLICATION_CONTEXT;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
APPLICATION_CONTEXT = applicationContext;
}
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 执行本地事务 并获取执行结果
*
* @param message
* @param o
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
String msg = new String(message.getBody());
System.out.println("------------消息执行本地事务--------------");
try {
// 此处必须通过接口强制转换,否则很可能获取不到代理类
boolean isCommit = ((TransactionalMQService) APPLICATION_CONTEXT.getBean(this.getClass())).process(msg);
if (isCommit) {
System.out.println("------------消息执行提交--------------");
return LocalTransactionState.COMMIT_MESSAGE;
}
System.out.println("------------消息执行回滚--------------");
return LocalTransactionState.ROLLBACK_MESSAGE;
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("------------消息执行异常--------------");
return LocalTransactionState.UNKNOW;
}
/**
* 查询本地事务执行结果
*
* @param messageExt
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println("------------消息查询结果--------------");
// 此处必须通过接口强制转换,否则很可能获取不到代理类
boolean success = ((TransactionalMQService) APPLICATION_CONTEXT.getBean(this.getClass())).checkSuccess(new String(messageExt.getBody()));
if (success) {
System.out.println("------------查询消息提交--------------");
return LocalTransactionState.COMMIT_MESSAGE;
}
System.out.println("------------查询消息回滚--------------");
return LocalTransactionState.ROLLBACK_MESSAGE;
}
/**
* 发送事务消息
*
* @param tag 标签
* @param msg 消息内容
* @return
*/
boolean sendTransactionalMsg(String tag, String msg) {
String destination = StringUtils.isEmpty(tag) ? TOPIC : TOPIC + ":" + tag;
// 此处如果使用MQ其他版本,可能导致强转异常
((TransactionMQProducer) rocketMQTemplate.getProducer()).setTransactionListener(this);
org.springframework.messaging.Message message = MessageBuilder.withPayload(msg).build();
TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction(destination, message, null);
System.out.println("Send transaction msg result: " + sendResult);
return sendResult.getSendStatus() == SendStatus.SEND_OK;
}
/**
* 发送同步消息
*
* @param tag 标签
* @param msg 消息内容
* @return
*/
boolean sendSyncMsg(String tag, String msg) {
String destination = StringUtils.isEmpty(tag) ? TOPIC : TOPIC + ":" + tag;
org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(msg).build();
SendResult sendResult = rocketMQTemplate.syncSend(destination, message);
System.out.println("Send syn msg result: " + sendResult);
return sendResult.getSendStatus() == SendStatus.SEND_OK;
}
}
5. 定义接口
public interface UserService {
UserDTO getByUsername(String username);
// 创建消息并处理本地逻辑
boolean sendCreateUserMsg(String msg);
}
6. 添加实现类
@Service
public class UserServiceImpl extends AbstractRocketMQListener implements UserService, TransactionalMQService {
@Autowired
private UserMapper userMapper;
@Override
public UserDTO getByUsername(String username) {
return userMapper.selectByUsername(username);
}
@Override
public boolean sendCreateUserMsg(String msg) {
return super.sendTransactionalMsg("create_user", msg);
}
@Override
@Transactional(rollbackFor = Throwable.class)
public boolean process(String msg) {
UserDTO user = JSONObject.parseObject(msg, UserDTO.class);
String[] names = user.getUsername().split("_");
for (String username : names) {
user.setUsername(username);
user.setCreatedTime(new Date());
if (userMapper.add(user) != 1) {
throw new RuntimeException("Add user failed");
}
}
return true;
}
@Override
public boolean checkSuccess(String msg) {
// 查询数据是否已成功
UserDTO user = JSONObject.parseObject(msg, UserDTO.class);
String[] names = user.getUsername().split("_");
for (String username : names) {
if (userMapper.selectByUsername(username) == null) {
return false;
}
}
return true;
}
}
7. User实体类和Mapper
比较简单,不粘贴了,可根据Controller反推表字段。
8. 定义Controller
@RestController
public class IndexController {
@Autowired
private UserService userService;
@GetMapping("/index")
private String index(@RequestParam("name") String name) {
UserDTO user = new UserDTO();
user.setUsername(name);
user.setType(0);
boolean success = userService.sendCreateUserMsg(JSONObject.toJSONString(user));
return String.valueOf(success);
}
}
9. 执行结果
访问url: http://127.0.0.1:8081/index?name=1991_1991 事务成功回滚
------------消息执行本地事务--------------
org.springframework.dao.DuplicateKeyException:
### Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '1991' for key 'idx_username'
### The error may exist in com/cloud/demo/consumer/mapper/UserMapper.xml
### The error may involve com.cloud.demo.consumer.mapper.UserMapper.add-Inline
### The error occurred while setting parameters
### SQL: insert into u_user (`username`, `type`, `created_time`) values(?, ?, ?)
### Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry '1991' for key 'idx_username'
... 异常消息省略
------------消息执行异常--------------
Send transaction msg result: SendResult [sendStatus=SEND_OK, msgId=0A09A7D0E4B018B4AAC28A0E02170008, offsetMsgId=null, messageQueue=MessageQueue [topic=TOPIC_A, brokerName=DESKTOP-FEANKDA, queueId=0], queueOffset=200]
访问url: http://127.0.0.1:8081/index?name=1991_1990 事务成功提交
------------消息执行本地事务--------------
------------消息执行提交--------------
Send transaction msg result: SendResult [sendStatus=SEND_OK, msgId=0A09A7D0E4B018B4AAC28A0FCA78000A, offsetMsgId=null, messageQueue=MessageQueue [topic=TOPIC_A, brokerName=DESKTOP-FEANKDA, queueId=3], queueOffset=202]
消费者配置
添加时间监听器
如果抛出异常,消息会被回放至队列中,可再次消费,一般重复3~5次即可,可配置修改
不抛出异常,方法执行完毕,消息被消费完毕。
@Component
@RocketMQMessageListener(topic = "TOPIC_A", consumerGroup = "Group_create_user", selectorExpression = "create_user")
public class RocketMQConsumerService implements RocketMQListener<String> {
public void onMessage(String s) {
System.out.println("Receive msg: " + s);
// throw new RuntimeException("0000");
}
}
// 如果有多个消费者,selectorExpression可用于区分tag,由不同的consumerGroup区分 如下:
@Component
@RocketMQMessageListener(topic = "TOPIC_A", consumerGroup = "Group_increase_account", selectorExpression = "increase_account")
public class RocketMQConsumerService implements RocketMQListener<String> {
public void onMessage(String s) {
System.out.println("Receive increase account msg: " + s);
// throw new RuntimeException("0000");
}
}