当项目系统到了一定的成熟度,业务庞大且稳定的程度后,往往会引入一些服务治理的技术。我们希望能够更好管理我们的系统。往小了说可以小到异常码的异常治理。系统在线上生产环境出现了各种各样异常的时候,我们如果对异常进行处理呢?别人如何来理解我的系统发生的异常呢?往大了说到链路追踪和日志治理 : 定位线上业务故障、性能低、丢数据。甚至我们需要一个监控系统对系统的各项指标进行监测:硬件层面的cpu、内存。JVM层面的fullGC频率、业务接口调用频率。
系统异常治理错误码规范分析
异常处理包括系统异常发生时的捕获和处理、接口异常的规范、对异常的统一化的理解。
通常来说:线上跑的系统出现了异常,最后都应该是在对外提供的接口处提供。比方说http接口、rpc接口、mq消费、后台定时调度线程。这几种情况往往是驱动了你的系统运行。
HTTP接口拿到了一个请求,开始通过service -> dao -> mapper ->rpc -> es -> redis等一系列的手段完成这个请求的处理,完成功能。功能完成期间可能会出一些异常,之后层层上抛。异常收敛到http接口入口处,发现本次请求异常了,在这里把异常做一个规范化的封装和定义返回给你的前端。前端根据规划化的一套异常定义:理解你的异常是什么并用他的用户的语言反馈出来
RPC接口对其他系统提供了一个接口。rpc接口入口处拿到了这个异常,针对这个异常封装和定义。对异常做一个治理返回(规范化)
当然对于MQ或者后台定时调度线程也是一样的。如果执行一个功能出了问题,MQ往往会返回一个异常给broker,让他下次来重试消费。定时调度一个功能如果出错了 ,我们也要对异常规范化处理。比如记录这个异常日志并上报异常给监控系统,发送短信或邮件告警。
HTTP参数检查示例:
protected void processInternal(ProcessContext processContext) {
CreateOrderRequest createOrderRequest = processContext.get("createOrderRequest");
ParamCheckUtil.checkObjectNonNull(createOrderRequest);
// 订单ID
String orderId = createOrderRequest.getOrderId();
ParamCheckUtil.checkStringNonEmpty(orderId, OrderErrorCodeEnum.ORDER_ID_IS_NULL);
OrderInfoDO order = orderInfoDAO.getByOrderId(orderId);
ParamCheckUtil.checkObjectNull(order, OrderErrorCodeEnum.ORDER_EXISTED);
// 业务线标识
Integer businessIdentifier = createOrderRequest.getBusinessIdentifier();
ParamCheckUtil.checkObjectNonNull(businessIdentifier, OrderErrorCodeEnum.BUSINESS_IDENTIFIER_IS_NULL);
if (BusinessIdentifierEnum.getByCode(businessIdentifier) == null) {
throw new OrderBizException(OrderErrorCodeEnum.BUSINESS_IDENTIFIER_ERROR);
}
// 用户ID
String userId = createOrderRequest.getUserId();
ParamCheckUtil.checkStringNonEmpty(userId, OrderErrorCodeEnum.USER_ID_IS_NULL);
// 订单类型
Integer orderType = createOrderRequest.getOrderType();
ParamCheckUtil.checkObjectNonNull(businessIdentifier, OrderErrorCodeEnum.ORDER_TYPE_IS_NULL);
if (OrderTypeEnum.getByCode(orderType) == null) {
throw new OrderBizException(OrderErrorCodeEnum.ORDER_TYPE_ERROR);
}
// 卖家ID
String sellerId = createOrderRequest.getSellerId();
ParamCheckUtil.checkStringNonEmpty(sellerId, OrderErrorCodeEnum.SELLER_ID_IS_NULL);
// 配送类型
Integer deliveryType = createOrderRequest.getDeliveryType();
ParamCheckUtil.checkObjectNonNull(deliveryType, OrderErrorCodeEnum.USER_ADDRESS_ERROR);
if (DeliveryTypeEnum.getByCode(deliveryType) == null) {
throw new OrderBizException(OrderErrorCodeEnum.DELIVERY_TYPE_ERROR);
}
// 地址信息
String province = createOrderRequest.getProvince();
String city = createOrderRequest.getCity();
String area = createOrderRequest.getArea();
String streetAddress = createOrderRequest.getStreet();
ParamCheckUtil.checkStringNonEmpty(province, OrderErrorCodeEnum.USER_ADDRESS_ERROR);
ParamCheckUtil.checkStringNonEmpty(city, OrderErrorCodeEnum.USER_ADDRESS_ERROR);
ParamCheckUtil.checkStringNonEmpty(area, OrderErrorCodeEnum.USER_ADDRESS_ERROR);
ParamCheckUtil.checkStringNonEmpty(streetAddress, OrderErrorCodeEnum.USER_ADDRESS_ERROR);
// 区域ID
String regionId = createOrderRequest.getRegionId();
ParamCheckUtil.checkStringNonEmpty(regionId, OrderErrorCodeEnum.REGION_ID_IS_NULL);
// 经纬度
BigDecimal lon = createOrderRequest.getLon();
BigDecimal lat = createOrderRequest.getLat();
ParamCheckUtil.checkObjectNonNull(lon, OrderErrorCodeEnum.USER_LOCATION_IS_NULL);
ParamCheckUtil.checkObjectNonNull(lat, OrderErrorCodeEnum.USER_LOCATION_IS_NULL);
// 收货人信息
String receiverName = createOrderRequest.getReceiverName();
String receiverPhone = createOrderRequest.getReceiverPhone();
ParamCheckUtil.checkStringNonEmpty(receiverName, OrderErrorCodeEnum.ORDER_RECEIVER_IS_NULL);
ParamCheckUtil.checkStringNonEmpty(receiverPhone, OrderErrorCodeEnum.ORDER_RECEIVER_IS_NULL);
// 客户端设备信息
String clientIp = createOrderRequest.getClientIp();
ParamCheckUtil.checkStringNonEmpty(clientIp, OrderErrorCodeEnum.CLIENT_IP_IS_NULL);
// 商品条目信息
List<CreateOrderRequest.OrderItemRequest> orderItemRequestList = createOrderRequest.getOrderItemRequestList();
ParamCheckUtil.checkCollectionNonEmpty(orderItemRequestList, OrderErrorCodeEnum.ORDER_ITEM_IS_NULL);
for (CreateOrderRequest.OrderItemRequest orderItemRequest : orderItemRequestList) {
Integer productType = orderItemRequest.getProductType();
Integer saleQuantity = orderItemRequest.getSaleQuantity();
String skuCode = orderItemRequest.getSkuCode();
ParamCheckUtil.checkObjectNonNull(productType, OrderErrorCodeEnum.ORDER_ITEM_PARAM_ERROR);
ParamCheckUtil.checkObjectNonNull(saleQuantity, OrderErrorCodeEnum.ORDER_ITEM_PARAM_ERROR);
ParamCheckUtil.checkStringNonEmpty(skuCode, OrderErrorCodeEnum.ORDER_ITEM_PARAM_ERROR);
}
// 订单费用信息
List<CreateOrderRequest.OrderAmountRequest> orderAmountRequestList = createOrderRequest.getOrderAmountRequestList();
ParamCheckUtil.checkCollectionNonEmpty(orderAmountRequestList, OrderErrorCodeEnum.ORDER_AMOUNT_IS_NULL);
for (CreateOrderRequest.OrderAmountRequest orderAmountRequest : orderAmountRequestList) {
Integer amountType = orderAmountRequest.getAmountType();
ParamCheckUtil.checkObjectNonNull(amountType, OrderErrorCodeEnum.ORDER_AMOUNT_TYPE_IS_NULL);
if (AmountTypeEnum.getByCode(amountType) == null) {
throw new OrderBizException(OrderErrorCodeEnum.ORDER_AMOUNT_TYPE_PARAM_ERROR);
}
}
Map<Integer, Integer> orderAmountMap = orderAmountRequestList.stream()
.collect(Collectors.toMap(CreateOrderRequest.OrderAmountRequest::getAmountType,
CreateOrderRequest.OrderAmountRequest::getAmount));
// 订单支付原价不能为空
if (orderAmountMap.get(AmountTypeEnum.ORIGIN_PAY_AMOUNT.getCode()) == null) {
throw new OrderBizException(OrderErrorCodeEnum.ORDER_ORIGIN_PAY_AMOUNT_IS_NULL);
}
// 订单运费不能为空
if (orderAmountMap.get(AmountTypeEnum.SHIPPING_AMOUNT.getCode()) == null) {
throw new OrderBizException(OrderErrorCodeEnum.ORDER_SHIPPING_AMOUNT_IS_NULL);
}
// 订单实付金额不能为空
if (orderAmountMap.get(AmountTypeEnum.REAL_PAY_AMOUNT.getCode()) == null) {
throw new OrderBizException(OrderErrorCodeEnum.ORDER_REAL_PAY_AMOUNT_IS_NULL);
}
String couponId = createOrderRequest.getCouponId();
if (StringUtils.isNotEmpty(couponId)) {
// 订单优惠券抵扣金额不能为空
if (orderAmountMap.get(AmountTypeEnum.COUPON_DISCOUNT_AMOUNT.getCode()) == null) {
throw new OrderBizException(OrderErrorCodeEnum.ORDER_DISCOUNT_AMOUNT_IS_NULL);
}
}
// 订单支付信息
List<CreateOrderRequest.PaymentRequest> paymentRequestList = createOrderRequest.getPaymentRequestList();
ParamCheckUtil.checkCollectionNonEmpty(paymentRequestList, OrderErrorCodeEnum.ORDER_PAYMENT_IS_NULL);
Integer totalPayAmount = 0;
for (CreateOrderRequest.PaymentRequest paymentRequest : paymentRequestList) {
Integer payType = paymentRequest.getPayType();
Integer accountType = paymentRequest.getAccountType();
if (payType == null || PayTypeEnum.getByCode(payType) == null) {
throw new OrderBizException(OrderErrorCodeEnum.PAY_TYPE_PARAM_ERROR);
}
if (accountType == null || AccountTypeEnum.getByCode(accountType) == null) {
throw new OrderBizException(OrderErrorCodeEnum.ACCOUNT_TYPE_PARAM_ERROR);
}
Integer payAmount = paymentRequest.getPayAmount();
if (payAmount == null) {
// 支付金额不能为空
throw new OrderBizException(OrderErrorCodeEnum.PAY_AMOUNT_IS_NULL);
}
totalPayAmount += payAmount;
}
if (!totalPayAmount.equals(orderAmountMap.get(AmountTypeEnum.REAL_PAY_AMOUNT.getCode()))) {
throw new OrderBizException(OrderErrorCodeEnum.TOTAL_PAY_AMOUNT_ERROR);
}
}
RPC风控异常处理示例:
/**
* 订单风控检查
*/
@SentinelResource(value = "RiskRemote:checkOrderRisk", fallbackClass = RiskRemoteFallback.class, fallback = "checkOrderRiskFallback")
public CheckOrderRiskDTO checkOrderRisk(CheckOrderRiskRequest checkOrderRiskRequest) {
JsonResult<CheckOrderRiskDTO> jsonResult = riskApi.checkOrderRisk(checkOrderRiskRequest);
if (!jsonResult.getSuccess()) {
throw new OrderBizException(jsonResult.getErrorCode(), jsonResult.getErrorMessage());
}
return jsonResult.getData();
}
状态机统一异常处理
首先我们看一个示例:状态机统一异常处理
@Override
public void fire(Object event, Object context) {
super.fire(event, context);
Exception exception = exceptionThreadLocal.get();
if (exception != null) {
exceptionThreadLocal.remove();
if (exception instanceof BaseBizException) {
throw (BaseBizException) exception;
} else {
throw new RuntimeException(exception);
}
}
}
正常情况下状态机:调用fire(event,context) 方法,会调用onStateChange方法。假如onStateChange方法抛出业务异常,这里会被状态机接管,然后使用一个Squirrel-Foundation内部的异常TransitionException。
可以看到它对我们的业务异常进行包装。然后抛出TransitionException异常。
我们一般的情景:在SpringBoot中调用状态机开始状态流转,调用了fire方法,接着得到一个TransitionException异常,但是这显然不是我们想要的结果。我们希望onStateChange方法抛出的异常如果是业务异常BaseBizException,则fire方法抛出的也是业务异常。
所以这里采用了一种方式,在onStateChange方法中使用ThreadLocal将状态保存起来,
那么fire方法就无法检测到我们实际业务代码是否抛出了异常,此时等fire方法返回的时候,我们再判断ThreadLocal中是否有异常,
如果有就直接抛出,这样就可以实现我们所需要的结果。
异常码与消息规范设计
为了应对系统里各个地方频繁出现的一些异常,我们需要设计一套多平台甚至公司级别的异常治理。各个系统都提供一套统一的异常码以及配套异常消息提示。其实通俗来说就是code+msg抛出
以下给出一些通用异常示例:
// 正向订单业务异常枚举
/**
* 生成订单号
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 00 | 00 |
* +--------+---------+-------+
*/
ORDER_NO_TYPE_ERROR("160000", "订单号类型错误"),
/**
* 订单下单
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 01 | 00 |
* +--------+---------+-------+
*/
ORDER_EXISTED("160100", "订单已存在"),
ORDER_ITEM_IS_NULL("160108", "订单商品信息不能为空"),
TOTAL_PAY_AMOUNT_ERROR("160123", "总的支付金额错误"),
/**
* 订单预支付
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 02 | 00 |
* +--------+---------+-------+
*/
ORDER_PAY_AMOUNT_ERROR("160200", "订单支付金额错误"),
ORDER_PRE_PAY_ERROR("160201", "订单支付发生错误"),
ORDER_PRE_PAY_EXPIRE_ERROR("160202", "已经超过支付订单的截止时间"),
/**
* 订单支付回调
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 03 | 00 |
* +--------+---------+-------+
*/
ORDER_PAY_CALLBACK_ERROR("160300", "订单支付回调发生错误"),
ORDER_CANCEL_PAY_CALLBACK_PAY_TYPE_SAME_ERROR("160304", "接收到支付回调的时候订单已经取消,且支付回调为同种支付方式"),
ORDER_CANCEL_PAY_CALLBACK_PAY_TYPE_NO_SAME_ERROR("160305", "接收到支付回调的时候订单已经取消,且支付回调非同种支付方式"),
/**
* 移除订单
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 04 | 00 |
* +--------+---------+-------+
*/
ORDER_CANNOT_REMOVE("160400", "订单不允许删除"),
/**
* 调整订单配置地址
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 05 | 00 |
* +--------+---------+-------+
*/
ORDER_NOT_ALLOW_TO_ADJUST_ADDRESS("160500", "订单不允许调整配送地址"),
ORDER_DELIVERY_NOT_FOUND("160501", "订单配送记录不存在"),
ORDER_DELIVERY_ADDRESS_HAS_BEEN_ADJUSTED("160502", "订单配送地址已被调整过"),
/**
* 订单履约
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 09 | 00 |
* +--------+---------+-------+
*/
ORDER_FULFILL_ERROR("160900", "订单履约失败"),
ORDER_NOT_ALLOW_INFORM_WMS_RESULT("160901", "订单不允许通知物流配送结果"),
/**
* 取消订单
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 08 | 00 |
* +--------+---------+-------+
*/
CANCEL_ORDER_CHILD_STATUS_CHANGED("160814", "子订单状态已变更,不能取消订单"),
CANCEL_ORDER_LINKED_STATUS_CHANGED("160815", "当前订单的主单包含其他不允许取消的子订单,该笔订单取消失败"),
CANCEL_ORDER_LINKED_STATUS_CANCELED("160816", "当前订单包含已取消过的订单,不能重复取消"),
/**
* 正向通用异常
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 40 | 00 |
* +--------+---------+-------+
*/
USER_ID_IS_NULL("164000", "用户ID不能为空"),
ORDER_ID_IS_NULL("164001", "订单号不能为空"),
BUSINESS_IDENTIFIER_IS_NULL("164002", "业务线标识不能为空"),
// 逆向订单业务异常枚举
/**
* 用户发起售后申请
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 50 | 00 |
* +--------+---------+-------+
*/
AFTER_SALE_ORDER_ID_IS_NULL("165000", "发起售后的订单号不能为空"),
AFTER_SALE_ORDER_STATUS_ERROR("165007", "售后单状态错误,非已签收状态的订单不能申请售后"),
AFTER_SALE_PROCESS_RETURN_GOODS("165009", "处理售后退款重复"),
/**
* 缺品
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 51 | 00 |
* +--------+---------+-------+
*/
LACK_SKU_CODE_IS_NULL("165105", "缺品skuCode不能为空"),
LACK_NUM_IS_LT_0("165106", "缺品数量不能小于0"),
LACK_NUM_IS_GE_SKU_ORDER_ITEM_SIZE("165109", "缺品数量不能大于或等于下单商品数量"),
/**
* 撤销售后
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 52 | 00 |
* +--------+---------+-------+
*/
REVOKE_AFTER_SALE_REQUEST_IS_NULL("165200", "撤销售后入参不能为空"),
REVOKE_AFTER_SALE_ID_IS_NULL("165201", "撤销售后ID不能为空"),
REVOKE_AFTER_SALE_CANNOT_FREIGHT_OR_COUPONS("165205", "已产生运费or优惠券售后单,无法撤销"),
/**
* 查询售后列表
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 53 | 00 |
* +--------+---------+-------+
*/
AFTER_SALE_LIST_BUSINESS_IDENTIFIER_IS_NULL("165300", "查询售后列表业务线标识不能为空"),
/**
* 查询售后详情
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 54 | 00 |
* +--------+---------+-------+
*/
AFTER_SALE_DETAIL_ID_IS_NULL("165400", "查询售后详情售后ID不能为空"),
/**
* 支付退款回调
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 55 | 00 |
* +--------+---------+-------+
*/
PAY_REFUND_CALLBACK_STATUS_FAILED("165500", "支付退款回调状态错误"),
PAY_REFUND_CALLBACK_BATCH_NO_IS_NULL("165503", "支付退款回调批次号不能为空"),
PAY_REFUND_CALLBACK_AFTER_SALE_ID_IS_NULL("165509", "支付退款回调售后订单号不能为空"),
/**
* 接收售后审核结果
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 56 | 00 |
* +--------+---------+-------+
*/
AFTER_SALE_AUDIT_TYPE_IS_ERROR("165600", "优惠券售后单无需审核"),
AFTER_SALE_AUDIT_CANNOT_REPEAT("165601", "不能重复处理客服审核信息"),
AFTER_SALE_AUDIT_ITEM_CANNOT_NULL("165602", "售后商品信息不能为空"),
/**
* 查询售后支付信息
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 57 | 00 |
* +--------+---------+-------+
*/
AFTER_SALE_REFUND_INFO_IS_NULL("165700", "售后支付信息不能为空"),
/**
* 实际退款
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 58 | 00 |
* +--------+---------+-------+
*/
REFUND_MONEY_REPEAT("165800", "执行退款操作重复"),
REFUND_ORDER_AMOUNT_FAILED("165801", "调用支付退款接口失败"),
REFUND_ENUM_STATUS_IS_NULL("165802", "退款枚举状态值不能是空"),
/**
* 逆向通用异常
* +--------+---------+-------+
* | first | middle | last |
* +========+=========+=======+
* | 16 | 90 | 00 |
* +--------+---------+-------+
*/
COLLECTION_PARAM_CANNOT_BEYOND_MAX_SIZE("169001", "[{0}]大小不能超过{1}"),
SEND_MQ_FAILED("169003", "发送MQ消息失败"),
ORDER_ID_PATTERN_ERROR("169005", "订单ID格式不正确"),
;
没有异常的规范肯定是不行的,在系统调用的入口处一定要把异常通用化标准化的抛出。各个系统之间接口对接的时候都是靠统一异常码交互。
多个系统会有多个功能流程,每个流程里也会有多个异常点,所以针对每个异常点都要设计异常码与提示。那么问题来了,那么多系统那么多流程代码,异常体系该如何去设计呢?
业务异常枚举错误码code定义⽬前做如下规范:设计如上述代码示例
- code总⻓度为6
- 前两位表示微服务或者通⽤异常(公司级别定义)
- 中间两位表示功能模块
- 最后两位⽤来递增表示具体的异常。
通用异常code:
- 10:表示客户端通⽤异常
- 20:表示服务端通⽤的异常
系统未知异常 | -1 |
客户端HTTP请求⽅法错误 | 1001 |
客户端请求体JSON格式错误或字 段类型不匹配 | 1003 |
业务⽅法参数检查不通过 | 2001 |
// =========== 系统级别未知异常 =========
/**
* 系统未知错误
*/
SYSTEM_UNKNOWN_ERROR("-1", "系统未知错误"),
// =========== 客户端异常 =========
/**
* 客户端HTTP请求方法错误
* org.springframework.web.HttpRequestMethodNotSupportedException
*/
CLIENT_HTTP_METHOD_ERROR("1001", "客户端HTTP请求方法错误"),
/**
* 客户端@RequestBody请求体JSON格式错误或字段类型错误
* org.springframework.http.converter.HttpMessageNotReadableException
* <p>
* eg:
* 1、参数类型不对:{"test":"abc"},本身类型是Long
* 2、{"test":} test属性没有给值
*/
CLIENT_REQUEST_BODY_FORMAT_ERROR("1003", "客户端请求体JSON格式错误或字段类型不匹配"),
// =========== 服务端异常 =========
/**
* 通用的业务方法入参检查错误
* java.lang.IllegalArgumentException
*/
SERVER_ILLEGAL_ARGUMENT_ERROR("2001", "业务方法参数检查不通过"),
;