在分布式系统中,同时满足“CAP定律”中的“一致性”、“可用性”和“分区容错性”三者是不可能的。 在互联网领域的绝大多数的场景,都需要牺牲强一致性来换取系统的高可用性, 系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
提供回滚接口
在多个原子操作前加BFF层来协调调用A、B服务, 如果有些需要同步返回结果,可以按照”串行”的方式去调用。 如果调用A失败,则不会盲目去调用B。 如果调用B失败,则尝试去回滚调用A的操作。
举个例子说明: 我们的某个论坛网站,每天登录成功后会奖励用户5个积分, 但是积分和用户又是两套独立的子系统服务,对应不同的DB,这控制起来就比较麻烦了。
解决思路: 1.把登录和加积分的服务调用放在BFF层一个本地方法中。
2.当用户请求登录接口时,先执行加积分操作,加分成功后再执行登录操作 如果登录成功,那当然最好了,积分也加成功了。 如果登录失败,则调用加积分对应的回滚接口(执行减积分的操作)。
总结:这种方式缺点比较多,通常在复杂场景下是不推荐使用的,除非是非常简单的场景, 非常容易提供回滚,而且依赖的服务也非常少的情况。
这种实现方式会造成代码量庞大,耦合性高。而且非常有局限性,因为有很多的业务是无法很简单的实现回滚的, 如果串行的服务很多,回滚的成本实在太高。
本地消息表
举个经典的跨行转账的例子来描述。
第一步伪代码如下,扣款1W,通过本地事务保证了凭证消息插入到消息表中。
1
2
3
4
5
begin transaction
update a set amount=amount-10000 where userId=1;
insert into message(userId,amount,status) values(1,10000,1);
end transaction
cmmmit;
第二步,通知对方银行账户上加1W了。 通常采用两种方式: 采用时效性高的MQ,由对方订阅消息并监听,有消息时自动触发事件。 采用定时轮询扫描的方式,去检查消息表的数据。 两种方式其实各有利弊,仅仅依靠MQ,可能会出现通知失败的问题。 而过于频繁的定时轮询,效率也不是最佳的(90%是无用功)。 所以,我们一般会把两种方式结合起来使用。
通知的问题解决了,那么消息消费方如果消息重复消费,往用户账号上多加了钱。 对此我们可以在消费方,也通过一个消费状态表来记录消费状态。在执行“加款”操作之前, 检测下该消息(提供标识)是否已经消费过,消费完成后,通过本地事务控制来更新这个“消费状态表”。 这样子就避免重复消费的问题。
上诉的方式是一种非常经典的实现,基本避免了分布式事务,实现了“最终一致性”。 但是,关系型数据库的吞吐量和性能方面存在瓶颈,频繁的读写消息会给数据库造成压力。 所以,在真正的高并发场景下,该方案也会有瓶颈和限制的。
MQ(非事务消息)
先看伪代码
1
2
3
4
5
6
7
8
9
10
11
12
public void trans(){
try{
//1.操作数据库
bool result = dao.update(model);//操作数据库失败,会抛出异常
//2.如果第一步成功,则操作消息队列(投递消息)
if(result) {
mq.append(model);//如果向mq中投递消息失败,方法内部抛出异常
}
}catch (Exception e) {
rollback();//如果发生异常,回滚
}
}
根据上述代码及注释,我们来分析下可能的情况:
- 操作数据库成功,向MQ中投递消息也成功,皆大欢喜
- 操作数据库失败,不会向MQ中投递消息了
- 操作数据库成功,但是向MQ中投递消息时失败,向外抛出了异常,刚刚执行的更新数据库的操作将被回滚
- 从上面分析的几种情况来看,貌似问题都不大的。那么我们来分析下消费者端面临的问题:
消息出列后,消费者对应的业务操作要执行成功。如果业务执行失败,消息不能失效或者丢失。 需要保证消息与业务操作一致, 尽量避免消息重复消费。如果重复消费,也不能因此影响业务结果。 如何保证消息与业务操作一致,不丢失?
主流的MQ产品都具有持久化消息的功能。如果消费者宕机或者消费失败,都可以执行重试机制的 (有些MQ可以自定义重试次数)。
如何避免消息被重复消费造成的问题?
- 保证消费者调用业务的服务接口的幂等性
- 通过消费日志或者类似状态表来记录消费状态,便于判断(建议在业务上自行实现,而不依赖MQ产品提供该特性)
总结:这种方式比较常见,性能和吞吐量是优于使用关系型数据库消息表的方案。 如果MQ自身和业务都具有高可用性,理论上是可以满足大部分的业务场景的。 不过在没有充分测试的情况下,不建议在交易业务中直接使用。
MQ(事务消息)
举个例子,Bob向Smith转账,那我们到底是先发送消息,还是先执行扣款操作?
好像都可能会出问题。如果先发消息,扣款操作失败,那么Smith的账户里面会多出一笔钱。 反过来,如果先执行扣款操作,后发送消息,那有可能扣款成功了但是消息没发出去,Smith收不到钱。
RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址,
第二阶段执行本地事物,
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。
如果确认消息发送失败了怎么办?
RocketMQ会定期扫描消息集群中的事物消息,这时候发现了Prepared消息,它会向消息发送者确认,
Bob的钱到底是减了还是没减呢?如果减了是回滚还是继续发送确认消息呢?
RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。
这样就保证了消息发送与本地事务同时成功或同时失败。
最靠谱的实现方式。