日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

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

發布時間:2025/3/19 java 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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后端架构开荒实战(二)——单机到集群的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。