外观
项目只要实现功能就行了吗
项目只要实现功能就行了吗?
前言
本文主要针对日常开发中,在项目上的一些选型考量以及遇到问题的解决思路等问题进行分析,具体围绕实战项目就以下几点进行展开:
- 在持续迭代过程中常见的问题&解决方案
- 性能问题&常见解决方案
- 并发安全性问题&解决方案
- 演进过程中会遇到的各种问题&解决方案
项目的持续迭代会带来哪些问题
一个商业级的项目的生命周期大体如下:
阶段一:立项
阶段二:业务设计
阶段三:技术设计
阶段四:模块划分
阶段五:开发
阶段六:测试
阶段七:交付上线
通常版本一的项目发布后,会持续2-7
的流程进行版本迭代,在多个版本迭代期间,开发人员会不断交替,秉持业务优选的原则,和各个开发人员的代码开发习惯不同等问题,项目代码势必会越来越臃肿,通常问题是:
- 对于框架和中间件的调用五花八门,没有统一的规范
- 技术框架的代码嵌入到业务代码中,导致后期想要更换技术框架的成本变得很高昂
- 对于中间件的引入没有统一的规范,调用方式多样,版本不一致,导致每次迭代都会有各种冲突和奇葩问题的出现
- ...
解决方案
要有底层架构设计思想的能力,和对优秀的框架底层的抽象能力,这样才能提升代码的健壮性。
注
拿思想层面举例,要先把基础框架设计并定义完备再进行业务的开发,这些基础的技术架构交给专门人员去维护。
在代码层面举例,就是抽离出一个模块,进行业务的一个抽象设计,各个api的规范,这些事先定义好后再按照各个市面成熟的框架来具体实现这些定义好的接口,业务开发的人员只用负责业务逻辑开发和抽象框架的使用。
start =>framework模块
=> 核心模块(core) => 定义一个抽象接口 (interface)=> 简单实现(abstract class)
=> 围绕核心模块的其他模块: 在核心模块的基础上,根据不同成熟框架/解决方案完成不同的继承实现各自的模块
end=>业务模块, 业务开发人员根据业务的需求随时引入某个或删除某个模块
重要
在项目开发的时,一定要有架构分离的意识,不论是任何编程都应该有这种思想。
拓展
DDD(领域驱动模型),是一个比较好的思想,落地比较难,但了解该思想很有帮助。
项目的性能问题
一般性能的指标分为以下几种:
- TPS(每秒提交的事务数量)
- QPS(每秒处理的查询数量)
- 吞吐量(每秒处理的请求数量)
TPS
提示
TPS的优化空间有限,一般追求稳定而不是效率。通常是做横向拓展,以提升硬件数量以及性能的优化方式为主。
但一般的优化方式
有以下几点:
- 剥离出非主线的耗时的下游关联业务,保证主业务数据正确落库之后立即返回,其他的下游关联业务采用消息通知的机制去异步化执行
- 有必要的话,采用多线程来来优化效率
警告
只能用于存库前的准备工作(比如一些耗时的查询操作),因为事务是不支持多线程的, 在这种场合下,如果某个线程出现异常状态,其他线程检测不到,是不会进行事务的回滚的。
- 合理利用缓存,前置的查询尽量使用缓存来优化查询效率
- 适量增加机器数量来保证服务的总体TPS
- tomcat调优和JVM调优
QPS
- 合理利用缓存,但是要注意应用的场景和失效策略等问题(比如利用mysql缓存,先查询一次数据进行预热。利用第三方缓存进行优化)
- 合理利用数据库索引来提升查询效率
- tomcat调优和JVM调优(可以参考老师的博客:https://blog.rubinchu.com/2022/01/04/jvm%e8%b0%83%e4%bc%98%e5%ae%9e%e6%88%98/)
项目的并发安全性问题
无论是哪个行业的项目,都会存在并发的问题。我们在生活中也常常会遇到,比如说:
- 一个促销活动中,商家由于商品太火爆导致超卖了!(大部分商家还是比较乐意看到这个现象的)
- 一个秒杀活动中,小明由于点击抢购点击的太猛烈,发现自己竟然抢到了好几个秒杀商品!
- 审核人员驳回了一个申请,第二天发现自动通过了!
- ...
一个优秀程序员的重要衡量标准之一,就是看他对于并发问题的处理能力。同时,并发处理又是面试中的重灾区,无论是什么级别的程序员面试,多多少少都会面临并发编程的考验。所以,掌握并时时刻刻的考虑并发性的问题,是我们的一个必备技能。
那么,我们怎么解决并发所带来的问题呢?一个字:锁。下面我们就来聊一聊业界常见的锁。
悲观锁
所谓的悲观锁,我理解的就是交警的角色。他指定同一个时间,谁能过马路,谁需要等待,大家同从指挥,有条不紊的通过十字路口。 Java中的悲观锁:
- synchronized关键字
- ReentrantLock
注
特点:同一个资源只能同时被一个线程所占有,其他线程只能排队等待。
注意
容易产生的问题:死锁。在处理并发问题的时候,稍微一不小心,就可能写一个死锁的代码出来。
提示
在写代码的时候要这样的思维:
- 我这个方法会不会有并发性的问题?
- 如果有的话,我们使用悲观锁来解决会不会有死锁的可能?
是不是所有的并发场景,我们都使用死锁来解决呢?
可以这样处理,但是有一些场景需要用到的是乐观锁,如果我们的业务场景,锁冲突极其严重,必须要使用悲观锁来处理。
乐观锁
所谓的乐观锁,一般的解释就是在执行操作的时候假设不会有并发问题,是一种乐观的态度,我总觉的这种说法其实不是很准确。
我理解的乐观锁其实是一个不断重试的过程,他就像我们两个人在一个很较窄的路上迎面走,当我先给对方让路,对方也是同样想法(这就产生了并发)的时候,总会有冲突(我往左,他往左,我往右,他往右)。但是,我们不会说就不走了,总会重试出我往走,她往右的时候。所以说,乐观锁最大的特点就是重试。
那映射到我们的业务,为了实现乐观锁,一般的解决方案是在数据表里面添加一个version字段,标识当前数据记录的版本。每次查询都会将版本信息查询出来,修改提交的时候,需要把版本信息回传回来做更新。最后执行的SQL伪代码为:
UPDATE table_name SET cloumn1 = newValue, version = version + 1 WHERE id = id AND version = version;
除了业务级别,我们的JUC包里面的好多工具类,比如Atomic开头的类,使用的CAS就是乐观锁的一种实现,它是通过不断地自旋重试来达到并发数据安全打的效果。
通过以上的例子,我们就可以了解到,乐观锁的适用场景是一些对于写操作并发量不是很大的场景。比如我们的办公OA系统的一些审核功能等等。如果我们的电商下单扣库存使用乐观锁的话,在锁碰撞极其严重的情况下,其表现就会远远低于悲观锁了。
项目的架构演变过程和问题
最近,老师在拜读一个国外大神的著作,里面的一句话对我的触动很大:不要为了可能发生的情况而过度设计。什么意思呢?意思就是,我们往往有一个误区:优秀的设计模式应该能用的时候就用、一个项目上来就应该拆分成分布式的结构.......
技术人员,很容易忽略业务的重要性。我们一定要有一个观念就是,我们的项目是因为业务的特点,所以才有的目前的架构设计。如果我们业务中只有一种类型的订单,就不要设计什么工厂类,如果我们的公司体量不大,日活也不大,就不要盲目的拆分项目为分布式的架构。总结起来就是:不要脱离业务,什么时候做什么事。
我们本小节不拿特别理论的东西来讲项目的架构演变过程之类的,特别官方的东西,那些东西随便一搜,到处都是。我们就那我们的网盘项目来做一个剖析:
比如我们是一个初创的小公司,用户很少,我们就应该弄一个简单的单体项目去运行,原因就是:我们公司业务不是很复杂,迭代很快。我们使用一个优秀的单体架构,完全可以使用事件监听机制去将业务模块解耦,使用异步机制做优化,使用本地缓存来提高查询效率,使用本地的锁机制来解决并发的数据安全问题,使用本地的事务来解决非原子性的组合操作。
随着我们公司业务的发展和用户量的增多,我们发现一台服务顶不住我们的用户访问的需求了。我们此时就应该加机器,使用NGINX做负载均衡,多加几个单体服务去支撑。到这个时候,我们原来的架构设计就会暴露出很多问题:
- 多实例部署导致本地缓存的失效有问题
- 多实例部署使用本地锁,导致并发数据安全问题
这个时候,我们就需要更换缓存的实现方式为分布式缓存,使用Redis也好,Tair也罢。锁的话,也需要更换成分布式锁,具体集成Redis或者ZK或者其他中间件。如果我们前面的架构设计的不好,使用的缓存或者是锁没有做抽象,而是具体的视线侵入到业务代码中的话,我们就需要重构业务代码了,这对于整个业务线上面的人来说,不异于于一场灾难。
随着业务的继续发展,我们发现业务越来越复杂。维护成本日益增加,我们想到打的解决方案就是做服务拆分,将原来的单体架构模式改为分布式的架构系统,通常来讲,这样做是对的。但是我们拆分之后,会发现新的问题:
- 原来的本地事务由于微服务化导致不在一个上下文中而失去其意义
- 服务拆分之后,数据库也需要做对应的拆分
这就需要我们将事务机制变更为分布式事务。数据库的拆分,就要考虑数据的平滑迁移问题......
基本上,拆分为分布式架构之后,我们的架构不会有大的变动了。
以上的一个整体流程,不代表每个项目都会遇到。之所以罗列出来,是给同学们一个印象,大家要知道项目在迭代过程中的一个整体的演变流程,以及每一个流程中,我们会遇到哪些问题,使用什么去解决。