欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 编程语言 > java >内容正文

java

Java后端架构开荒实战(二)——单机到集群

发布时间:2025/3/19 java 50 豆豆
生活随笔 收集整理的这篇文章主要介绍了 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

<parent><groupId>com.robbendev</groupId><artifactId>robbendev-shop-backend</artifactId><version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion><artifactId>shop-common</artifactId> <packaging>jar</packaging><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency> </dependencies> ...

用户登陆
用户登陆算是比较独立的模块,单拎一小节。

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整理架构 :

├── boot //聚合了所有模块的单应用启动模块 │ ├── pom.xml │ └── src │ ├── main │ └── test ├── build //这里指定了打包顺序 │ ├── pom.xml ├── pom.xml ├── shop-common //项目的公用类库 │ ├── pom.xml │ └── src └── shop-modules //项目的模块拆分├── pom.xml├── shop-market //营销模块 │ ├── market-interfaces //营销服务接口二方库│ ├── market-service //营销服务│ ├── pom.xml├── shop-orders //订单模块│ ├── orders-interfaces //订单服务接口二方库│ ├── orders-service //订单服务│ ├── pom.xml├── shop-product //产品服务│ ├── pom.xml │ ├── product-interfaces //产品服务接口二方库│ ├── product-service //产品服务└── shop-user //用户服务├── pom.xml├── user-interfaces //用户服务接口二方库└── user-service //用户服务

可以看到不同模块是按照模块组织,每个业务模块通过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接口具体实现吧,以订单模块为例子,现在写一个更新订单接口。

├── orders-interfaces │ └── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── robbendev.shop.order.dto │ │ │ │ ├── request │ │ │ │ │ └── FindOrderReq.java │ │ │ │ └── response │ │ │ │ └── FindOrderResp.java │ │ │ └── service │ │ │ └── IOrderApi.java ├── orders-service │ └── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── robbendev │ │ │ └── shop │ │ │ └── order │ │ │ ├── application │ │ │ │ ├── IOrderService.java //应用服务接口 │ │ │ │ └── IOrderServiceImpl.java //应用服务实现类 其实这里可以不用接口,但是兼容一些人的开发习惯吧。 │ │ │ ├── domain │ │ │ │ ├── Order.java //实体 │ │ │ │ └── OrderRepository.java //仓储接口 │ │ │ ├── infrastucture │ │ │ │ ├── dataobject │ │ │ │ │ └── OrderDO.java //数据对象 │ │ │ │ ├── mapper │ │ │ │ │ └── OrderMapper.java //数据接口 │ │ │ │ └── repository │ │ │ │ └── OrderRepositoryImpl.java //仓储的db实现 │ │ │ └── interfaces │ │ │ └── OrderController.java //暴露的外部api,需要实现interfaces包中的 IOrderApi

模块间通信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配置,因为要注意一下打包顺序。

<parent><artifactId>robbendev-shop-backend</artifactId><groupId>com.robbendev</groupId><version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion><artifactId>build</artifactId><packaging>pom</packaging><modules><module>../shop-common</module><module>../shop-modules/shop-market/market-interfaces</module><module>../shop-modules/shop-orders/orders-interfaces</module><module>../shop-modules/shop-product/product-interfaces</module><module>../shop-modules/shop-user/user-interfaces</module><module>../shop-modules/shop-market/market-service</module><module>../shop-modules/shop-orders/orders-service</module><module>../shop-modules/shop-product/product-service</module><module>../shop-modules/shop-user/user-service</module><module>../boot</module> </modules>

可以看到先打包项目公共类库(根据之前的概念,组织公共类库的发布是属于另外的项目,应该有独立的生命周期。),再打包模块接口,最后打包模块应用。这样就不会出现说”哎呀,你搞了什么,我怎么这个文件又找不到。“

再看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的实现

@Service public class TokenServiceRedisImpl implements TokenService {@ResourceStringRedisTemplate stringRedisTemplate;@Overridepublic void save(Token token) {stringRedisTemplate.opsForValue().set(token.getToken(), JsonUtilByFsJson.beanToJson(token), 1, TimeUnit.DAYS);}@Overridepublic void remove(String token) {stringRedisTemplate.delete(token);} }

然后在自己的登陆服务里面切换一下就行。

负载均衡
借助Kubernetes的特性,我们可以很容易的实现水平扩容和负载均衡。

把这玩意直接改成你希望扩展的数量就行,然后kubernetes service会自动负载。

或者改yml

spec:progressDeadlineSeconds: 600replicas: 1 //这里改副本数量revisionHistoryLimit: 10

小结
本篇主要覆盖了一个java后端从0到1再到集群的一个过程。主要是一些工程上的实践和方法论,同时也是我自己实践过程的一些心路历程。

在服务层做了集群以后,后面我会继续讲一下数据层一如的一些实践,比如数据源分库,中间件分库分表等等,最后再讲微服务。风格的话还是和这篇文章类似。

觉得有收获的同学们帮忙点个赞。

记得继续支持Remi酱哦~~

文章来源:https://www.tuicool.com/articles/raQnmuf

与50位技术专家面对面20年技术见证,附赠技术全景图

总结

以上是生活随笔为你收集整理的Java后端架构开荒实战(二)——单机到集群的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。