体系中的事务反常

体系中的事务反常

体系中的事务反常

建立体系结构时,关于反常,咱们一般要考虑这样几件工作。

体系中有哪些反常

这个问题其实很简略:一类是事务反常,例如“用户输入的证件号不合法”、“银行卡四要素鉴权失利”、“余额缺乏”等事务逻辑上的问题;除此之外的全都是体系反常,例如网络超时、数据库锁超时、乃至仓库溢出内存溢出等等。

事务反常中,有几种特其他反常。当咱们是经过相似达观锁的办法来检测幂等时,在流程中任何一点上都有或许发现当时数据、当时事务现已履行过一遍了。这时体系不能按正常逻辑持续处理,有必要中止并回滚此前的操作;一起需求接口上给调用方回来一个成功的成果。此刻,就需求一个专门的反常来处理这种问题。

此外,尽管绝大多数状况下,发作反常就应当回滚事务,但偶然也有破例:某些反常不需求回滚事务。这种反常也需求有额定的符号和处理。

体系反常一般没什么其他办法,除了重试便是抛出。可是事务反常,值得咱们考虑考虑。

<hr/>

事务反常有什么特色

在事务体系中、由咱们手动抛出的反常,是一类怎样的反常呢?

问题常常能且只能用户处理

想象一下,用户输入的证件号不合法,除了提示用户从头输入一遍,还有什么办法?四要素鉴权失利,除了提示用户从头输入一遍,还有什么办法?余额缺乏了,除了让用户去充值,还有什么办法?没办法,你只能交给用户去处理。

而且许多时分,这类问题能够交给用户去处理。

证件号错了,用户能够从头输入一个正确的;四要素鉴权失利,用户能够从头输入一遍;余额缺乏了,用户能够充值。事务反常的问题是能够交给用户来处理的——这也是最省事儿的办法。

当然,还有一种办法是给开发提bug或工单,要求开发查一下……可是这样做了的话,一般会把开发的仇视值拉满:“都现已有那么清晰的提示了,还要咱们查什么”。

体系常常只要一种处理流程

许多事务功用中,体系都只要一种处理流程,没有代替流程。因而,这个流程呈现问题后,咱们就只能中止体系流程、反应给用户去处理。

明显,假如体系有可代替计划的话,那么,主流程发作问题时,咱们就能够测验“康复上下文”并运用代替流程再处理一遍。假如一切的代替流程都失利了,终究再交给用户处理。

例如, 把String解析为Date时,咱们能够先依照"yyyy-MM-dd"解析一遍;假如解析失利再测验按"yyyyMMdd"解析一遍。又如,假如事务答应,那么用户四要素鉴权失利时,能够测验一次三要素鉴权;假如三要素还失利,再测验一次二要素鉴权。还有,体系做逾期还款时,有一个“主动减免三天罚息”的逻辑:假如钱不行还了,先测验减免一天,还不行就再减免一天,还不行就再减免一天……直到够还了、或许现已把罚息违约金减免完了、或许现已减免了三天的钱了。这些都是有代替流程的状况,就不能简略地把反常丢给用户处理。

<hr/>

怎样界说事务反常类

一般,咱们需求声明一个特定的反常类,用来符号当时发作的问题是“事务逻辑发作问题”,而不是“体系发作毛病”。例如,假如是网络毛病,或许会抛出IOException,假如是数据库问题,或许会是SQLException,这种便是“体系发作毛病”。可是,假如是“用户输入的证件号不合法”、“银行卡四要素鉴权失利”、“余额缺乏”等事务逻辑上的问题,许多时分咱们都运用事务反常直接向上抛出。

命名

所谓编程,95%的状况下都是在命名。事务反常也相同。尽管大多数状况下事务反常都会命名为BizException、BusinessException或许ServiceException之类的姓名,可是,我更主张用体系姓名来命名事务反常:MySystemException、HerSystemException等。

假如你感受过一个体系里有四五个ServiceException类,你就知道我为什么主张这样命名了。我曾经在体系A中抛出了一个体系B中声明的ServiceException,成果是明显的:体系A中的阻拦器没有对体系B的ServiceException做处理,我抛出的那个反常就直接被作为500回来给客户端了。

Checked or Runtime

事务反常是运用受检反常仍是运行时反常?时至今日,这个问题现已没有太多的疑问了:应该运用运行时反常。

假如运用受检反常,那么抛出反常的代码就有必要在办法名上声明throws。可是,体系对绝大多数事务反常都束手无策,对办法上声明的throws也别无挑选,只要把它沿着调用链逐层向上传递。这种做法除了引进冗余的代码和编译问题外,对体系没有什么实质性的协助。

运用运行时反常,则能够让体系代码对底层反常无感知——既不需求在办法名上声明throws,也不需求逐层把反常声明向上传递。这样对大多数代码和程序员都更友爱。

error code

事务反常里带上过错码,其实是被接口回来值绑定的。接口回来值要给前端一个过错码,用以差异后端发作了什么过错、前端怎么处理。当发作反常时,为了确保接口回来值的一致性,就要把反常转换成对应的过错码。体系反常一般会一致转换为一个过错码;事务反常不能这样一刀切,就只好自己带上过错码了。假如一切接口都不需求回来过错码,而是直接跳转页面,那么事务反常里其实能够不回来过错码。

我所见的大多数状况下,事务反常里的过错码都是一串数字。0000代表成功、1000代表入参不合法、2000代表四要素鉴权失利、3000代表余额缺乏,等等。

用数字做过错码有两个优点。

其一是比较安全。咱们并不知道拜访体系的究竟是用户仍是***,因而不能让他知道究竟出了什么错。例如用户登录不上体系时,不能奉告对方究竟是用户名过错仍是暗码过错,避免被爆炸出用户信息。此刻,假如体系回来一个1001过错码,请问究竟是用户名错了仍是暗码错了?不知道,总归你从头输入就对了。

其二是能够便利地做归并处理。这一点上HttpStatusCode便是一个非常好的比如。咱们都知道,HttpStatusCode中,2xx是正常回来,3xx是资源方位改变,4xx是拜访不到服务,5xx是服务内部反常。这样,咱们就能够用if(code >=200 && code <300)这样的代码来把这一类问题一致处理了。

当然,有优点就有害处。最直接的害处便是过错码一多,开发自己也记不清楚哪个过错码代表什么问题了。随之导致的“次生灾祸”便是新增反常时,过错码很容易发作重复。对trouble shooting、接口对接来说,这都是不大但也不小的问题。

除了数字之外,也有些体系直接用能够表意的字符串来做过错码:“SUCCESS”天然是成功,“ILLEGAL_PARAM”表明参数不合法,“FOUR_ITMES_ERROR”表明四要素鉴权失利、“BALANCE_NOT_ENOUGH”表明余额缺乏,诸如此类。

这种办法当然不如运用数字那么安全,可是愈加一望而知,接口对接和查问题时愈加便利快捷——安全和快捷在许多场景下都是这样对立的。而且,这类过错码呈现重复的或许性比纯数字的更小。假如体系仅仅供给中后台服务而不需求直接和前端用户交互,那么这种能够表意的过错码是更好的挑选。

error message

毋庸置疑,事务反常里应该带上过错信息:无论是给用户看仍是给开发看,咱们都需求一个清晰的信息。

不过,给用户看的和给开发看的仍是有区其他。

给用户看的要点在于描绘怎样处理这个问题:四要素鉴权失利了需求去联络银行承认银行卡预留手机号;余额缺乏了需求点击某个链接去充值等等。

给开发看的要点在于描绘问题是什么:是唯一键抵触、仍是锁超时、或许爽性便是死锁了?是网络超时了、仍是404了?

一般来说,给用户看的案牍应该由产品或许交互来定;可是产品对体系中的“反常”了解得并不多。因而,绝大多数的事务反常的案牍都是开发自己定的。这就使得许多时分用户面临体系提示会一脸茫然:这是发作了什么问题?除了打客服电话之外,我还能怎么办?所以有时分我会觉得,反常信息和用户提示,不能混为一谈。给用户的提示信息应该由接口、乃至前端来处理;而反常中的信息只供给给开发trouble shooting用。当然,要这样做的话,需求支付很大的编码本钱,未必合算。

data

反常里要不要带上发作问题时的上下文数据?这取决于捕获这个反常之后要做怎样的处理。假如需求运用上下文数据,往往就只能在反常里把它带出来了。

例如,咱们有一个校验,要求在阻拦住用户之后,把用户类型(新用户、老用户、黑名单用户)发送给前端,前端据此将用户引导到不同的产品页面上。这时分,咱们就运用了一个“带数据的反常”来处理这种状况:

public class ApplyController{
public Result<Apply> apply(long userId){
Result<Apply> result = new Result<>();
try{
Apply apply = applyService.apply(userId);
result.setCode(SUCCESS_CODE);
result.setCode(SUCCESS_MSG);
result.setData(apply);
}catch(UserTypeException ute){
result.setCode(ute.getCode());
result.setMsg(ute.getMsg());
// 在这儿处理了用户类型校验反常带出来的数据
Apply apply = new Apply();
apply.setUserType(ute.getUserType());
result.setData(apply);
}
// 其它反常交给AOP一致处理
return result;
}
}

是否需求运用子类

绝大多数状况下,咱们自界说的事务反常都只需求一个类就行:

public class MySystemException extends RuntimeException{
private final String code;
private final String msg;
public MySystemException(String code, String msg){
this.code = code;
this.msg = msg;
}
// getters,略
}

可是,就如UserTypeException所展现的那样:有些时分仅仅运用MySystemException 满意不了需求,咱们还需求对它进行扩展,经过一个子类来传递更多信息。

public class UserTypeExceptin extends MySystemException {
private final UserType userType;
public UserTypeException(Usertype userType){
super("2002","抱愧,您不能请求这款产品。");
this.userType = userType;
}
// getters
}

运用标准反常仍是自界说反常

标准反常和自界说反常的首要差异在于:一切运用Java的人都应该了解Java的标准反常;但并不是一切人都了解你的自界说反常。

假如你的反常只在自己体系的内部运用,那么主张运用自界说反常。自界说反常比标准反常更“定制化”,在处理各种问题时也愈加灵敏便利。而且,开发和保护一个体系的人有义务去了解这个体系内部的一些约好、标准和标准,自界说反常天然也在此列。

可是,当你的反常需求供给给其他人运用的时分——例如你要给人供给一个jar包,而jar包中不得不抛出一些反常的时分,这时就应当尽量运用标准反常,因为调用方并没有义务了解你的内部完成细节。

这就像你在自己家园被狗咬了,你能够用家园话喊“起开起开”;可是假如你在外国被狗咬了,你就得喊“help”,乃至或许要喊“SOS”、“May Day”,即便在日本、法国这种非英语国家,这些呼救音讯也是通用的。

总归,所谓“自界说”反常,是在你的“自界说”规模内的标准反常。假如是他人走进你的“自界说”规模,那么能够要求他人来遵从你的标准。可是假如要走出这个规模,那么你的“自界说”就不能作为标准了。

<hr/>

怎样处理事务反常

作为技能,知道“是什么”和“为什么”之后,咱们还需求知道“怎么做”。

只在必要的当地运用反常

信任一切介绍反常的文章中都会说到这一点,只不过表述办法会有不同:“运用if条件判别代替反常”、“只在真实呈现问题的时分才运用反常”、“不要用反常来操控事务流程”,诸如此类。

所谓运用反常的“必要的当地”,个人了解,需求满意以下两个条件

每一段代码块都有其前置条件和后置条件。前置条件是对输入数据的束缚,而后置条件则是对输出条件的束缚。运用反常的第一个条件,便是代码流程中的数据违背了这两个条件中的恣意一项。例如,参数校验不经过是违背前置条件的一种常见景象,而调用某个服务接口得到一个超时反常则是一种违背后置条件的景象。在这些场景下,咱们都能够考虑经过抛出反常来反应问题。

运用反常的第二个条件,便是自己处理不了违背两项条件束缚的状况。假如某项参数校验失利后,咱们能够给它设置一个默认值,那就不用抛出反常了。假如调用接口超时后咱们能够重试、或许能够转为异步处理,那也不用急着抛出反常。只要一起满意了两个条件,咱们才能够说,这个当地有必要抛出一个反常。

在事务进口处一致处理反常

这是最简略、也是最常见的一种做法。无论是运用ControllerAdvice,仍是用AOP,咱们都能够一致地捕获Controller、Dubbo或gRpc接口、MQ顾客乃至守时使命中抛出的反常,并针对各进口的特性进行处理。详细的完成办法这儿就不赘述了。

当然,运用这种办法来处理反常,有必定的前提条件。

首要,要在事务进口进行处理,那么这反常有必要契合前文说到的两个特色,即“只能交给用户处理”和“没有代替流程”。天然的,这儿的“用户”也包含体系服务的调用方。不然的话,体系应当在更适宜的当地——如反常的抛出点,或许离反常最近的事务现场——进行必要的处理。

其次,一致处理有必要以一致标准为根底。假如有的接口回来Result<T>,有的接口回来Response<U>,乃至有些接口直接回来String/Long,明显咱们无法对它们做一致处理。

第三,一致处理只会针对事务反常和体系反常两个基类来做处理,而不会、也不应该针对某种特定反常进行特别处理。假如在某些特别场景下需求运用特定反常并进行特别处理,应该在那个场景内进行处理,而不应当把某个特别场景的需求放到一致处理的模块中来。

在特别场景下运用特定反常并进行特别处理

那么,都有哪些场景下需求运用特定反常呢?

从根子上来说,当咱们需求抛出反常、而且除了一致处理所需信息(如过错码、过错提示案牍等信息)之外,还需求凭借反常来传递更多信息时,咱们就有必要运用特定反常了。

比较常见的场景是某些第三方结构组件要求运用特定反常来符号某些特别流程。例如,Spring的事务管理结构就需求在@Transactional注解中符号rollbackFor和noRollbackFor两项特点,而这两项特点都只承受Class<? extends Throwable>;Spring的重试结构也需求在@Retryable注解中符号include和exclude两项特点,他们相同只承受Class<? extends Throwable>。假如咱们要运用这两个结构供给的某些功用,一般都需求声明并抛出某种特定反常。这种状况下,这些反常所传递的信息便是经过其类界说奉告对应的处理模块:“要/不要回滚事务”或“要/不要进行重试”。

前面说到过,在运用达观锁来做幂等处理时,因为达观锁常常在事务调用链的深处才会被触发,此刻咱们往往只能用反常来奉告调用方:这个事务操作现已履行过至少一次了,不要重复操作。一起,考虑到接口幂等性,第N次调用与第一次调用应该回来相同的成果,也便是“处理成功”的成果。因而,当触发达观锁时,咱们应当声明、抛出并捕获一个特定反常,并将这个反常成果转换为“处理成功”。这也是一种运用特定反常的场景,达观锁反常也是经过其类界说传递出“这个事务操作现已履行过至少一次了”这个信息。

有些体系会经过清洗接口层日志,来剖析事务中呈现的问题。而为了进行日志清洗和剖析,除了接口终究回来成果之外,有时还需求把事务调用链深处的一些数据也“带”到接口层日志中,如是调用哪个三方接口时呈现了过错、犯错时的入参和回来值别离是什么,等等。此刻,除了声明一个特定反常,恐怕也没有其他东西能够把这类信息传递出来了。

我遇到过的最特其他一个场景,是这样一个查询接口。

开始,它的功用是从数据库中查出一张用户绑定的银行卡,并判别这张卡有没有经过四要素认证。假如没有经过四要素认证,那么就抛出事务反常,以奉告用户从头绑定一张银行卡。

跟着事务扩展,另一个模块也来调用这个接口。可是新的模块要求:假如数据库中查到了卡、仅仅这张卡没有经过四要素认证,那么需求把这张卡的数据回来给它,以便利它回填到四要素认证表单中,然后供给更好的用户体会。

这么一来,同一个接口、针对同一种状况,有了两种处理逻辑。一种不需求回来数据,直接抛出事务反常;另一种则需求回来数据,还需求回来“是否经过四要素认证”这样一个信息。

明显地,我的处理办法是声明晰一个新的事务反常,在这个事务反常中带上了相关数据。这样,既兼容了原有逻辑,又满意了新的需求,轻松简略。

当然,这个问题还有其它的处理计划,无妨拿出来比照一下。

体系中的事务反常