- 什么是分布式事务
在分布式系统中,因为跨服务调用接口,存在多个不同的事务,每个事务都互不影响。就存在分布式事务的问题
- 分布式事务解决思路
- 使用强一致性
表示a系统更新的数据,b系统必须同时更新到。
- 使用最终一致性
表示a系统更新的数据,b系统可以暂时不更新,但是最终还是要同步。
本文采用的时最终一致性解决分布式事务。
- RocketMQ事务消息机制
- 事务消息 也称半消息
生产者发送一条事务消息,该消息不可以被消息。
待生产者发送commit或rollback指令后,rocketMQ会将消息删除、或投递消费者 - 代码思路
// 生产者发送事务消息 该消息不可被消费 rocketMQTemplate.sendMessageInTransaction("tx_groups_order", "my_topic", message, null); // COMMIT:即生产者通知Rocket该消息可以消费 RocketMQLocalTransactionState.COMMIT; // ROLLBACK:即生产者通知Rocket将该消息删除 RocketMQLocalTransactionState.ROLLBACK; // UNKNOWN:即生产者通知Rocket继续查询该消息的状态 RocketMQLocalTransactionState.UNKNOWN;
- 采用RocketMQ解决分布式事务思路
- 实现思路
1. 发送事务消息,该消息不可消费
2. 返回事务消息是否发送成功
3. 执行本地事务,将订单消息保存到数据库
4. 将本地事务结果返回给Rocket
5. 如果本地事务没有将结果返回给Rocket,则Rocket定时检查
6. 如果本地事务提交commit,则Rocket则将该消息设置为可以消费
思路: 保证订单要先保存到数据库且成功,消费者才可以拿到消息
- 基于rocket事务消息代码实现
生产者
- 创建订单项目
在application.properties配置如下:
手动事务工具类spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tbl_order?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver server.port=2018 // rocket的nameserver地址 rocketmq.name-server=192.168.2.115:9876 rocketmq.producer.group=groups_order
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; // 执行手动事务 @Service public class TransationalUtils { @Autowired public DataSourceTransactionManager transactionManager; public TransactionStatus begin() { TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionAttribute()); return transaction; } public void commit(TransactionStatus transaction) { transactionManager.commit(transaction); } public void rollback(TransactionStatus transaction) { transactionManager.rollback(transaction); } }
- 生产者发送事务消息
将订单消息以事务消息先发送给Rocket,此时该消费未保存到数据库。import com.alibaba.fastjson.JSONObject; import com.pitch.entity.Order; import com.pitch.mapper.OrderMapper; import org.apache.rocketmq.spring.core.RocketMQTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Service; import java.util.List; /** * @author xiaobo * @Description OrderService * @createTime 2020-03-25 20:08 */ @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RocketMQTemplate rocketMQTemplate; public List<Order> findAll(){ return orderMapper.findAll(); } public void saveOrder(Order order) throws Exception{ // 封装订单消息 String msg = JSONObject.toJSONString(order); MessageBuilder<String> stringMessageBuilder = MessageBuilder.withPayload(msg); stringMessageBuilder.setHeader("msg", msg); Message message = stringMessageBuilder.build(); // 发送事务消息 且该消息不允许消费 tx_groups_order: 指定版事务消息组 rocketMQTemplate.sendMessageInTransaction("tx_groups_order", "my_topic", message, null); } }
- 生产者本地事务方法
executeLocalTransaction():import com.alibaba.fastjson.JSONObject; import com.pitch.entity.Order; import com.pitch.mapper.OrderMapper; import com.pitch.utils.utils.TransationalUtils; import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener; import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener; import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.stereotype.Component; import org.springframework.transaction.TransactionStatus; /** * @author xiaobo * @Description ProducerListener * @createTime 2020-03-25 21:55 */ @Component @RocketMQTransactionListener(txProducerGroup = "tx_groups_order") // tx_groups_order:对应发送时的事务消息组 public class ProducerOrderListener implements RocketMQLocalTransactionListener { @Autowired private OrderMapper orderMapper; @Autowired private TransationalUtils transationalUtils; // 执行本地事务 @Override public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) { // 获取订单的半消息 MessageHeaders headers = message.getHeaders(); Object object = headers.get("msg"); Order order = JSONObject.parseObject((String)object, Order.class); TransactionStatus begin = null; try { // 开启事务 begin = transationalUtils.begin(); // 添加订单 int result = orderMapper.insert(order); // 提交事务 transationalUtils.commit(begin); // 通知broker 将半消息设置为可以消费 return null; } catch (Exception e) { if (begin != null) { // 如果出现异常 ,则回滚事务 transationalUtils.rollback(begin); // // 通知broker 将消息删除 return RocketMQLocalTransactionState.ROLLBACK; } } return null; } // 检查事务是否提交 --当本地事务没有返回结果给rocket,rocket就会定时通过该方法检查 @Override public RocketMQLocalTransactionState checkLocalTransaction(Message message) { // 获取订单的半消息 MessageHeaders headers = message.getHeaders(); Object object = headers.get("msg"); if (object == null) { // 如果不存在, 则通知broker删除消息 return RocketMQLocalTransactionState.ROLLBACK; } // 获取消息中订单id Order order = JSONObject.parseObject((String) object, Order.class); Long id = order.getId(); // 通过id查询订单是否下单成功 Order result = orderMapper.findById(id); if (result == null) { // 如果为null, 返回UNKNOWN, broker会再次重试(默认15次) return RocketMQLocalTransactionState.UNKNOWN; } // 通过broker, 该半消息可以消费 return RocketMQLocalTransactionState.COMMIT; } }
通过本地事务方法,将订单保存到数据库。
如保存成功则通知RocketMQ将改消息消费,否则删除该消息。
checkLocalTransaction
如果RocketMQ没有收到本地事务的返回结果,则RocketMQ通过该方法主动查询判断。
消费者
- 创建派单项目
application.propreties配置如下:
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/tbl_send?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC spring.datasource.username=root spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver server.port=2019 rocketmq.name-server=192.168.2.115:9876 rocketmq.producer.group=groups_send
- 消费者代码
import com.alibaba.fastjson.JSONObject; import com.pitch.service.SendService; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.spring.annotation.ConsumeMode; 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.Component; /** * @author xiaobo * @Description UserConsumer * @createTime 2020-03-24 19:59 */ @Component // topic:主题 pitch_groups:组 ConsumeMode.ORDERLY: 一个队列对应一个线程 consumeThreadMax:消费者最大线程为1 @RocketMQMessageListener(topic = "my_topic", consumerGroup = "groups_order" , consumeMode = ConsumeMode.ORDERLY, consumeThreadMax = 1) public class SendConsumer implements RocketMQListener<MessageExt> { @Autowired private SendService sendService; @Override public void onMessage(MessageExt m) { // 获取订单消息 String result = new String(m.getBody()); JSONObject json = JSONObject.parseObject(result); Long id = json.getLong("id"); // 开始派单 sendService.saveSend(id); System.out.println("用户:"+Thread.currentThread().getName()+"开始派单,"+"--订单消息:"+result); } }
- 测试