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的多线程和线程池的使用,你真的清