上文书说明了 强一致性解决方案,这里说下最终一致性:最终一致性是 基于BASE的,BASE允许有软状态的存在,允许一段时间内的不一致性,但最终是一致的。
下面介绍几种最终一致性的解决方案。
解决方案一:基于本地消息表
基于本地消息表实现的原理是:
- 将本事务外的操作记录在消息表中。
比如说电商网站中,要进行下订单并支付的操作。这里下订单和支付是两个事务,因为支付回跳到支付宝、微信去支付,并等待一个回调;而下订单是在自己的电商系统中完成操作。 - 其他事务,提供操作接口。操作接口成功返回success,失败返回fail。
这里的操作接口在上面的例子中就是回调接口,支付宝或者微信将支付的消息通过回调接口传到你的系统当中,你的系统再去更改订单的状态。如果更改成功返回success。 - 定时任务轮询本地任务表,将未执行的消息发送给操作接口。
这里相当于微信或者支付宝的一个操作,它如果没有收到你的ack(可能网络中断,或者系统内部错误),会继续轮询你的回调接口,只不过间隔时间会越来越长,超过retry次数进行人工处理。
这种方式有什么优点呢:避免了分布式事务,实现了最终一致性。因为它把事务给拆分了,并没有同时的去执行这两个事务。
缺点在于:重试的时候需要去注意幂等性的操作。
本地消息表Demo
仍然使用前文的数据库337和338,不过我们这里多了一个本地消息表,所以,不光要使用账户表account
还要使用一个消息表,我们将消息表放入数据库338中,并且使用338的账户表account
。
创建消息表:payment_msg
在另一个数据库337中,我们创建一个订单表order
:
为了简化,这里就不加入其他属性了。
然后使用generate-mapper生成一系列必要的类和mapper,不做赘述。
下面来看支付接口PaymentService:(类比于支付宝支付接口)
@Service
public class PaymentService {
@Autowired
private Account338Mapper account338Mapper;
@Autowired
private PaymentMsgMapper paymentMsgMapper;
/**
* @return 0:成功,1:用户不存在,2:余额不足
*/
@Transactional(transactionManager = "tm337")
public int payment(int userId, int orderId, BigDecimal amount){
//支付操作
Account338 account338=account338Mapper.selectByPrimaryKey(userId);
if (account338==null)
return 1;
if (account338.getAmount().compareTo(amount)<0)
return 2;
account338.setAmount(account338.getAmount().subtract(amount));
account338Mapper.updateByPrimaryKey(account338);
//消息表中存放记录
PaymentMsg paymentMsg=new PaymentMsg();
paymentMsg.setOrderId(orderId);
paymentMsg.setStatus(0);//未发送
paymentMsg.setFailureCount(0);//重试次数
paymentMsgMapper.insertSelective(paymentMsg);
return 0;
}
}
由于两个mapper操作的是同一个数据源,所以只需要一个事务就能控制。
再来看订单接口:(类比于本系统下订单)
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
/**
* 订单回调接口
* @param orderId
* @return 0:成功,1:订单不存在
*/
public int handleOrder(int orderId){
Order order=orderMapper.selectByPrimaryKey(orderId);
if (order==null)
return 1;
order.setOrderStatus(1);//已支付
orderMapper.updateByPrimaryKey(order);
return 0;
}
}
由于我们这里只写了回调接口,没有写下订单接口,所以需要去数据库自己初始化一个订单数据:
为上面两个类创建controller,在此不做赘述。
现在写定时任务,由于需要回调handlerOrder,所以引入包:
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.11</version>
</dependency>
写一个定时任务:
@Service
public class OrderScheduler {
@Autowired
private PaymentMsgMapper paymentMsgMapper;
@Scheduled(cron = "0/10 * * * * ?")
public void Ordernotify() throws IOException {
PaymentMsgExample example=new PaymentMsgExample();
example.createCriteria().andStatusEqualTo(0);//0:未发送
List<PaymentMsg> paymentMsgs=paymentMsgMapper.selectByExample(example);
if (paymentMsgs==null||paymentMsgs.size()==0) return;
for(PaymentMsg msg:paymentMsgs){
int orderId=msg.getOrderId();
//发送回调请求
CloseableHttpClient httpClient=HttpClientBuilder.create().build();
HttpPost httpPost=new HttpPost("http://localhost:8080/handleOrder");
NameValuePair pair=new BasicNameValuePair("orderId",orderId+"");
List<NameValuePair> pairs=new ArrayList<>();
pairs.add(pair);
HttpEntity httpEntity=new UrlEncodedFormEntity(pairs);
httpPost.setEntity(httpEntity);
//处理返回值
CloseableHttpResponse response=httpClient.execute(httpPost);
String result= EntityUtils.toString(response.getEntity());
if ("success".equals(result)){
msg.setStatus(1);//发送成功
}else{
Integer failureCount = msg.getFailureCount();
msg.setFailureCount(failureCount+1);
if (failureCount>5){
//超过重试次数
msg.setStatus(2);//重试失败
}
}
paymentMsgMapper.updateByPrimaryKey(msg);
}
}
}
运行结果就不做展示了。
解决方案二:基于MQ
该方法与基于本地消息表的原理和流程基本一致。不同点在于:
- 使用MQ来替代本地消息表,存储方案不一样,数据库压力下降。
- 定时任务改为消费者,更高效,更可靠。
MQ更适合公司内的系统,而不用公司之间使用本地消息表更合适。
因为之前写过rabbitmq的文章,可以参考RabbitMQ池化方案和ThreadLocal实现RabbitMQ消息的批量发送,这里代码就不做赘述了。