数据一致性-接口调用一致性

微服务接口调用常涉及到数据的一致性问题,提供几种提高接口的数据一致性思路

场景

客户端调用A服务的接口,A服务接口中又调用了B服务
如果A服务和B服务都执行成功,则成功,并且二者事务都应提交
如果A服务或B服务任意一个失败,则失败,且二者事务都不执行或回滚
因为网络请求的不可靠性,如果A调用B失败,可能原因有:

  1. B没有接收到网络请求
  2. B收到后执行失败
  3. B执行成功,请求返回时异常
  4. B调用超时,B可能执行成功也可能失败。

因此当A调用B时,可能出现数据不一致的情况。

接口几种方式

方式一:feign直接调用

A服务接口:

1
2
3
4
5
6
7
try {
业务代码A
feign调用B服务接口
事务提交
} catch (Exception e) {
事务回滚
}

B服务接口:

1
2
3
4
5
6
7
8
try {
业务代码B
事务提交
//此时接口状态码2XX
} catch (Exception e) {
事务回滚
//此时接口状态码非2XX
}

一致性分析:

  1. feign调用B服务接口成功,状态码为2XX。则B服务事务已经提交,A进行事务提交。
    若A事务提交成功,则一致; 若A事务提交失败但此时B中事务已经提交,则不一致。
  2. B没有接收到网络请求。B未被执行,feign调用抛出异常,A事务不进行提交,进入回滚,数据一致。
  3. B执行成功,请求返回时异常。B事务已经提交,feign调用B服务接口异常,A事务回滚,数据不一致。
  4. B调用超时。可能为B没有接收到网络请求,也可能B执行成功,请求返回时异常,也可能B收到请求响应缓慢。一致性状态不确定,都有可能。

方式二:基于可靠消息

服务A业务代码执行完后发送消息到消息队列,如rabbitmq,并用ack等方式确保发送成功;
B服务接收消费成功后,手动确认消息,kafka则用手动提交位移的方式。

A服务生产者:

1
2
3
4
5
6
7
8
9
10
11
try {
业务代码A
ack = rabbitmqProducer.sendAndGetAck
if !ack {
//发送失败可以设置自动重试,不重试就抛出异常
throws new RabbitmqSendException
}
事务提交
} catch (Exception e) {
//事务回滚
}

B服务消息队列消费端一:

1
2
3
4
5
6
7
8
9
10
//
try {
业务代码B
channel.basicAck手动确认//kafka则手动提交位移
事务提交
//此时接口状态码2XX
} catch (Exception e) {
事务回滚
//此时接口状态码非2XX
}

B服务消息队列消费端二:

1
2
3
4
5
6
7
8
9
10
//
try {
业务代码B
事务提交
//此时接口状态码2XX
} catch (Exception e) {
事务回滚
//此时接口状态码非2XX
}
channel.basicAck手动确认//kafka则手动提交位移

一致性分析:

  1. A服务发送消息到消息队列成功,却提交事务失败,出现数据不一致。
  2. B消费端一:手动确认后,消息从消息队列删除,B事务提交失败,出现数据不一致。
  3. B消费端二:B事务提交成功,手动确认失败,可能会重复收到该条消息,出现不一致。
    此时可在消息中添加uuid,服务B收到消息后根据uuid进行一次去重再处理等方式来实现幂等性。

方式三:预执行+确认+回查,类似TCC

A服务需要插入一个表transaction_record记录调用状态,提供给B服务回调。

A服务业务接口:

1
2
3
4
5
6
7
8
9
10
11
12
String uuid = generateUUID()//生成一个uuid
try{
feign.preCreate(uuid,...)//feign调用B预执行,比如B服务为创建订单服务,预创建一个订单,但状态为待确认
业务代码A
将uuid插入transaction_record表中
事务提交
}catch (Exception e) {
事务回滚
feign.cancel(uuid)//feign调用B取消,比如B服务为创建订单服务,设置该订单状态为取消
}

feign.confirm(uuid)//feign调用B确认,比如B服务为创建订单服务,设置该订单状态为确认,此时订单可用

A服务服务回查接口,提供给B服务回查状态:

1
2
3
get /v1/transaction/{uuid}

从transaction_record表中查询,有则返回确认,没有则返回取消

B服务需要提供预执行、确认、取消接口。若预执行后迟迟没有执行确认或取消,则B向A回查,根据结果确认或取消。

一致性分析:

  1. A中preCreate执行异常。应抛出异常,不再执行业务代码和事件表插入uuid,去执行cancel。若B执行则状态也为未确认,不影响一致性;
    若cancel也执行失败,比如此时B挂掉,B重启后应去调用服务A的回查接口,确定状态。状态一致。
  2. 业务代码A执行失败,事务回滚,feign调用B取消。若取消成功,则状态一致;若取消失败,应抛出异常,confirm不再执行。状态一致。
  3. 业务代码A执行成功,事务提交失败,执行事务回滚。若回滚失败,抛出异常,不再执行cancel和confirm,B会执行超时回查确定状态;若回滚成功,则执行取消。状态一致。
  4. A事务提交成功,确认失败(比如A执行确认时,服务A或者B刚好挂掉)。则服务B超时回查,发现uuid存在,修改状态为确认。状态一致。
  5. 预处理完成后,去执行业务代码,若业务代码执行缓慢,B服务认为超时,则服务B超时回查,若A的事务还未提交,A的回查接口返回取消,
    则B被取消,A却提交了事务,此时出现事务状态的不一致。此时可以通过设置B稍微大的超时时间来调整,可以让服务A在预处理的feign调用时传入期待的超时时间。

总结

以上是接口间调用的几种方式,这里只提供一种大概的思路,应用时可以自己优化,同步发送也可修改为异步+重试等方式。
若一致性要求可采用方式三,uuid+插入表也可采用其他的方式实现。为了提高一致性,接口调用也要尽量是幂等的,可通过业务逻辑的幂等性或uuid实现。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!