事务一致性
首先,我们来回顾一下ACID原则:
- Atomicity:原子性,改变数据状态要么是一起完成,要么一起失败
- Consistency:一致性,数据的状态是完整一致的
- Isolation:隔离线,即使有并发事务,互相之间也不影响
- Durability:持久性, 一旦事务提交,不可撤销
在单体应用中,我们可以利用关系型数据库的特性去完成事务一致性,但是一旦应用往微服务发展,根据业务拆分成不用的模块,而且每个模块的数据库已经分离开了,这时候,我们要面对的就是分布式事务了,需要自己在代码里头完成ACID了。比较流行的解决方案有:两阶段提交、补偿机制、本地消息表(利用本地事务和MQ)、MQ的事务消息(RocketMQ)。
大家可以到此篇文章去了解一下:分布式事务的四种解决方案
CAP定理
1998年,加州大学的计算机科学家 Eric Brewer 提出,分布式系统有三个指标。
- Consistency:一致性
- Availability:可用性
- Partition tolerance:分区容错
Eric Brewer 说,这三个指标不可能同时做到。这个结论就叫做 CAP 定理。
微服务中,不同模块之间使用的数据库是不同的,不同模块之间部署的服务去也有可能是不用的,那么分区容错是无法避免的,因为服务之间的调用不能保证百分百的没问题,所以系统设计必须考虑这种情况。因此,我们可以认为CAP的P总是成立的,剩下的C和A无法同时做到。
实际上根据分布式系统中CAP原则,当P(分区容忍)发生的时候,强行追求C(一致性),会导致(A)可用性、吞吐量下降,此时我们一般用最终一致性来保证我们系统的AP能力。当然不是放弃C,而是放弃强一致性,而且在一般情况下CAP都能保证,只是在发生分区容错的情况下,我们可以通过最终一致性来保证数据一致。
事件驱动实现最终一致性
事件驱动架构在领域对象之间通过异步的消息来同步状态,有些消息也可以同时发布给多个服务,在消息引起了一个服务的同步后可能会引起另外消息,事件会扩散开。严格意义上的事件驱动是没有同步调用的。
例子:
在电商里面,用户下单必须根据库存来确定订单是否成交。
项目架构:SpringBoot2+Mybatis+tk-Mybatis+ActiveMQ【因为小例子,不做成Spring Cloud架构】
首先,我们来看看正常的服务之间调用:
代码:
@Override
@Transactional(rollbackFor = Exception.class)
public Result placeOrder(OrderQuery query) {
Result result = new Result();
// 先远程调用Stock-Service去减少库存
RestTemplate restTemplate = new RestTemplate();
//请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
//封装成一个请求对象
HttpEntity entity = new HttpEntity(query, headers);
// 同步调用库存服务的接口
Result stockResult = restTemplate.postForObject("http://127.0.0.1:8081/stock/reduceStock",entity,Result.class);
if (stockResult.getCode() == Result.ResultConstants.SUCCESS){
Order order = new Order();
BeanUtils.copyProperties(query,order);
order.setOrderStatus(1);
Integer insertCount = orderMapper.insertSelective(order);
if (insertCount == 1){
result.setMsg("下单成功");
}else {
result.setMsg("下单失败");
}
}else {
result.setCode(Result.ResultConstants.FAIL);
result.setMsg("下单失败:"+stockResult.getMsg());
}
return result;
}
我们可以看到,这样的服务调用的弊端多多:
1、订单服务需同步等待库存服务的返回结果,接口结果返回延误。
2、订单服务直接依赖于库存服务,只要库存服务崩了,订单服务不能再正常运行。
3、订单服务需考虑并发问题,库存最后可能为负。
下面开始利用事件驱动实现最终一致性
1、在订单服务新增订单后,订单的状态是“已开启”,然后发布一个Order Created事件到消息队列上
代码:
@Transactional(rollbackFor = Exception.class)
public Result placeOrderByMQ(OrderQuery query) {
Result result = new Result();
// 先创建订单,状态为下单0
Order order = new Order();
BeanUtils.copyProperties(query,order);
order.setOrderStatus(0);
Integer insertCount = orderMapper.insertSelective(order);
if (insertCount == 1){
// 发送 订单消息
MqOrderMsg mqOrderMsg = new MqOrderMsg();
mqOrderMsg.setId(order.getId());
mqOrderMsg.setGoodCount(query.getGoodCount());
mqOrderMsg.setGoodName(query.getGoodName());
mqOrderMsg.setStockId(query.getStockId());
jmsProducer.sendOrderCreatedMsg(mqOrderMsg);
// 此时的订单只是开启状态
result.setMsg("下单成功");
}
return result;
}
2、库存服务在监听到消息队列OrderCreated中的消息,将库存表中商品的库存减去下单数量,然后再发送一个Stock Locked事件给消息队列。
代码:
/**
* 接收下单消息
* @param message 接收到的消息
* @param session 上下文
*/
@JmsListener(destination = ORDER_CREATE,containerFactory = "myListenerContainerFactory")
@Transactional(rollbackFor = Exception.class)
public void receiveOrderCreatedMsg(Me