Java后端架构开荒实战(二)——单机到集群
Java后端架构开荒实战(二)——单机到集群
一、前言
上一篇文章做了一些准备工作,这边文章正式开始写代码。
在做好单实例架构之后,升级到集群是一件很容易的事情,所以把单机和集群放在这一篇一起说。
二、单体项目架构
在开始前先说一下本文一些名词的定义吧。
组织(org):这个就是公司的意思,一个公司组织下面可能会有多个项目。
项目(project):项目在内部是要自洽的,项目和项目的调用之间就属于第三方调用了。比如本文提到的电商后端就是一个项目,组织公共类库就属于另外一个项目,每个项目有自己的生命周期。
应用(application):应用一般是一个领域服务的形式,在单体应用中可能是一个业务模块,在微服务架构中可能是一个微服务。
2.1 组织公共类库
这种二方库一般是公司组织级别的,就是封装了所有项目都可能用到的公共方法、配置和工具类等等,注意区别与项目里面的公共类库,这些类库的设计要注意通用性。
一些项目级别的专有配置和工具就不要放到这里来啦。
可以按照springboot源码那样按maven模块组织,也可以简单一点只分包吧。
贴一下web方面经常需要的配置:
统一返回结果BaseResult,一个通用的用接口层的范型返回对象是非常重要的。
public class BaseResult<T> {/*** 返回状态*/private boolean success;/*** 返回状态码*/private String code;/*** 返回信息*/private String message;/*** 返回数据*/private T data;...跨域配置,注意这里@ConditionalOnWebApplication web应用才生效。
/*** <p>* 跨域配置* </p>** @author robbendev*/ @ConditionalOnWebApplication @Configuration public class GlobalCorsConfig {@Beanpublic CorsFilter corsFilter() {//1.添加CORS配置信息CorsConfiguration config = new CorsConfiguration();//放行哪些原始域config.addAllowedOrigin("*");//是否发送Cookie信息config.setAllowCredentials(true);//放行哪些原始域(请求方式)config.addAllowedMethod("*");//放行哪些原始域(头部信息)config.addAllowedHeader("*");config.setMaxAge(3600L);//暴露哪些头部信息(因为跨域访问默认不能获取全部头部信息)config.addExposedHeader("Content-Type");config.addExposedHeader("X-Requested-With");config.addExposedHeader("accept");config.addExposedHeader("Origin");config.addExposedHeader("Access-Control-Request-Method");config.addExposedHeader("Access-Control-Request-Headers");//2.添加映射路径UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();configSource.registerCorsConfiguration("/**", config);//3.返回新的CorsFilter.return new CorsFilter(configSource);} }通用业务异常,web应用的一般在业务层抛出手动抛出,由全局异常捕获转然后转化成通用返回值返回。
/*** 通用业务异常** @author robbendev*/ @EqualsAndHashCode(callSuper = true) @Data public class BizException extends RuntimeException implements Serializable {/*** 序列化*/private static final long serialVersionUID = -4636716497382947499L;/*** 错误码*/private String code;/*** 错误信息*/private String message;/*** 错误详情*/private Object data; }备份流 (RequestBakRequestWrapper就不贴了),拦截器那里会用到。
/*** 对request请求进行包装备份请求参数** @author robbendev*/ @ConditionalOnWebApplication @Component @ServletComponentScan @WebFilter(filterName = "requestBakFilter") public class RequestBakFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest servletRequest = (HttpServletRequest) request;RequestBakRequestWrapper requestWrapper = new RequestBakRequestWrapper(servletRequest);chain.doFilter(requestWrapper, response);}@Overridepublic void destroy() {} }其他的配置各个公司的最佳实践不一而同。
2.2 项目公共类库
这种公共类库是项目级别的,每个不同的项目会有项目内部的自定义公用类库需求。
如果你需要web开发就需要springboot-web诸如此类,这些就定义在这里。
项目依赖
shop-common/pom/xml
用户登陆
用户登陆算是比较独立的模块,单拎一小节。
spring security + jwt的方案。
服务端session这种。
大家可以自行搜索一下oauth2.0和一些单点登录的方案。
shop项目的话用户登陆token签发是通过服务端session来做的。对应的服务定义在shop-common里面。贴一个token的本地缓存简单实现
@Service public class TokenServiceImpl implements TokenService {private Map<String, Token> session = new ConcurrentHashMap<>();@Overridepublic void save(Token token) {session.put(token.getToken(), token);}@Overridepublic void remove(String token) {session.remove(token);} }有兴趣的同学可以试试如何实现过期缓存。
文件服务
不贴代码了,也是属于shop-common模块的,各个云服务商都提供样板代码。 注意做成接口实现分离形式,在项目里浅封装一下。
其他
还有一些应用级别的配置类、拦截器,日志处理等等。代码先不贴了,这些实践现在都很成熟。
2.3 应用模块组织
如何组织我们项目的业务模块能够有一个比较好的扩展性?
业务模块全部放在一个maven模块里面,通过分包的方式组织模块。
这种方式通过分包的方式组织模块,但是由于没有架构层面的强约束,很容易各个模块的方法混在一起,在后期不容易拆分。
通过maven模块化组织,让每个模块引入其他业务模块的接口,每个业务模块实现自己的业务方法。
明显可以看到第二种方式在大型项目后台中有一个比较好的拓展性:
实现了模块之间的解耦合。
如果是单体应用部署只用打包在一起部署,如果是微服务的话引入服务层框架,对每个模块单独部署。升级方便。
避免在项目初期引入过多复杂的组件,同时又有快速扩展能力。按需升级。
贴代码robbendev-shop-backend整理架构 :
可以看到不同模块是按照模块组织,每个业务模块通过ineterfaces模块和其他模块通信。
2.4 应用架构
应用架构的方法论
下面看一下单个应用模块如何组织,单个应用构建的的方法论现在已经比较成熟,这里说两种
经典的三层架构- controller、service、dao、entity
这种很容易让service层膨胀的很大,一个类几千行,每个方法可能会变成事务脚本。
好处就是比较符合直觉思维,写起来也快,代码阅读起来也比较顺利。 缺点可能service层过于臃肿,代码的业务含义不强。
ddd建模 - interfaces、application、infrastruture、domain
这个可以参考一下相关书籍,这里不赘述。我自己还是比较偏向这一种的,现在也慢慢开始流行起来了。一些核心的概念包括聚合、仓储、领域服务、领域事件、应用服务等。
领域对象建模主要是帮助如何建设一个自洽的应用,是属于应用层而不是架构层的方法论。但是由于领域对象建模的思想和微服务思想有大部分相似的地方,所以在做微服务的拆分的时候可以用领域对象方法来做指导,其实微服务拆分本来就是业务模块、限界上下文的划分。
完全的领域建模落地实施起来会比较困难,尤其是在实体的状态管理,领域事件溯源等。所以在实际开发中不用完全照搬领域对象建模的概念,接下来我贴一下我自己的领域对象建模实践。
首先刚才说到的接口实现分离,把二方库依赖版本添加到之前我们提到的统一二方库依赖pom.xml中
贴一下market-service的pom:
//... <dependency><groupId>com.robbendev</groupId><artifactId>robbdendev-common</artifactId><version>${robbendev-common.version}</version> </dependency><dependency><groupId>com.robbendev</groupId><artifactId>shop-common</artifactId><version>${robbendev-shop-backend.version}</version> </dependency><dependency><groupId>com.robbendev</groupId><artifactId>market-interfaces</artifactId><version>${robbendev-shop-backend.version}</version> </dependency><dependency><groupId>com.robbendev</groupId><artifactId>orders-interfaces</artifactId><version>${robbendev-shop-backend.version}</version> </dependency><dependency><groupId>com.robbendev</groupId><artifactId>product-interfaces</artifactId><version>${robbendev-shop-backend.version}</version> </dependency>...这样我们就可以通过接口访问其他模块的方法。
贴一下单个模块的分包,这里单个业务其实可以继续分模块解耦合,但是考虑项目初期的业务复杂程度不会很大,所以还是只分包做分层处理,模块开发的时候团队之间约定好一些基本规范。 order模块按照领域对象建模的分包:
├── orders-interfaces │ ├── pom.xml │ └── src │ ├── main │ │ ├── java │ │ │ └── com.robbebdev.shop.order │ │ │ ├── dto //模块接口参数 │ │ │ │ ├── request //入参定义 │ │ │ │ └── response //出参定义 │ │ │ └── service //模块服务接口 ├── orders-service │ ├── pom.xml │ └── src │ ├── main │ │ ├── java │ │ │ └── com.robbendev.shop.order │ │ │ ├── application //应用服务层 │ │ │ ├── domain //领域层 │ │ │ ├── infrastucture //基础设施层 │ │ │ └── interfaces //用户接口层 ├── pom.xml可以看到有两个maven模块 一个是interfaces模块,里面有模块接口定义和参数定义 一个是service模块,里面会在用户接口层实现interfaces里面的服务接口方法,其他层就和一个ddd的项目差不多。
业务代码
贴一个demo接口具体实现吧,以订单模块为例子,现在写一个更新订单接口。
模块间通信api
/*** <p>* 模块通信的api,具体的实现在用户接口层。* </p>** @author robbendev* @since 2021/4/1 5:07 下午*/ public interface IOrderApi {BaseResult<FindOrderResp> findOrder(FindOrderReq req); }应用服务
/*** <p>* 应用服务,这里是浅浅的一层,可以作为领域层的门面,实体到出参的转换在这里做。* </p>** @author robbendev* @since 2021/4/1 5:35 下午*/ @Service public class IOrderServiceImpl implements IOrderService {@ResourceOrderRepository orderRepository;@Overridepublic FindOrderResp findOrder(FindOrderReq req) {Order order = orderRepository.findById(req.getId());FindOrderResp findOrderResp = new FindOrderResp();findOrderResp.setAmount(order.getAmount());findOrderResp.setProductName(order.getProductName());findOrderResp.setId(order.getId());return findOrderResp;} }实体
/*** 实体,聚合,聚合根!概念参考ddd。像id这些可以用primitive domain实现,像这样。* <code>private OrderId id;</code>** @author robbendev* @since 2021/4/1 5:14 下午*/ @Data public class Order {private Long id;private BigDecimal amount;private String productName; }仓储接口
/*** <p>* 仓储接口,概念参考ddd,可以有多个实现,db实现呀,es实现等。* </p>** @author robbendev* @since 2021/4/1 5:25 下午*/ public interface OrderRepository {Order findById(Long id); }数据对象
/*** <p>* 数据对象,和数据库表字段一一对应。* </p>** @author robbendev* @since 2021/4/1 5:16 下午*/ @Data public class OrderDO {private Long id;private BigDecimal amount;private String productName; }数据库访问接口
/*** <p>* 数据库访问接口* </p>** @author robbendev* @since 2021/4/1 5:27 下午*/ @Mapper public interface OrderMapper {@Select("select * from order where id =#{id}")OrderDO getById(Long id); }仓储的实现
/*** <p>* 仓储的db实现。* </p>** @author robbendev* @since 2021/4/1 5:25 下午*/ @Component public class OrderRepositoryDBImpl implements OrderRepository {@ResourceOrderMapper orderMapper;@Overridepublic Order findById(Long id) {OrderDO orderDO = orderMapper.getById(id);//对象转换替换方案 mapsStruct 或者beanUtils。//有对实体作状态跟踪的方案,但是比较复杂,这里没有选用。//所以在ddd选型的时候不用全上,适合就好。Order order = new Order();order.setId(orderDO.getId());order.setAmount(orderDO.getAmount());order.setProductName(orderDO.getProductName());return order;} }用户接口
/*** <p>* 用户接口(user interface,概念参考ddd)api* </p>** @author robbendev* @since 2021/4/1 5:13 下午*/ @RestController @RequestMapping("/order") public class OrderController implements IOrderApi {@ResourceIOrderService orderService;@Override@PostMapping("/findOrder")public BaseResult<FindOrderResp> findOrder(@RequestBody FindOrderReq req) {FindOrderResp resp = orderService.findOrder(req);return BaseResult.success(resp);}}数据库ddl和配置文件就不写了,就一个springboot默认数据库配置。
2.5 单体应用启动
在集成之前先看下build模块打包项目pom配置,因为要注意一下打包顺序。
可以看到先打包项目公共类库(根据之前的概念,组织公共类库的发布是属于另外的项目,应该有独立的生命周期。),再打包模块接口,最后打包模块应用。这样就不会出现说”哎呀,你搞了什么,我怎么这个文件又找不到。“
再看boot模块的pom文件和代码
<parent><artifactId>robbendev-shop-backend</artifactId><groupId>com.robbendev</groupId><version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion><packaging>jar</packaging> <artifactId>boot</artifactId><dependencies><dependency><groupId>com.robbendev</groupId><artifactId>market-interfaces</artifactId></dependency><dependency><groupId>com.robbendev</groupId><artifactId>market-service</artifactId></dependency><dependency><groupId>com.robbendev</groupId><artifactId>orders-interfaces</artifactId></dependency><dependency><groupId>com.robbendev</groupId><artifactId>orders-service</artifactId></dependency>//...产品用户</dependencies>然后在boot模块里面,几行代码就可以运行一个springboot web程序
/*** <p>** </p>** @author robbendev* @since 2021/3/31 2:43 下午*/ @SpringBootApplication public class AppBoot {public static void main(String[] args) {SpringApplication.run(AppBoot.class, args);} }运行成功截图
2021-04-01 16:40:33.987 INFO 9926 --- [ main] com.robbendev.shop.AppBoot : Starting AppBoot on huluobindeMacBook-Pro.local with PID 9926 (/Users/huluobin/IdeaProjects/robbendev-shop-backend/boot/target/classes started by huluobin in /Users/huluobin/IdeaProjects/robbendev-common) 2021-04-01 16:40:33.991 INFO 9926 --- [ main] com.robbendev.shop.AppBoot : No active profile set, falling back to default profiles: default 2021-04-01 16:40:34.856 INFO 9926 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) 2021-04-01 16:40:34.868 INFO 9926 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] 2021-04-01 16:40:34.869 INFO 9926 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.37] 2021-04-01 16:40:34.969 INFO 9926 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext 2021-04-01 16:40:34.970 INFO 9926 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 887 ms 2021-04-01 16:40:35.150 INFO 9926 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor' 2021-04-01 16:40:35.301 INFO 9926 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' 2021-04-01 16:40:35.309 INFO 9926 --- [ main] com.robbendev.shop.AppBoot : Started AppBoot in 1.944 seconds (JVM running for 3.03)然后post一下我们刚才的接口,一切ok。
好啦,到这里我们整个项目的框架就搭建好了,现在可以按照模块去进行业务开发了。
三、集群
分布式Session
之前我们token是使用的本地缓存,那么在集群情况下就可能会出现不同请求落在不同实例上,导致缓存失效。解决方案:
每个实例都存一份。这样有点浪费。
请求的时候按照一定的路由规则保证每次落在相同的机器上。有点麻烦
把session单独出来。这样需要保证全局缓存的稳定。
这里选第三种方案了,也比较主流。看一下redis的实现
然后在自己的登陆服务里面切换一下就行。
负载均衡
借助Kubernetes的特性,我们可以很容易的实现水平扩容和负载均衡。
把这玩意直接改成你希望扩展的数量就行,然后kubernetes service会自动负载。
或者改yml
spec:progressDeadlineSeconds: 600replicas: 1 //这里改副本数量revisionHistoryLimit: 10小结
本篇主要覆盖了一个java后端从0到1再到集群的一个过程。主要是一些工程上的实践和方法论,同时也是我自己实践过程的一些心路历程。
在服务层做了集群以后,后面我会继续讲一下数据层一如的一些实践,比如数据源分库,中间件分库分表等等,最后再讲微服务。风格的话还是和这篇文章类似。
觉得有收获的同学们帮忙点个赞。
记得继续支持Remi酱哦~~
文章来源:https://www.tuicool.com/articles/raQnmuf
与50位技术专家面对面20年技术见证,附赠技术全景图总结
以上是生活随笔为你收集整理的Java后端架构开荒实战(二)——单机到集群的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: Java后端架构开荒实战(一)——基础设
- 下一篇: Java的多线程和线程池的使用,你真的清