引言
我想以熵增定律作为开篇语,熵增定律:在一个孤立系统里,如果没有外力做功,其总混乱度(熵)会不断增大。
套用到软件系统来说,我的理解是一个软件系统,如果不做任何优化改进,其最终一定会越来越乱,越来越重。所以要打破软件的熵增定律,就需要不断的进行优化重构,不断的做熵减。
以我对系统优化重构的经验来看,不管是对一个方法、一个类、一个模块或者一个系统,其重构的流程都可以采用以下步骤:
重构的步骤.png本篇文章准备完成的是CBS系统重构流程的第一和第二步,或者说是完成第一和第二步的部分方案内容。
归类分析问题
归类问题
首先需要的是发现并归类问题,我对最近收集和了解到的问题做了下归类,总结下来最重要的几个问题如下:
CBS重点问题归类.png以上是站在个人角度认为CBS重构最需先解决的问题,还有很多其他的问题尚未罗列。
分析问题
一般我分析问题会从两个维度来对问题进行标记,一个是优先级,另一个是解决难度。
优先级
首先看下优先级,优先级主要会考量严重程度、问题的影响的范围、客户的重要程度。而目前分行反馈问题都会说客户很重要,因此客户重要程度这个点就先略过。
从严重程度和问题影响范围来分析,我对问题做了标记,具体看上图。优先级为1的可以理解成现在普遍存在,而且可能造成客户流失、项目延期的问题。优先级为2的是有影响但是能容忍的,但是优先级为2的常常是大部分问题的根源。
解决难度
解决难度会按以下三个个级别来标记:
1.短时间内能给出方案,使用红色圆圈标记。
2.需要深入了解或者专题讨论才能给出方案,使用黄色圆圈标记;
3.问题需要很长时间才能解决,半年甚至几年,使用蓝色圆圈标记;
上述两个维度的个人详细的思考和分析过程就不描述了,在图中我把所有问题都按这两个维度进行了标记。有了标记后,我们就可以对问题的解决顺序进行梳理了,简单梳理不难得出以下结果:
问题解决顺序.png下面针对上图前分页和日志做初步的方案设计吧。
方案设计
解决现有核心问题的方案设计需要满足以下两条设计原则:
-
兼任现有代码,尽量对已有代码不侵入或少量侵入。
-
业务方使用时尽量少改动已有代码。
分页方案设计
下面我们梳理下CBS目前的数据查询流程,为了简化起见,省略了其他不相关流程。如下图所示:
分页重构-现有设计.png从图中可以看出,分页实现需要解决以下两个问题即可:
1.改造DBConnection以支持分页
2.解决因数据过滤后不足一页问题
改造DBConnection
我们先重点看下DBConnection的改造方案。
分页重构-优化方案.png如图所示,为了尽量少的修改现有代码,分页的实现主要做如下的设计修改:
1.DBConnection新增方法preparePage()、executePage(),以支持分页模式的预提交和执行,方法的输入输出与现有的保持完成一致。
2.新增PageHelper类,提供方法startPage(offset,limit)、getPageSQL(),业务Table类需要手动启动分页,同时提供组装分页SQL的方法。
3.业务Table类首先将调用的方法调整为preparePage()、executePage(),同时在调用preparePage()前启动分页。
从以上设计可以看出,新增的分页功能对原有代码完全没有修改,已有的业务不会受任何影响。同时新增方法将做兼容,也就是后续新的业务不管是否分页都可以使用新增方法来实现查询功能。
数据权限问题
造成数据权限问题主要是因为CBS数据权限的过滤方式采用的是先查后过滤,这样将导致在DB查询到的数据假设为20条,在后台过滤后将小于20条。最终用户在界面看到的一页数据可能忽多忽少。
为解决数据权限问题,个人总结后有如下两种方案:
方案一:将数据权限的过滤修改为使用SQL方式实现,也即是查询数据的同时增加数据权限相关条件。
方案二:后台适当延长分页的limit,再加上前台查询时,若当此查询数据不足一页则发起二次查询。
首先看下方案一,该方案有两个较大的问题,一是需大幅度修改原有功能;二是数据权限维度较多,可能无法在一条SQL中完成数据权限的过滤。
基于以上两个问题不满足上述的设计原则,决定不采纳方案一。
其次分析下方案二:方案二的核心是每次分页查询时适当的调整limit值,假设增加N,从而最终查询到的数据为limit+N。而这时又引出如下新的问题:
1.若数据权限过滤后返回的数据大于一页如何处理?
2.若适当调整后最终返回的数据仍不足一页如何处理?
3.N的值如何确定?
下面我们逐个来分析如何解决上述问题。
问题1、若数据权限过滤后数据大于所需一页数据时,我们只需在Filter过滤过程中增加计数器,Filter每次循环过滤是若数据为用户有权限的数据,该计数器加1。当计数器大于一页长度时则不再对后续数据进行组装返回。
问题2、若适当调整后最终返回的数据仍不足一页时,前台在收到后台数据时需自动发起二次查询,以补足一页数据。(这个问题需要解决吗?概率有多大?)
问题3、N的值如何确定,可以使用采样统计法来获得,包括以下几个过程:
- 采样:编写脚本对生产数据进行采样,采样用户为系统高频使用的20%用户。查询这些用户已分配数据权限的业务数据,计算查询第一页数据所需的偏移量值。如下样例(每页20条):
- 统计:根据上述采样结果,统计出所有业务的平均值,该值即为所需的N。若考虑精准度可以每个业务单独计算,分页查询时按不同业务采用不同的N值。
上述采用统计过程中的数据仅为暂时待定值,比如20%、平均值。采用统计的方法追求的是满足大部分。保证在大部分场景前台都不需二次查询来补充一页数据,而小部分场景的多次查询可以暂时忽略。
使用采样统计后还要做什么?
功能上线后,需要做的事情是:监控-->验证-->优化。因此我们需要在数据权限过滤时,对N的有效性做监控。也就是要记录使用limit+N后的以下数据:1.是否仍少于一页,少了多少;2.是否多余一页,多了多少。后续就可以根据记录的日志调整N值。
上述数据权限的设计已经有些复杂化了,后续希望讨论后可以简化。其实最好的解决方案应该是方案一:数据权限下沉到SQL。但是相较而言方案一的成本和风险来说,方案二又更能接受。
日志方案设计
日志的重要性不言而喻,想要问题排查快,日志完善少不了。
日志方案更多是日志的规范化,下面从以下两点进行说明:
1.制定日志输出内容规范。
2.制定日志输出埋点规范。
1.日志内容规范
目前CBS后台项目代码日志内容极其不规范,一方面,因CBS直接使用了Log4j日志框架,而Log4j不支持占位符,导致代码日志输出大量的字符串拼接。另一方面,日志内容说明格式、参数输出格式均五花八门。
在不修改已有代码暂时不升级日志框架情况下,为解决上述两个问题,可封装统一的日志输出工具类(CBSLogger),初步设计如下:
public interface CBSLogger {
/**
* info 级别日志,内容包括日志摘要说明以及参数,不允许在msg中自己拼接参数。
* <p>例子:
* logger.info("新增用户. >>> userId=[{}],userName=[{}],mobile=[{}]",userId,userName,mobile);
* </p>
*
* @param msg 日志内容
* @param params 参数
*/
void info(String msg, String... params);
/**
* debug 级别日志,内容包括日志摘要说明以及参数,不允许在msg中自己拼接参数。
* <p>例子:
* logger.debug("新增用户. >>> userId=[{}],userName=[{}],mobile=[{}]",userId,userName,mobile);
* </p>
*
* @param msg 日志内容
* @param params 参数
*/
void debug(String msg, String... params);
/**
* warn 级别日志,内容包括日志摘要说明以及参数,不允许在msg中自己拼接参数。
* <p>例子:
* logger.warn("新增用户错误. >>> userId=[{}],userName=[{}],mobile=[{}]",userId,userName,mobile);
* </p>
*
* @param msg 日志内容
* @param params 参数
*/
void warn(String msg, String... params);
/**
* error 级别日志,内容包括日志摘要说明以及参数,不允许在msg中自己拼接参数,案发现场信息和异常堆栈信息。
* <p>例子:
* logger.error("新增用户插入数据库异常. >>> userId=[{}],userName=[{}],mobile=[{}]",e,userId,userName,mobile);
* </p>
*
* @param msg 日志内容
* @param params 参数
*/
void error(String msg, Exception e, String... params);
}
2.日志输出埋点规范
ERROR
系统逻辑出错、异常等重要的错误信息,影响到程序正常运行的异常情况。
1.第三方对接的异常(包括第三方错误码)。
2.影响功能使用的异常,包括SQLException和除了业务异常之外的所有异常(RuntimeException和Exception)。
特别说明:
1.如果有Throwable信息,需要记录完成的堆栈信息:
log.error("获取用户[{}]的用户信息时出错",userName,e);
2.如果进行了抛出异常操作,请不要记录error日志,有最终方进行处理。
反例(不要这么做)
try{
....
}catch(Exception ex){
String errorMessage=String.format("Error while reading information of user [%s]",userName);
logger.error(errorMessage,ex);
throw new UserServiceException(errorMessage,ex);
}
WARN
不应该出现但不影响程序、当前请求正常运行的异常情况。
1.业务异常记录。
2.有容错机制时出现的错误情况。
INFO
系统运行信息、第三方对接信息。
1.Service(Manager)方法中涉及对系统/业务状态的变更。
2.主要逻辑中的分步骤。
3.客户端请求参数。
4.第三方对接是的调用参数和调用结果。
特别说明:
并不是所有Service(Manager)方法出入口都打日志,单一、简单的方法是没有意义的。Task除外,Task必须记录开始和结束日志。
DEBUG
1.debug信息要有意义,最好有相关参数。
2.生产环境需要关闭debug日志信息。
特别说明:
如果代码中出现以下代码,可以进行优化。
//1. 获取用户基本信息
//2. 获取用户账户信息
优化后代码:
logger.debug("开始获取用户[{}]基本信息",userId);
logger.debug("获取用户[{}]基本信息为[{}]",userId,userJson);
logger.debug("开始获取用户[{}]账户信息",userId);
logger.debug("员工[{}]账户信息为[{}]",userId,accountJson);
以上是关于分页和日志的初步优化设计方案了,直联和登录的问题需要专题讨论再输出优化方案。
章末语
最后,我想说说我对如何解决问题的理解和方法。首先我认为解决问题其实可以转化为解决问题提出者的情绪,对,就是情绪。
我觉得每个问题里面都包含了情绪,一种是现有的功能用的不爽情绪,一种是功能不足够的不满情绪。那我们怎么解决情绪问题呢,我们可以首先解决不爽情绪,然后解决不满情绪。这是因为很多不满的情绪都是基于不爽的情绪之上才出现的。
可以想象一下,如果使用者在使用功能的时候经常出现问题,那他肯定会有很多不爽的情绪。连带着对很多功能都觉得不顺眼,甚至觉得功能的开发者还没有自己聪明。从而就可能提出很多新功能需求或者优化需求。
反过来,如果使用者在使用功能的时候问题很少很流畅,那他就算是真的有很多想要的功能也会多加思索后才确定要不要提出来。
因此我们在解决问题的时候可以优先解决造成用户不爽的问题(已有功能的问题),有时会发现解决了这些问题后,很多客户的不满的问题(想要的功能)其实都可以延期或者拒绝掉。这样我们才能给自己空出更多的时间来优化和重构系统。
网友评论