一开始我想着是在Feign的ErrorDecoder上做自定义的异常处理,来实现根据http code抛出各种异常。但是Feign与Hystrix结合之后,发现一个问题,只要服务调用抛出了Throwable类就会触发Hystrix的fallback(前提是配置了fallback)。想来想去都没有想到怎么利用这套机制来实现业务逻辑上的异常分支和服务器处理异常。最后,灵光一现:
我把异常分成两大类,ClientException和ServiceException。无论哪一种异常,都会导致对应服务本地事务回滚。当发生ClientException时,向客户端返回的http code为200,当发生ServiceException时,向客户端返回的http code为500。
ClientException是由客户端引起的异常,比如输入非法参数或者用户登录输入错误的密码导致登录失败而业务逻辑进入到非主线分支,这类问题其实都是客户端导致的,只需要拒绝服务让客户端提交正确的参数即可恢复。因此当遇到这种异常的时候,其实业务逻辑是执行成功的,只不过进入了非主线分支而已。
ServiceException则是由服务器上的逻辑漏洞或者其他什么原因导致的无法由客户端重新提交来纠正的错误,比如服务器代码上写的不严谨在某个地方报了空指针异常,这种问题客户端无论提交多少次正确的参数都无法修正,因此是属于服务器内部错误。
将异常分成这两类之后,处理起来就清晰很多了。现在假设有两个服务A、B,外部请求进来的时候,由网关服务转发至A,A的处理过程中需要调用B,此时可能会发生以下几种情况:
1、A调用B之前发生ClientException,以Http Code 200返回结果至外部客户端,http Body中会有固定的格式包含自定义的业务code、msg、data等。客户端解析code后提示用户msg并执行对应的逻辑来重新提交请求。通俗一点讲就是登陆的时候输入了错误的密码,请求提交后页面提示密码错误,此时重新输入正确密码,提交,登录成功。
2、A调用B之前发生ServiceException,这个没办法,服务暂时不可用了。等重新发布来修复。
3、A调用B时,B发生ClientException。此时Http Code 200返回结果至A,但是A在从http Body中提取data时,会发现业务code不等于操作成功的那个值,则A抛出对应的ClientException,让客户端重新提交正确参数。
4、A调用B时,B发生ServiceException。则A的Feign 客户端在解析的时候就会抛出FeignException异常(由于http code为500)。此时有两种选择,A捕获到FeignException,本地事务中断回滚,并告知客户端服务整体不可用,不过这样体验不好。还有一种选择,就是当A的feign客户端配置了fallback,则发生FeignException时会进入对应的fallback代码,那么在这段代码里可以向消息队列里发送这个调用请求!对应的B会监听这个调用消息,然而此时无论B调用多少次都会报错,不过没关系。紧急发布一个补丁,修复导致ServiceException的那个bug,然后部署上线。当新版本的B代码监听到这个消息时,请求就会处理成功,以达到最终一致性!
5、A调用B成功后,A本地事务提交出错,这个问题比较恶心,粗略想了一下A try catch一下然后调用B的补偿接口来使B的数据恢复正常。
最后,为了安全起见,微服务之前涉及到修改操作的接口,都要保证幂等性。最简单的一个情况就是服务调用超时,Feign会自动重新发送请求几次,那么假设某服务响应过慢,则极有可能导致请求重复提交,若没有做到幂等则会产生错误数据。另外一个就是基于消息队列来保证最终一致性,也有可能发生请求重复处理。