Java后端架构开荒实战(二)——单机到集群
Java后端架構(gòu)開(kāi)荒實(shí)戰(zhàn)(二)——單機(jī)到集群
一、前言
上一篇文章做了一些準(zhǔn)備工作,這邊文章正式開(kāi)始寫(xiě)代碼。
在做好單實(shí)例架構(gòu)之后,升級(jí)到集群是一件很容易的事情,所以把單機(jī)和集群放在這一篇一起說(shuō)。
二、單體項(xiàng)目架構(gòu)
在開(kāi)始前先說(shuō)一下本文一些名詞的定義吧。
組織(org):這個(gè)就是公司的意思,一個(gè)公司組織下面可能會(huì)有多個(gè)項(xiàng)目。
項(xiàng)目(project):項(xiàng)目在內(nèi)部是要自洽的,項(xiàng)目和項(xiàng)目的調(diào)用之間就屬于第三方調(diào)用了。比如本文提到的電商后端就是一個(gè)項(xiàng)目,組織公共類庫(kù)就屬于另外一個(gè)項(xiàng)目,每個(gè)項(xiàng)目有自己的生命周期。
應(yīng)用(application):應(yīng)用一般是一個(gè)領(lǐng)域服務(wù)的形式,在單體應(yīng)用中可能是一個(gè)業(yè)務(wù)模塊,在微服務(wù)架構(gòu)中可能是一個(gè)微服務(wù)。
2.1 組織公共類庫(kù)
這種二方庫(kù)一般是公司組織級(jí)別的,就是封裝了所有項(xiàng)目都可能用到的公共方法、配置和工具類等等,注意區(qū)別與項(xiàng)目里面的公共類庫(kù),這些類庫(kù)的設(shè)計(jì)要注意通用性。
一些項(xiàng)目級(jí)別的專有配置和工具就不要放到這里來(lái)啦。
可以按照springboot源碼那樣按maven模塊組織,也可以簡(jiǎn)單一點(diǎn)只分包吧。
貼一下web方面經(jīng)常需要的配置:
統(tǒng)一返回結(jié)果BaseResult,一個(gè)通用的用接口層的范型返回對(duì)象是非常重要的。
public class BaseResult<T> {/*** 返回狀態(tài)*/private boolean success;/*** 返回狀態(tài)碼*/private String code;/*** 返回信息*/private String message;/*** 返回?cái)?shù)據(jù)*/private T data;...跨域配置,注意這里@ConditionalOnWebApplication web應(yīng)用才生效。
/*** <p>* 跨域配置* </p>** @author robbendev*/ @ConditionalOnWebApplication @Configuration public class GlobalCorsConfig {@Beanpublic CorsFilter corsFilter() {//1.添加CORS配置信息CorsConfiguration config = new CorsConfiguration();//放行哪些原始域config.addAllowedOrigin("*");//是否發(fā)送Cookie信息config.setAllowCredentials(true);//放行哪些原始域(請(qǐng)求方式)config.addAllowedMethod("*");//放行哪些原始域(頭部信息)config.addAllowedHeader("*");config.setMaxAge(3600L);//暴露哪些頭部信息(因?yàn)榭缬蛟L問(wèn)默認(rèn)不能獲取全部頭部信息)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);} }通用業(yè)務(wù)異常,web應(yīng)用的一般在業(yè)務(wù)層拋出手動(dòng)拋出,由全局異常捕獲轉(zhuǎn)然后轉(zhuǎn)化成通用返回值返回。
/*** 通用業(yè)務(wù)異常** @author robbendev*/ @EqualsAndHashCode(callSuper = true) @Data public class BizException extends RuntimeException implements Serializable {/*** 序列化*/private static final long serialVersionUID = -4636716497382947499L;/*** 錯(cuò)誤碼*/private String code;/*** 錯(cuò)誤信息*/private String message;/*** 錯(cuò)誤詳情*/private Object data; }備份流 (RequestBakRequestWrapper就不貼了),攔截器那里會(huì)用到。
/*** 對(duì)request請(qǐng)求進(jìn)行包裝備份請(qǐng)求參數(shù)** @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() {} }其他的配置各個(gè)公司的最佳實(shí)踐不一而同。
2.2 項(xiàng)目公共類庫(kù)
這種公共類庫(kù)是項(xiàng)目級(jí)別的,每個(gè)不同的項(xiàng)目會(huì)有項(xiàng)目?jī)?nèi)部的自定義公用類庫(kù)需求。
如果你需要web開(kāi)發(fā)就需要springboot-web諸如此類,這些就定義在這里。
項(xiàng)目依賴
shop-common/pom/xml
用戶登陸
用戶登陸算是比較獨(dú)立的模塊,單拎一小節(jié)。
spring security + jwt的方案。
服務(wù)端session這種。
大家可以自行搜索一下oauth2.0和一些單點(diǎn)登錄的方案。
shop項(xiàng)目的話用戶登陸token簽發(fā)是通過(guò)服務(wù)端session來(lái)做的。對(duì)應(yīng)的服務(wù)定義在shop-common里面。貼一個(gè)token的本地緩存簡(jiǎn)單實(shí)現(xiàn)
@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);} }有興趣的同學(xué)可以試試如何實(shí)現(xiàn)過(guò)期緩存。
文件服務(wù)
不貼代碼了,也是屬于shop-common模塊的,各個(gè)云服務(wù)商都提供樣板代碼。 注意做成接口實(shí)現(xiàn)分離形式,在項(xiàng)目里淺封裝一下。
其他
還有一些應(yīng)用級(jí)別的配置類、攔截器,日志處理等等。代碼先不貼了,這些實(shí)踐現(xiàn)在都很成熟。
2.3 應(yīng)用模塊組織
如何組織我們項(xiàng)目的業(yè)務(wù)模塊能夠有一個(gè)比較好的擴(kuò)展性?
業(yè)務(wù)模塊全部放在一個(gè)maven模塊里面,通過(guò)分包的方式組織模塊。
這種方式通過(guò)分包的方式組織模塊,但是由于沒(méi)有架構(gòu)層面的強(qiáng)約束,很容易各個(gè)模塊的方法混在一起,在后期不容易拆分。
通過(guò)maven模塊化組織,讓每個(gè)模塊引入其他業(yè)務(wù)模塊的接口,每個(gè)業(yè)務(wù)模塊實(shí)現(xiàn)自己的業(yè)務(wù)方法。
明顯可以看到第二種方式在大型項(xiàng)目后臺(tái)中有一個(gè)比較好的拓展性:
實(shí)現(xiàn)了模塊之間的解耦合。
如果是單體應(yīng)用部署只用打包在一起部署,如果是微服務(wù)的話引入服務(wù)層框架,對(duì)每個(gè)模塊單獨(dú)部署。升級(jí)方便。
避免在項(xiàng)目初期引入過(guò)多復(fù)雜的組件,同時(shí)又有快速擴(kuò)展能力。按需升級(jí)。
貼代碼robbendev-shop-backend整理架構(gòu) :
可以看到不同模塊是按照模塊組織,每個(gè)業(yè)務(wù)模塊通過(guò)ineterfaces模塊和其他模塊通信。
2.4 應(yīng)用架構(gòu)
應(yīng)用架構(gòu)的方法論
下面看一下單個(gè)應(yīng)用模塊如何組織,單個(gè)應(yīng)用構(gòu)建的的方法論現(xiàn)在已經(jīng)比較成熟,這里說(shuō)兩種
經(jīng)典的三層架構(gòu)- controller、service、dao、entity
這種很容易讓service層膨脹的很大,一個(gè)類幾千行,每個(gè)方法可能會(huì)變成事務(wù)腳本。
好處就是比較符合直覺(jué)思維,寫(xiě)起來(lái)也快,代碼閱讀起來(lái)也比較順利。 缺點(diǎn)可能service層過(guò)于臃腫,代碼的業(yè)務(wù)含義不強(qiáng)。
ddd建模 - interfaces、application、infrastruture、domain
這個(gè)可以參考一下相關(guān)書(shū)籍,這里不贅述。我自己還是比較偏向這一種的,現(xiàn)在也慢慢開(kāi)始流行起來(lái)了。一些核心的概念包括聚合、倉(cāng)儲(chǔ)、領(lǐng)域服務(wù)、領(lǐng)域事件、應(yīng)用服務(wù)等。
領(lǐng)域?qū)ο蠼V饕菐椭绾谓ㄔO(shè)一個(gè)自洽的應(yīng)用,是屬于應(yīng)用層而不是架構(gòu)層的方法論。但是由于領(lǐng)域?qū)ο蠼5乃枷牒臀⒎?wù)思想有大部分相似的地方,所以在做微服務(wù)的拆分的時(shí)候可以用領(lǐng)域?qū)ο蠓椒▉?lái)做指導(dǎo),其實(shí)微服務(wù)拆分本來(lái)就是業(yè)務(wù)模塊、限界上下文的劃分。
完全的領(lǐng)域建模落地實(shí)施起來(lái)會(huì)比較困難,尤其是在實(shí)體的狀態(tài)管理,領(lǐng)域事件溯源等。所以在實(shí)際開(kāi)發(fā)中不用完全照搬領(lǐng)域?qū)ο蠼5母拍?#xff0c;接下來(lái)我貼一下我自己的領(lǐng)域?qū)ο蠼?shí)踐。
首先剛才說(shuō)到的接口實(shí)現(xiàn)分離,把二方庫(kù)依賴版本添加到之前我們提到的統(tǒng)一二方庫(kù)依賴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>...這樣我們就可以通過(guò)接口訪問(wèn)其他模塊的方法。
貼一下單個(gè)模塊的分包,這里單個(gè)業(yè)務(wù)其實(shí)可以繼續(xù)分模塊解耦合,但是考慮項(xiàng)目初期的業(yè)務(wù)復(fù)雜程度不會(huì)很大,所以還是只分包做分層處理,模塊開(kāi)發(fā)的時(shí)候團(tuán)隊(duì)之間約定好一些基本規(guī)范。 order模塊按照領(lǐng)域?qū)ο蠼5姆职?
├── orders-interfaces │ ├── pom.xml │ └── src │ ├── main │ │ ├── java │ │ │ └── com.robbebdev.shop.order │ │ │ ├── dto //模塊接口參數(shù) │ │ │ │ ├── request //入?yún)⒍x │ │ │ │ └── response //出參定義 │ │ │ └── service //模塊服務(wù)接口 ├── orders-service │ ├── pom.xml │ └── src │ ├── main │ │ ├── java │ │ │ └── com.robbendev.shop.order │ │ │ ├── application //應(yīng)用服務(wù)層 │ │ │ ├── domain //領(lǐng)域?qū)?/span> │ │ │ ├── infrastucture //基礎(chǔ)設(shè)施層 │ │ │ └── interfaces //用戶接口層 ├── pom.xml可以看到有兩個(gè)maven模塊 一個(gè)是interfaces模塊,里面有模塊接口定義和參數(shù)定義 一個(gè)是service模塊,里面會(huì)在用戶接口層實(shí)現(xiàn)interfaces里面的服務(wù)接口方法,其他層就和一個(gè)ddd的項(xiàng)目差不多。
業(yè)務(wù)代碼
貼一個(gè)demo接口具體實(shí)現(xiàn)吧,以訂單模塊為例子,現(xiàn)在寫(xiě)一個(gè)更新訂單接口。
模塊間通信api
/*** <p>* 模塊通信的api,具體的實(shí)現(xiàn)在用戶接口層。* </p>** @author robbendev* @since 2021/4/1 5:07 下午*/ public interface IOrderApi {BaseResult<FindOrderResp> findOrder(FindOrderReq req); }應(yīng)用服務(wù)
/*** <p>* 應(yīng)用服務(wù),這里是淺淺的一層,可以作為領(lǐng)域?qū)拥拈T面,實(shí)體到出參的轉(zhuǎn)換在這里做。* </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;} }實(shí)體
/*** 實(shí)體,聚合,聚合根!概念參考ddd。像id這些可以用primitive domain實(shí)現(xiàn),像這樣。* <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; }倉(cāng)儲(chǔ)接口
/*** <p>* 倉(cāng)儲(chǔ)接口,概念參考ddd,可以有多個(gè)實(shí)現(xiàn),db實(shí)現(xiàn)呀,es實(shí)現(xiàn)等。* </p>** @author robbendev* @since 2021/4/1 5:25 下午*/ public interface OrderRepository {Order findById(Long id); }數(shù)據(jù)對(duì)象
/*** <p>* 數(shù)據(jù)對(duì)象,和數(shù)據(jù)庫(kù)表字段一一對(duì)應(yīng)。* </p>** @author robbendev* @since 2021/4/1 5:16 下午*/ @Data public class OrderDO {private Long id;private BigDecimal amount;private String productName; }數(shù)據(jù)庫(kù)訪問(wèn)接口
/*** <p>* 數(shù)據(jù)庫(kù)訪問(wèn)接口* </p>** @author robbendev* @since 2021/4/1 5:27 下午*/ @Mapper public interface OrderMapper {@Select("select * from order where id =#{id}")OrderDO getById(Long id); }倉(cāng)儲(chǔ)的實(shí)現(xiàn)
/*** <p>* 倉(cāng)儲(chǔ)的db實(shí)現(xiàn)。* </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);//對(duì)象轉(zhuǎn)換替換方案 mapsStruct 或者beanUtils。//有對(duì)實(shí)體作狀態(tài)跟蹤的方案,但是比較復(fù)雜,這里沒(méi)有選用。//所以在ddd選型的時(shí)候不用全上,適合就好。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);}}數(shù)據(jù)庫(kù)ddl和配置文件就不寫(xiě)了,就一個(gè)springboot默認(rèn)數(shù)據(jù)庫(kù)配置。
2.5 單體應(yīng)用啟動(dòng)
在集成之前先看下build模塊打包項(xiàng)目pom配置,因?yàn)橐⒁庖幌麓虬樞颉?/p>
<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>
可以看到先打包項(xiàng)目公共類庫(kù)(根據(jù)之前的概念,組織公共類庫(kù)的發(fā)布是屬于另外的項(xiàng)目,應(yīng)該有獨(dú)立的生命周期。),再打包模塊接口,最后打包模塊應(yīng)用。這樣就不會(huì)出現(xiàn)說(shuō)”哎呀,你搞了什么,我怎么這個(gè)文件又找不到。“
再看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>//...產(chǎn)品用戶</dependencies>然后在boot模塊里面,幾行代碼就可以運(yùn)行一個(gè)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);} }運(yùn)行成功截圖
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。
好啦,到這里我們整個(gè)項(xiàng)目的框架就搭建好了,現(xiàn)在可以按照模塊去進(jìn)行業(yè)務(wù)開(kāi)發(fā)了。
三、集群
分布式Session
之前我們token是使用的本地緩存,那么在集群情況下就可能會(huì)出現(xiàn)不同請(qǐng)求落在不同實(shí)例上,導(dǎo)致緩存失效。解決方案:
每個(gè)實(shí)例都存一份。這樣有點(diǎn)浪費(fèi)。
請(qǐng)求的時(shí)候按照一定的路由規(guī)則保證每次落在相同的機(jī)器上。有點(diǎn)麻煩
把session單獨(dú)出來(lái)。這樣需要保證全局緩存的穩(wěn)定。
這里選第三種方案了,也比較主流。看一下redis的實(shí)現(xiàn)
然后在自己的登陸服務(wù)里面切換一下就行。
負(fù)載均衡
借助Kubernetes的特性,我們可以很容易的實(shí)現(xiàn)水平擴(kuò)容和負(fù)載均衡。
把這玩意直接改成你希望擴(kuò)展的數(shù)量就行,然后kubernetes service會(huì)自動(dòng)負(fù)載。
或者改yml
spec:progressDeadlineSeconds: 600replicas: 1 //這里改副本數(shù)量revisionHistoryLimit: 10小結(jié)
本篇主要覆蓋了一個(gè)java后端從0到1再到集群的一個(gè)過(guò)程。主要是一些工程上的實(shí)踐和方法論,同時(shí)也是我自己實(shí)踐過(guò)程的一些心路歷程。
在服務(wù)層做了集群以后,后面我會(huì)繼續(xù)講一下數(shù)據(jù)層一如的一些實(shí)踐,比如數(shù)據(jù)源分庫(kù),中間件分庫(kù)分表等等,最后再講微服務(wù)。風(fēng)格的話還是和這篇文章類似。
覺(jué)得有收獲的同學(xué)們幫忙點(diǎn)個(gè)贊。
記得繼續(xù)支持Remi醬哦~~
文章來(lái)源:https://www.tuicool.com/articles/raQnmuf
與50位技術(shù)專家面對(duì)面20年技術(shù)見(jiàn)證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的Java后端架构开荒实战(二)——单机到集群的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Java后端架构开荒实战(一)——基础设
- 下一篇: Java的多线程和线程池的使用,你真的清