分布式事务的本质是最终一致性。
分布式事务的几种解决方案
- 基于数据库XA/JTA协议的方式;
需要数据库厂商支持;JAVA组件有atomikos等- 异步校对数据的方式;
支付宝、微信支付主动查询支付状态、对账单的形式;- 基于可靠消息(MQ)的解决方案;
异步场景、通用性较强、拓展性较高- TCC编程式解决方案;
严选、阿里、蚂蚁金服自己封装的DTX;
分布式系统的数据一致性问题
错误示例
@Transaction(rollbackFor=Exception.class)
public void createOrder(JSONObject orderInfo) throws Exception {
// 1. 保存订单信息(订单系统)
orderService.saveOrder(orderInfo);
// 2. 通过HTTP接口发送订单信息到运单系统
String result = callDispatchHttpApi(orderInfo);
if(!"ok".equals(result)) {
throw new Exception("订单创建失败,原因:运单接口调用失败!");
}
}
分布式事务问题:
1. 接口调用成功,订单系统数据库事务提交失败,运单系统没有回滚,产生数据;
2. 接口调用超时,订单系统数据库事务回滚,运单系统接口继续执行,产生数据;
消息确认
分布式事务的实现方案
1. 可靠地发送消息(本地消息表 + 确认机制)
生产者(订单中心应用程序)在保存订单记录的同一个本地事务中,同时将要发送到运单中心的消息保存到本地消息表中;
public class OrderDBService {
/**
* 保存订单记录
*/
public void saveOrder(JSONObject orderInfo) throws Exception {
// 1. 保存订单记录
String sql = "insert into t_order(order_id, user_id, order_content, create_time) values(?,?,?,now())";
int count = jdbcTemplate.update(sql, orderInfo.get("orderId"), orderInfo.get("userId"), orderInfo.get("orderContent"));
if(count != 1) {
throw new Exception("订单创建失败,原因:数据库操作失败!");
}
// 2. 记录发往MQ消息记录
saveMQLocalMessage(orderInfo);
}
/**
* 记录发往MQ消息记录
*/
public void saveMQLocalMessage(JSONObject orderInfo) throws Exception {
String sql = "insert into t_mq_message(unique_id, msg_content, msg_status, create_time) values(?,?,?,now())";
int count = jdbcTemplate.update(sql, orderInfo.get("orderId"), orderInfo.toJSONString(),0, now());
}
}
保存订单后,将订单消息发送到RabbitMQ服务器中(开启发布确认机制);收到确认后更新本地消息表的状态;
spring:
rabbitmq:
host: XXX.XXX.XXX.XXX
port: YYYY
username: AAA
password: BBB
publisher-confirms: true # 开启消息发送确认机制
public class MQService implements RabbitTemplate.ConfirmCallback {
public void sendMsg(JSONObject orderInfo) throw Exception {
// 发送消息到MQ
// CorrelationData当收到消息回执时,会附带上这个参数(orderId)
rabbitTemplate.convertAndSend("createOrderExchange", "", orderInfo.toJSONString(), new CorrelationData(orderInfo.getString("orderId"));
}
/**
* 回调
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println(" 回调id:" + correlationData);
if (ack) { // 成功确认
System.out.println("消息成功消费");
} else { // 失败确认
System.out.println("消息消费失败:" + cause);
// TODO 补偿或者其他后置处理
}
}
}
注意:如果出现回执没收到、消息状态修改失败等特殊情况。
处理方案:定时检查消息表,超时没发送成功,再次重发;
2. 可靠地消费消息(幂等性 + 手动确认机制)
防止消息重复处理,需先校验数据是否已被处理。
开启手动ACK模式:由消费者控制消息的重发/清除/丢弃。
【正常处理确认】
注意:出现异常一般会重试几次,由消费者自身记录重试次数,并进行次数控制(不会永远重试)
【失败丢弃或转移死信队列】
重试次数过多、消息内容格式错误等情况,通过线上预警机制通知运维人员。