还重构?就这代码只能铲了重写!
Java后端技术
共 9740字,需浏览 20分钟
·
2022-01-21 07:25
往期热门文章:
1、是怎么样的SQL优化能做到 900W+数据,从17s到300ms? 2、再见 BeanUtils!对比 12 种 Bean 自动映射工具,就它性能最拉跨! 3、暴力拒绝白嫖,著名开源项目作者删库跑路,数千个应用程序无限输出乱码 4、两天两夜,1M图片优化到100kb! 5、12 个顶级 Bug 跟踪工具
一、前言
我们不一样,就你没对象!
对,你是面向过程编程的!业务价值
!可维护
、易扩展
、好交接
的特点。二、代码优化
1. 约定规范
# 提交:主要 type
feat: 增加新功能
fix: 修复bug
# 提交:特殊 type
docs: 只改动了文档相关的内容
style: 不影响代码含义的改动,例如去掉空格、改变缩进、增删分号
build: 构造工具的或者外部依赖的改动,例如webpack,npm
refactor: 代码重构时使用
revert: 执行git revert打印的message
# 提交:暂不使用type
test: 添加测试或者修改现有测试
perf: 提高性能的改动
ci: 与CI(持续集成服务)有关的改动
chore: 不修改src或者test的其余修改,例如构建过程或辅助工具的变动
# 注释:类注释配置
/**
* @description:
* @author: ${USER}
* @date: ${DATE}
*/
分支:开发前提前约定好拉分支的规范,比如 日期_用户_用途
,210905_xfg_updateRuleLogic提交: 作者,type: desc
如:小傅哥,fix:更新规则逻辑问题
参考Commit message 规范注释:包括类注释、方法注释、属性注释,在 IDEA 中可以设置类注释的头信息 Editor -> File and Code Templates -> File Header
推荐下载安装 IDEA P3C 插件Alibaba Java Coding Guidelines
,统一标准化编码方式。
2. 接口标准
Code码
和Info描述
,否则使用方很难知道这个接口是否调用成功还是异常,以及是什么情况的异常。public class Result implements java.io.Serializable {
private static final long serialVersionUID = 752386055478765987L;
/** 返回结果码 */
private String code;
/** 返回结果信息 */
private String info;
public Result() {
}
public Result(String code, String info) {
this.code = code;
this.info = info;
}
public static Result buildSuccessResult() {
Result result = new Result();
result.setCode(Constants.ResponseCode.SUCCESS.getCode());
result.setInfo(Constants.ResponseCode.SUCCESS.getInfo());
return result;
}
// ...get/set
}
public class RuleResult extends Result {
private String ruleId;
private String ruleDesc;
public RuleResult(String code, String info) {
super(code, info);
}
// ...get/set
}
// 使用
public RuleResult execRule(DecisionMatter request) {
return new RuleResult(Constants.ResponseCode.SUCCESS.getCode(), Constants.ResponseCode.SUCCESS.getInfo());
}
public class ResultData<T> implements Serializable {
private Result result;
private T data;
public ResultData(Result result, T data) {
this.result = result;
this.data = data;
}
// ...get/set
}
// 使用
public ResultDataexecRule(DecisionMatter request) {
return new ResultData(Result.buildSuccessResult(), new Rule());
}
两种接口返回结果的包装定义,都可以规范返回结果。在这样的方式包装后,使用方就可以用统一的方式来判断 Code码
并做出相应的处理。
3. 库表设计
0NF
第零范式是指没有使用任何范式,数据存放冗余大量表字段,而且这样的表结构非常难以维护。
1NF
第一范式是在第零范式冗余字段上的改进,把重复字段抽离出来,设计成一个冗余数据较少便于存储和读取的表结构。 同时在第一范式中也指出,表中的所有字段都应该是原子的、不可再分割的,例如:你不能把公司雇员表的部门名称和职责存放到一个字段。需要确保每列保持原子性
2NF
满足1NF后,要求表中的列,都必须依赖主键,确保每个列都和主键列之间联系,而不能间接联系,也就是一个表只能描述一件事情。需要确保表中的每列都和主键相关。
3NF
不能存在依赖关系,学号、姓名,到院系,院系到宿舍,需要确保每列都和主键列直接相关,而不是间接相关。
反三范式
有时候为了便于查询,会在如订单表冗余上当时用户的快照信息,比如用户下单时候的一些设置信息。 单列列表数据汇总到总表中一个数量值,便于查询的时候可以避免列表汇总操作。 可以在设计表的时候冗余一些字段,避免因业务发展情况多变,考虑不周导致该表繁琐的问题。
4. 算法逻辑
背景:这个一个商品活动秒杀的实现方案,最开始的设计是基于一个活动号ID进行锁定,秒杀时锁定这个ID,用户购买完后就进行释放。但在大量用户抢购时,出现了秒杀分布式 独占锁
后的业务逻辑处理中发生异常,释放锁失败。导致所有的用户都不能再拿到锁,也就造成了有商品但不能下单的问题。优化:优化独占竞态为分段静态,将活动ID+库存编号作为动态锁标识。当前秒杀的用户如果发生锁失败那么后面的用户可以继续秒杀不受影响。而失败的锁会有worker进行补偿恢复,那么最终会避免超卖以及不能售卖。
@Test
public void test_idx_hashMap() {
Mapmap = new HashMap<>(64);
map.put("alderney", "未实现服务");
map.put("luminance", "未实现服务");
map.put("chorology", "未实现服务");
map.put("carline", "未实现服务");
map.put("fluorosis", "未实现服务");
map.put("angora", "未实现服务");
map.put("insititious", "未实现服务");
map.put("insincere", "已实现服务");
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
map.get("insincere");
}
System.out.println("耗时(initialCapacity):" + (System.currentTimeMillis() - startTime));
}
背景:HashMap 数据获取时间复杂度在 O(1) -> O(logn) -> O(n),但经过特殊操作,可以把这个时间复杂度,拉到O(n) 操作:这是一个定义 HashMap
存放业务实现key,通过key调用服务的功能。但这里的key
,只有insincere
有用,其他的都是未实现服务。那你看到有啥问题了吗?这点代码乍一看没什么问题,看明白了就是代码里下砒霜!它的目的就一个,要让所有的key成一个链表放到HashMap中,而且把有用的key放到链表的最后,增加get时的耗时! 首先, new HashMap<>(64);
为啥默认初始化64个长度?因为默认长度是8,插入元素时,当链表长度为8时候会进行扩容和链表树化判断,此时就会把原有的key散列了,不能让所有key构成一个时间复杂度较高的链表。其次,所有的 key
都是刻意选出来的,因为他们在HashMap
计算下标时,下标值都为0,idx =(size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16))
,这样就能让所有key
都散列到同一个位置进行碰撞。而且单词insincere
的意思是;不诚恳的、不真诚的
!最后,前7个key其实都是废 key
,不起任何作用,只有最后一个 key 有服务。那么这样就可以在HashMap中建出来很多这样耗时的碰撞链表,当然要满足0.75
的负载因子,不要让HashMap扩容。
5. 职责分离
public interface IRuleExec {
void doRuleExec(String req);
}
public class RuleConfig {
protected MapconfigGroup = new ConcurrentHashMap<>();
static {
// ...
}
}
public class RuleDataSupport extends RuleConfig{
protected String queryRuleConfig(String ruleId){
return "xxx";
}
}
public abstract class AbstractRuleBase extends RuleDataSupport implements IRuleExec{
@Override
public void doRuleExec(String req) {
// 1. 查询配置
String ruleConfig = super.queryRuleConfig("10001");
// 2. 校验信息
checkRuleConfig(ruleConfig);
// 3. 执行规则{含业务逻辑,交给业务自己处理}
this.doLogic(configGroup.get(ruleConfig));
}
/**
* 执行规则{含业务逻辑,交给业务自己处理}
*/
protected abstract void doLogic(String req);
private void checkRuleConfig(String ruleConfig) {
// ... 校验配置
}
}
public class RuleExec extends AbstractRuleBase {
@Override
protected void doLogic(String req) {
// 封装自身业务逻辑
}
}
这是一种模版模式结构的定义,使用到了接口实现、抽象类继承,同时可以看到在 AbstractRuleBase
抽象类中,是负责完成整个逻辑调用的定义,并且这个抽象类把一些通用的配置和数据使用单独隔离出去,而公用的简单方法放到自身实现,最后是关于抽象方法的定义和调用,而业务类RuleExec
就可以按需实现自己的逻辑功能了。
6. 逻辑缜密
背景:这个产品功能的背景可能很大一部分研发都参与开发过,简单说就是满足用户使用积分抽奖的一个需求。上图左侧就是研发最开始设计的流程,通过RPC接口扣减用户积分,扣减成功后进行抽奖。但由于当天RPC服务不稳定,造成RPC实际调用成功,但返回超时失败。而调用RPC接口的uuid是每次自动生成的,不具备调用幂等性。所以造成了用户积分多支付现象。 处理:事故后修改抽奖流程,先生成待抽奖的抽奖单,由抽奖单ID调用RPC接口,保证接口幂等性。在RPC接口失败时由定时任务补偿的方式执行抽奖。流程整改后发现,补偿任务每周发生1~3次,那么也就是证明了RPC接口确实有可用率问题,同时也说明很久之前就有流程问题,但由于用户客诉较少,所以没有反馈。
7. 领域聚合
依靠领域驱动设计的设计思想,通过事件风暴建立领域模型,合理划分领域逻辑和物理边界,建立领域对象及服务矩阵和服务架构图,定义符合DDD分层架构思想的代码结构模型,保证业务模型与代码模型的一致性。通过上述设计思想、方法和过程,指导团队按照DDD设计思想完成微服务设计和开发。 拒绝泥球小单体、拒绝污染功能与服务、拒绝一加功能排期一个月 架构出高可用极易符合互联网高速迭代的应用服务 物料化、组装化、可编排的服务,提高人效
8. 服务分层
这是一个简化的分层逻辑结构,有聚合的领域、SDK组件、中间件和代码编排,并提供一些通用共性凝练出的服务治理功能。通过这样的分层和各个层级的实现方式,就可以更加灵活的承接需求了。
9. 并发优化
所以通常情况下更需要做去集中化处理,使用MQ消除峰,降低耦合,让数据可以最终一致性,也更要考虑在 Redis 下的使用,减少对数据库的大量锁处理。 合理的运用MQ、RPC、分布式任务、Redis、分库分表以及分布式事务只有这样的操作你才可能让自己的程序代码可以支撑起更大的业务体量。
10. 源码能力
@Around("aopPoint() && @annotation(dbRouter)")
public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable {
String dbKey = dbRouter.key();
if (StringUtils.isBlank(dbKey)) throw new RuntimeException("annotation DBRouter key is null!");
// 计算路由
String dbKeyAttr = getAttrValue(dbKey, jp.getArgs());
int size = dbRouterConfig.getDbCount() * dbRouterConfig.getTbCount();
// 扰动函数
int idx = (size - 1) & (dbKeyAttr.hashCode() ^ (dbKeyAttr.hashCode() >>> 16));
// 库表索引
int dbIdx = idx / dbRouterConfig.getTbCount() + 1;
int tbIdx = idx - dbRouterConfig.getTbCount() * (dbIdx - 1);
// 设置到 ThreadLocal
DBContextHolder.setDBKey(String.format("%02d", dbIdx));
DBContextHolder.setTBKey(String.format("%02d", tbIdx));
logger.info("数据库路由 method:{} dbIdx:{} tbIdx:{}", getMethod(jp).getName(), dbIdx, tbIdx);
// 返回结果
try {
return jp.proceed();
} finally {
DBContextHolder.clearDBKey();
DBContextHolder.clearTBKey();
}
}
这是 HashMap 哈希桶数组 + 链表 + 红黑树的数据结构,通过扰动函数 (size - 1) & (key.hashCode() ^ (key.hashCode() >>> 16));
解决数据碰撞严重的问题。但其实这样的散列算法、寻址方式都可以运用到数据库路由的设计实现中,还有整个数组+链表的方式其实库+表的方式也有类似之处。 数据库路由简化的核心逻辑实现代码如上,首先我们提取了库表乘积的数量,把它当成 HashMap 一样的长度进行使用。 当 idx 计算完总长度上的一个索引位置后,还需要把这个位置折算到库表中,看看总体长度的索引因为落到哪个库哪个表。 最后是把这个计算的索引信息存放到 ThreadLocal 中,用于传递在方法调用过程中可以提取到索引信息。
三、总结
讲道理,你几乎不太可能把一堆已经烂的不行的代码,通过重构的方式把他处理干净。细了说,你要改变代码结构分层、属性对象整合、调用逻辑封装,但任何一步的操作都可能会对原有的接口定义和调用造成风险影响,而且外部现有调用你的接口还需要随着你的改动而升级,可能你会想着在包装一层,但这一层包装仍需要较大的时间成本和几乎没有价值的适配。 所以我们在实际开发中,如果能让这些代码具有重构的可能,几乎就是要实时重构,每当你在添加新的功能、新的逻辑、修复异常时,就要考虑是否可以通过代码结构、实现方式、设计模式等手段的使用,改变不合理的功能实现。每一次,一点的优化和改变,也不会有那么难。 当你在接需求的时候,认真思考承接这样的业务诉求,都需要建设怎样的数据结构、算法逻辑、设计模式、领域聚合、服务编排、系统架构等,才能更合理的搭建出良好的具有易维护、可扩展的系统服务。如果你对这些还没有什么感觉,可以阅读 设计模式 和 手写Spring,这些内容可以帮助你提升不少的编程逻辑设计。
往期热门文章:
1、《历史文章分类导读列表!精选优秀博文都在这里了!》 2、程序员裸辞全职接单一个月的感触 3、Java8 Stream:2万字20个实例,玩转集合的筛选、归约、分组、聚合 4、字节终面:两个文件的公共URL怎么找? 5、留在一线,逃离一线?我从上海举家回成都的生活经历告诉你 6、公司规定所有接口都用 POST请求,这是为什么? 7、我被这个浏览了 746000 次的问题惊住了! 8、腾讯三面:40亿个QQ号码如何去重? 9、自从用完Gradle后,有点嫌弃Maven了!速度贼快! 10、一个员工的离职成本有多恐怖!
评论