通过MQ实现分布式事务
我们以简单的外卖系统逻辑举例
一、引入MQ之前的分布式架构
这个时候就会发生分布式事务问题,订单系统和配送系统的相对独立的,假设订单系统调用配送系统后超时了,然后订单系统异常回滚了,但是配送系统还是正常处理了,这就导致了事务问题,数据不一致了。
具体我们看代码示例:
订单系统:
/** 创建订单 */
@Transactional(rollbackFor = Exception.class) // 订单创建整个方法添加事务
public void createOrder(JSONObject orderInfo) throws Exception {
// 1. 订单信息 - 插入订单系统,订单数据库(事务-1)
orderDatabaseService.saveOrder(orderInfo);
// 2. 通过http接口发送订单信息到 运单系统
String result = callDispatchHttpApi(orderInfo);
if (!"ok".equals(result)) {
throw new Exception("订单创建失败,原因[运单接口调用失败]");
}
}
/**
* 通过http接口发送 运单系统,将订单号传过去
*
* @return 接口调用结果
*/
public String callDispatchHttpApi(JSONObject orderInfo) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
// 链接超时时间 > 3秒
requestFactory.setConnectTimeout(3000);
// 处理超时时间 > 2 秒
requestFactory.setReadTimeout(2000);
RestTemplate restTemplate = new RestTemplate(requestFactory);
String httpUrl = "http://127.0.0.1:8080/dispatch-api/dispatch?orderId=" + orderInfo.getString("orderId");
String result = restTemplate.getForObject(httpUrl, String.class);
return result;
}
配送系统:
@Transactional
public void dispatch(String orderId) throws Exception {
// 往数据库插入一条记录 调度系统数据库事务2
String sql = "insert into table_dispatch (dispatch_seq, order_id,dispatch_content) values (UUID(), ?, ?)";
int update = jdbcTemplate.update(sql, orderId, "派送此订单");
if (update != 1) {
throw new SQLException("调度数据插入失败,原因[数据库操作]");
}
}
单个系统中都做了事务处理,但是由于是分布式系统,两边相对独立,需要有一个机制来进行分布式事务的处理,也就是要么两边都失败回滚,要么两边都成功。
二、引入MQ后的分布式架构
订单系统:
增加MQ
@Service
@Transactional(rollbackFor = Exception.class)
public class MQService {
@Autowired
JdbcTemplate jdbcTemplate;
@Autowired
RabbitTemplate rabbitTemplate;
@PostConstruct
private void setup(){
//消息发送成功后调用
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
//ack为false,说明消息没有准确收到,返回
if (!b){
return;
}
String sql = "update tb_distributed_message set msg_status=1 where unique_id=?";
int count = jdbcTemplate.update(sql,correlationData.getId());
if (count != 1) {
System.out.println("更新消息状态出错");
}
}
});
}
/**
* 发送MQ消息,修改本地消息表的状态
*
* @throws Exception
*/
public void sendMsg(JSONObject msg) {
rabbitTemplate.convertAndSend("createOrderExchange", "", msg.toString(),
new CorrelationData(msg.getString("orderId")));
}
}
然后在下单后调用MQ
/** 创建订单 */
@Transactional(rollbackFor = Exception.class) // 订单创建整个方法添加事务
public void createOrder(JSONObject orderInfo) throws Exception {
// 1. 订单信息 - 插入订单系统,订单数据库(事务-1)
orderDatabaseService.saveOrder(orderInfo);
// 2. 将订单信息发布出去,等待配送系统订阅
mQService.sendMsg(orderInfo);
}
配送系统:
对MQ进行监听,有消息就消费,将配送单的创建放到接收到MQ后操作,成功消费就给订单系统正常反馈,反之就异常,保证两边的数据统一,从而达到分布式事务的目的,当然了只要保证数据最终一致即可,不是即时的。
@Component
public class OrderDispatchConsumer {
private final Logger logger = LoggerFactory.getLogger(OrderDispatchConsumer.class);
@Autowired
DispatchService dispatchService;
@RabbitListener(queues = "orderDispatchQueue")
public void messageConsumer(String message, Channel channel, @Header(AmqpHeaders.DELIVERY_TAG) long tag)
throws Exception {
try {
// mq里面的数据转为json对象
JSONObject orderInfo = JSONObject.parseObject(message);
logger.warn("收到MQ里面的消息:" + orderInfo.toJSONString());
Thread.sleep(5000L);
// 执行业务操作,同一个数据不能处理两次,根据业务情况去重,保证幂等性。 (拓展:redis记录处理情况)
String orderId = orderInfo.getString("orderId");
// 这里就是一个分配外卖小哥...
dispatchService.dispatch(orderId);
// ack - 告诉MQ,我已经收到啦
channel.basicAck(tag, false);
} catch (Exception e) {
// 异常情况 :根据需要去: 重发/ 丢弃
// 重发一定次数后, 丢弃, 日志告警
channel.basicNack(tag, false, false);
// 系统 关键数据,永远是有人工干预
}
// 如果不给回复,就等这个consumer断开链接后,mq-server会继续推送
}
}
三、测试
启动配送系统:
启动订单系统:
模拟请求(成功):
订单表和配送表正常录入
模拟请求(失败):
模拟请求异常,看下数据库的插入情况
都回滚了,测试完成
PS:这里的回滚需要人为处理,不能用自带的回滚机制。
我们在开发调试以及产品部署的过程中,难免要使用到shell工具
最近发现了一款同类产品FinalShell,还是一款良心国货。初步体验了一下,确实是良心之作。且免费(通用版),支持国货。
官网:http://www.hostbuf.com/
FinalShell是一体化的的服务器,网络管理软件,不仅是ssh客户端,还是功能强大的开发,运维工具,充分满足开发,运维需求。
特色功能:
免费海外服务器远程桌面加速,ssh加速,双边tcp加速,内网穿透。