從橫向和縱向兩個(gè)維度尋求復(fù)雜問題的答案
JAVA前線
歡迎大家關(guān)注公眾號(hào)「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實(shí)際應(yīng)用、架構(gòu)思維、職場分享、產(chǎn)品思考等等,同時(shí)也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
1 多維度思維
在知乎上看到了這個(gè)有意思的問題:一頭牛重800公斤,一座橋承重700公斤,牛應(yīng)該怎么過橋。初看題目我們不難得出兩個(gè)方案:橋梁加固、等待牛體重降至700公斤。
這兩個(gè)方案顯然是正確的,但是我們不能就此止步。因?yàn)檫@類問題考察的是思維方法論,直接給出答案反而非最重要,對于這個(gè)問題我們可以從合理性、結(jié)構(gòu)化、可行性三個(gè)維度進(jìn)行分析。
1.1 合理性分析
一頭800公斤的牛要通過承重700公斤的橋,這個(gè)需求本身合理嗎?我們可以從必要性、緊迫性、替代性這三個(gè)維度提出三個(gè)問題:
第一個(gè)問題問必要性:牛為什么要過橋,到底什么事情非要過橋不可
第二個(gè)問題問緊迫性:如果非要過橋,那么這個(gè)過橋的需求是否緊急
第三個(gè)問題問替代性:有沒有什么替代方案,是否可以坐船或者繞路走
1.2 結(jié)構(gòu)化分析
如果經(jīng)過討論結(jié)果是牛非過橋不可,那么我們再思考牛怎么過橋的方案。這里可以使用結(jié)構(gòu)化思維,將大問題拆分為小維度,盡量做到不遺漏和不重復(fù)。影響過橋的因素有這幾個(gè)維度:橋的維度、牛的維度、資源維度、環(huán)境維度。
橋的維度:加固橋使承重大于800公斤
牛的維度:等待牛的體重小于700公斤
資源維度:使用一臺(tái)吊機(jī)把牛運(yùn)過去
環(huán)境維度:取消環(huán)境重力
1.3 可行性分析
我們從橋的維度、牛的維度、資源維度、環(huán)境維度給出了方案,那么選擇哪個(gè)方案呢?這就需要我們進(jìn)行可行性評估,因時(shí)因地在資源制約下選擇當(dāng)前最合適的方案。
加固橋方案經(jīng)濟(jì)成本較高,等待牛的體重小于700公斤時(shí)間成本較高,取消環(huán)境重力技術(shù)難度較高,所以使用一臺(tái)吊機(jī)把牛運(yùn)過去這個(gè)方案目前看來最合適。
1.4 多維度思考
經(jīng)過我們從合理性、結(jié)構(gòu)化、可行性三個(gè)維度梳理之后,雖然答案沒有什么新穎之處,但是思維過程很清晰,思考方法也可以應(yīng)用在其它問題。之所以思維過程清晰,是因?yàn)槲覀儧]有一上來直接給出答案,而是從多個(gè)維度對為題進(jìn)行了分析,所以增加維度可以使思考過程更清晰。
2 縱向思維與橫向思維
思考維度可以從多方面進(jìn)行充實(shí),其中最常見的是增加橫向和縱向兩個(gè)維度,本文也著重討論兩個(gè)維度??傮w而言,橫向擴(kuò)展的是思考廣度,縱向擴(kuò)展的是思考深度,而應(yīng)用在不同場景中細(xì)節(jié)又各有不同。
2.1 時(shí)間管理四象限
時(shí)間管理理論四象限法則根據(jù)重要和緊急兩個(gè)維度,建立了一個(gè)四象限坐標(biāo),可以幫助我們解決主次不分的問題。我們分配工作時(shí)間時(shí)可以結(jié)合四象限法則,重要且緊急的任務(wù)優(yōu)先級(jí)最高,而不要急于處理不重要且不緊急的任務(wù)。

2.2 金字塔原理
金字塔思維的核心思想并不復(fù)雜:一件事情可以總結(jié)出一個(gè)中心思想,這個(gè)中心思想可以由三至七個(gè)論點(diǎn)支持,每個(gè)論點(diǎn)再可以由三至七個(gè)論據(jù)支持,基本結(jié)構(gòu)如下圖:

金字塔原理內(nèi)在結(jié)構(gòu)可以從縱向和橫向兩個(gè)維度分析,縱向結(jié)構(gòu)體現(xiàn)了結(jié)論先行和以上統(tǒng)下原則,橫向結(jié)構(gòu)體現(xiàn)了歸類分組和邏輯遞進(jìn)原則。關(guān)于金字塔原理詳細(xì)分析請參看我的文章:結(jié)構(gòu)化思維如何指導(dǎo)技術(shù)系統(tǒng)優(yōu)化。
文章分析到這里,我們發(fā)現(xiàn)縱向和橫向思維有助于厘清思路和增加條理性,下面我們看看縱向和橫向思維怎樣幫助程序員處理復(fù)雜問題。
3 架構(gòu)設(shè)計(jì)如何應(yīng)用縱橫思維
我們分析一個(gè)創(chuàng)建訂單業(yè)務(wù)場景,當(dāng)前有ABC三種訂單類型,A類型訂單價(jià)格9折,物流最大重量不能超過8公斤,不支持退款。B類型訂單價(jià)格8折,物流最大重量不能超過5公斤,支持退款。C類型訂單價(jià)格7折,物流最大重量不能超過1公斤,支持退款。按照需求字面含義平鋪直敘地寫代碼也并不難:
public class OrderServiceImpl implements OrderService {
@Resource
private OrderMapper orderMapper;
@Override
public void createOrder(OrderBO orderBO) {
if (null == orderBO) {
throw new RuntimeException("參數(shù)異常");
}
if (OrderTypeEnum.isNotValid(orderBO.getType())) {
throw new RuntimeException("參數(shù)異常");
}
// A類型訂單
if (OrderTypeEnum.A_TYPE.getCode().equals(orderBO.getType())) {
orderBO.setPrice(orderBO.getPrice() * 0.9);
if (orderBO.getWeight() > 9) {
throw new RuntimeException("超過物流最大重量");
}
orderBO.setRefundSupport(Boolean.FALSE);
}
// B類型訂單
else if (OrderTypeEnum.B_TYPE.getCode().equals(orderBO.getType())) {
orderBO.setPrice(orderBO.getPrice() * 0.8);
if (orderBO.getWeight() > 8) {
throw new RuntimeException("超過物流最大重量");
}
orderBO.setRefundSupport(Boolean.TRUE);
}
// C類型訂單
else if (OrderTypeEnum.C_TYPE.getCode().equals(orderBO.getType())) {
orderBO.setPrice(orderBO.getPrice() * 0.7);
if (orderBO.getWeight() > 7) {
throw new RuntimeException("超過物流最大重量");
}
orderBO.setRefundSupport(Boolean.TRUE);
}
// 保存數(shù)據(jù)
OrderDO orderDO = new OrderDO();
BeanUtils.copyProperties(orderBO, orderDO);
orderMapper.insert(orderDO);
}
}
上述代碼從功能上完全可以實(shí)現(xiàn)業(yè)務(wù)需求,但是程序員不僅要滿足功能,還需要思考代碼的可維護(hù)性。如果新增一種訂單類型,或者新增一個(gè)訂單屬性處理邏輯,那么我們就要在上述邏輯中新增代碼,如果處理不慎就會(huì)影響原有邏輯。
為了避免牽一發(fā)而動(dòng)全身這種情況,設(shè)計(jì)模式中的開閉原則要求我們面向新增開放,面向修改關(guān)閉,我認(rèn)為這是設(shè)計(jì)模式中最重要的一條原則:
當(dāng)需求變化時(shí)通過擴(kuò)展而不是通過修改已有代碼來實(shí)現(xiàn)變化,這樣就保證代碼穩(wěn)定性。擴(kuò)展也不是隨意擴(kuò)展,因?yàn)槭孪榷x了算法,擴(kuò)展也是根據(jù)算法擴(kuò)展,用抽象構(gòu)建框架,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)。標(biāo)準(zhǔn)意義的二十三種設(shè)計(jì)模式說到底最終都是在遵循開閉原則
如何改變平鋪直敘的思考方式?這就要為問題分析加上縱向和橫向兩個(gè)維度,我選擇使用分析矩陣方法,其中縱向表示策略,橫向表示場景。

3.1 縱向做隔離
縱向維度表示策略,不同策略在邏輯上和業(yè)務(wù)上應(yīng)該是隔離的,本實(shí)例包括優(yōu)惠策略、物流策略和退款策略,策略作為抽象,不同訂單類型去擴(kuò)展這個(gè)抽象,策略模式非常適合這種場景。
3.1.1 優(yōu)惠策略
// 優(yōu)惠策略
public interface DiscountStrategy {
public void discount(OrderBO orderBO);
}
// A類型訂單優(yōu)惠策略
@Component
public class TypeADiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.9);
}
}
// A類型訂單優(yōu)惠策略
@Component
public class TypeBDiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.8);
}
}
// A類型訂單優(yōu)惠策略
@Component
public class TypeCDiscountStrategy implements DiscountStrategy {
@Override
public void discount(OrderBO orderBO) {
orderBO.setPrice(orderBO.getPrice() * 0.7);
}
}
// 優(yōu)惠策略工廠
@Component
public class DiscountStrategyFactory implements InitializingBean {
private Map<String, DiscountStrategy> strategyMap = new HashMap<>();
@Resource
private TypeADiscountStrategy typeADiscountStrategy;
@Resource
private TypeBDiscountStrategy typeBDiscountStrategy;
@Resource
private TypeCDiscountStrategy typeCDiscountStrategy;
public DiscountStrategy getStrategy(String type) {
return strategyMap.get(type);
}
@Override
public void afterPropertiesSet() throws Exception {
strategyMap.put(OrderTypeEnum.A_TYPE.getCode(), typeADiscountStrategy);
strategyMap.put(OrderTypeEnum.B_TYPE.getCode(), typeBDiscountStrategy);
strategyMap.put(OrderTypeEnum.C_TYPE.getCode(), typeCDiscountStrategy);
}
}
// 優(yōu)惠策略執(zhí)行器
@Component
public class DiscountStrategyExecutor {
private DiscountStrategyFactory discountStrategyFactory;
public void discount(OrderBO orderBO) {
DiscountStrategy discountStrategy = discountStrategyFactory.getStrategy(orderBO.getType());
if (null == discountStrategy) {
throw new RuntimeException("無優(yōu)惠策略");
}
discountStrategy.discount(orderBO);
}
}
3.1.2 物流策略
// 物流策略
public interface ExpressStrategy {
public void weighing(OrderBO orderBO);
}
// A類型訂單物流策略
@Component
public class TypeAExpressStrategy implements ExpressStrategy {
@Override
public void weighing(OrderBO orderBO) {
if (orderBO.getWeight() > 9) {
throw new RuntimeException("超過物流最大重量");
}
}
}
// B類型訂單物流策略
@Component
public class TypeBExpressStrategy implements ExpressStrategy {
@Override
public void weighing(OrderBO orderBO) {
if (orderBO.getWeight() > 8) {
throw new RuntimeException("超過物流最大重量");
}
}
}
// C類型訂單物流策略
@Component
public class TypeCExpressStrategy implements ExpressStrategy {
@Override
public void weighing(OrderBO orderBO) {
if (orderBO.getWeight() > 7) {
throw new RuntimeException("超過物流最大重量");
}
}
}
// 物流策略工廠
@Component
public class ExpressStrategyFactory implements InitializingBean {
private Map<String, ExpressStrategy> strategyMap = new HashMap<>();
@Resource
private TypeAExpressStrategy typeAExpressStrategy;
@Resource
private TypeBExpressStrategy typeBExpressStrategy;
@Resource
private TypeCExpressStrategy typeCExpressStrategy;
@Override
public void afterPropertiesSet() throws Exception {
strategyMap.put(OrderTypeEnum.A_TYPE.getCode(), typeAExpressStrategy);
strategyMap.put(OrderTypeEnum.B_TYPE.getCode(), typeBExpressStrategy);
strategyMap.put(OrderTypeEnum.C_TYPE.getCode(), typeCExpressStrategy);
}
public ExpressStrategy getStrategy(String type) {
return strategyMap.get(type);
}
}
// 物流策略執(zhí)行器
@Component
public class ExpressStrategyExecutor {
private ExpressStrategyFactory expressStrategyFactory;
public void weighing(OrderBO orderBO) {
ExpressStrategy expressStrategy = expressStrategyFactory.getStrategy(orderBO.getType());
if (null == expressStrategy) {
throw new RuntimeException("無物流策略");
}
expressStrategy.weighing(orderBO);
}
}
3.1.3 退款策略
// 退款策略
public interface RefundStrategy {
public void supportRefund(OrderBO orderBO);
}
// A類型訂單退款策略
@Component
public class TypeARefundStrategy implements RefundStrategy {
@Override
public void supportRefund(OrderBO orderBO) {
orderBO.setRefundSupport(Boolean.FALSE);
}
}
// B類型訂單退款策略
@Component
public class TypeBRefundStrategy implements RefundStrategy {
@Override
public void supportRefund(OrderBO orderBO) {
orderBO.setRefundSupport(Boolean.TRUE);
}
}
// C類型訂單退款策略
@Component
public class TypeCRefundStrategy implements RefundStrategy {
@Override
public void supportRefund(OrderBO orderBO) {
orderBO.setRefundSupport(Boolean.TRUE);
}
}
// 退款策略工廠
@Component
public class RefundStrategyFactory implements InitializingBean {
private Map<String, RefundStrategy> strategyMap = new HashMap<>();
@Resource
private TypeARefundStrategy typeARefundStrategy;
@Resource
private TypeBRefundStrategy typeBRefundStrategy;
@Resource
private TypeCRefundStrategy typeCRefundStrategy;
@Override
public void afterPropertiesSet() throws Exception {
strategyMap.put(OrderTypeEnum.A_TYPE.getCode(), typeARefundStrategy);
strategyMap.put(OrderTypeEnum.B_TYPE.getCode(), typeBRefundStrategy);
strategyMap.put(OrderTypeEnum.C_TYPE.getCode(), typeCRefundStrategy);
}
public RefundStrategy getStrategy(String type) {
return strategyMap.get(type);
}
}
// 退款策略執(zhí)行器
@Component
public class RefundStrategyExecutor {
private RefundStrategyFactory refundStrategyFactory;
public void supportRefund(OrderBO orderBO) {
RefundStrategy refundStrategy = refundStrategyFactory.getStrategy(orderBO.getType());
if (null == refundStrategy) {
throw new RuntimeException("無退款策略");
}
refundStrategy.supportRefund(orderBO);
}
}
3.2 橫向做編排
橫向維度表示場景,一種訂單類型在廣義上可以認(rèn)為是一種業(yè)務(wù)場景,在場景中將獨(dú)立的策略進(jìn)行串聯(lián),模板方法設(shè)計(jì)模式適用于這種場景。
模板方法模式定義一個(gè)操作中的算法骨架,一般使用抽象類定義算法骨架。抽象類同時(shí)定義一些抽象方法,這些抽象方法延遲到子類實(shí)現(xiàn),這樣子類不僅遵守了算法骨架約定,也實(shí)現(xiàn)了自己的算法。既保證了規(guī)約也兼顧靈活性。這就是用抽象構(gòu)建框架,用實(shí)現(xiàn)擴(kuò)展細(xì)節(jié)。
// 創(chuàng)建訂單服務(wù)
public interface CreateOrderService {
public void createOrder(OrderBO orderBO);
}
// 抽象創(chuàng)建訂單流程
public abstract class AbstractCreateOrderFlow {
@Resource
private OrderMapper orderMapper;
public void createOrder(OrderBO orderBO) {
// 參數(shù)校驗(yàn)
if (null == orderBO) {
throw new RuntimeException("參數(shù)異常");
}
if (OrderTypeEnum.isNotValid(orderBO.getType())) {
throw new RuntimeException("參數(shù)異常");
}
// 計(jì)算優(yōu)惠
discount(orderBO);
// 計(jì)算重量
weighing(orderBO);
// 退款支持
supportRefund(orderBO);
// 保存數(shù)據(jù)
OrderDO orderDO = new OrderDO();
BeanUtils.copyProperties(orderBO, orderDO);
orderMapper.insert(orderDO);
}
public abstract void discount(OrderBO orderBO);
public abstract void weighing(OrderBO orderBO);
public abstract void supportRefund(OrderBO orderBO);
}
// 實(shí)現(xiàn)創(chuàng)建訂單流程
@Service
public class CreateOrderFlow extends AbstractCreateOrderFlow {
@Resource
private DiscountStrategyExecutor discountStrategyExecutor;
@Resource
private ExpressStrategyExecutor expressStrategyExecutor;
@Resource
private RefundStrategyExecutor refundStrategyExecutor;
@Override
public void discount(OrderBO orderBO) {
discountStrategyExecutor.discount(orderBO);
}
@Override
public void weighing(OrderBO orderBO) {
expressStrategyExecutor.weighing(orderBO);
}
@Override
public void supportRefund(OrderBO orderBO) {
refundStrategyExecutor.supportRefund(orderBO);
}
}
3.3 復(fù)雜架構(gòu)設(shè)計(jì)
上述實(shí)例業(yè)務(wù)和代碼并不復(fù)雜,其實(shí)復(fù)雜業(yè)務(wù)場景也不過是簡單場景的疊加、組合和交織,無外乎也是通過縱向做隔離、橫向做編排尋求答案。

縱向維度抽象出能力池這個(gè)概念,能力池中包含許多能力,不同的能力按照不同業(yè)務(wù)維度聚合,例如優(yōu)惠能力池,物流能力池,退款能力池。我們可以看到兩種程度的隔離性,能力池之間相互隔離,能力之間也相互隔離。
橫向維度將能力從能力池選出來,按照業(yè)務(wù)需求串聯(lián)在一起,形成不同業(yè)務(wù)流程。因?yàn)槟芰梢匀我饨M合,所以體現(xiàn)了很強(qiáng)的靈活性。除此之外,不同能力既可以串行執(zhí)行,如果不同能力之間沒有依賴關(guān)系,也可以如同流程Y一樣并行執(zhí)行,提升執(zhí)行效率。
4 數(shù)據(jù)分片如何應(yīng)用縱橫思維
現(xiàn)在有一個(gè)電商數(shù)據(jù)庫存放訂單、商品、支付三張業(yè)務(wù)表。隨著業(yè)務(wù)量越來越大,這三張業(yè)務(wù)數(shù)據(jù)表也越來越大,查詢性能顯著降低,數(shù)據(jù)拆分勢在必行。那么數(shù)據(jù)拆分也可以從縱向和橫向兩個(gè)維度進(jìn)行。
4.1 縱向分表
縱向拆分就是按照業(yè)務(wù)拆分,我們將電商數(shù)據(jù)庫拆分成三個(gè)庫,訂單庫、商品庫。支付庫,訂單表在訂單庫,商品表在商品庫,支付表在支付庫。這樣每個(gè)庫只需要存儲(chǔ)本業(yè)務(wù)數(shù)據(jù),物理隔離不會(huì)互相影響。

4.2 橫向分表
按照縱向拆分方案之后我們已經(jīng)有三個(gè)庫了,平穩(wěn)運(yùn)行了一段時(shí)間。但是隨著業(yè)務(wù)增長,每個(gè)單庫單表的數(shù)據(jù)量也越來越大,逐漸到達(dá)瓶頸。
這時(shí)我們就要對數(shù)據(jù)表進(jìn)行橫向拆分,所謂橫向拆分就是根據(jù)某種規(guī)則將單庫單表數(shù)據(jù)分散到多庫多表,從而減小單庫單表的壓力。
橫向拆分策略有很多方案,最重要的一點(diǎn)是選好ShardingKey,也就是按照哪一列進(jìn)行拆分,怎么分取決于我們訪問數(shù)據(jù)的方式。
4.2.1 范圍分片
如果我們選擇的ShardingKey是訂單創(chuàng)建時(shí)間,那么分片策略是拆分四個(gè)數(shù)據(jù)庫分別存儲(chǔ)每季度數(shù)據(jù),每個(gè)庫包含三張表分別存儲(chǔ)每個(gè)月數(shù)據(jù):

這個(gè)方案的優(yōu)點(diǎn)是對范圍查詢比較友好,例如我們需要統(tǒng)計(jì)第一季度的相關(guān)數(shù)據(jù),查詢條件直接輸入時(shí)間范圍即可。這個(gè)方案的問題是容易產(chǎn)生熱點(diǎn)數(shù)據(jù)。例如雙11當(dāng)天下單量特別大,就會(huì)導(dǎo)致11月這張表數(shù)據(jù)量特別大從而造成訪問壓力。
4.2.2 查表分片
查表法是根據(jù)一張路由表決定ShardingKey路由到哪一張表,每次路由時(shí)首先到路由表里查到分片信息,再到這個(gè)分片去取數(shù)據(jù)。我們分析一個(gè)查表法思想應(yīng)用實(shí)際案例。
Redis官方在3.0版本之后提供了官方集群方案RedisCluster,其中引入了哈希槽(slot)這個(gè)概念。一個(gè)集群固定有16384個(gè)槽,在集群初始化時(shí)這些槽會(huì)平均分配到Redis集群節(jié)點(diǎn)上。每個(gè)key請求最終落到哪個(gè)槽計(jì)算公式是固定的:
SLOT = CRC16(key) mod 16384
一個(gè)key請求過來怎么知道去哪臺(tái)Redis節(jié)點(diǎn)獲取數(shù)據(jù)?這就要用到查表法思想:
(1) 客戶端連接任意一臺(tái)Redis節(jié)點(diǎn),假設(shè)隨機(jī)訪問到節(jié)點(diǎn)A
(2) 節(jié)點(diǎn)A根據(jù)key計(jì)算出slot值
(3) 每個(gè)節(jié)點(diǎn)都維護(hù)著slot和節(jié)點(diǎn)映射關(guān)系表
(4) 如果節(jié)點(diǎn)A查表發(fā)現(xiàn)該slot在本節(jié)點(diǎn),直接返回?cái)?shù)據(jù)給客戶端
(5) 如果節(jié)點(diǎn)A查表發(fā)現(xiàn)該slot不在本節(jié)點(diǎn),返回給客戶端一個(gè)重定向命令,告訴客戶端應(yīng)該去哪個(gè)節(jié)點(diǎn)請求這個(gè)key的數(shù)據(jù)
(6) 客戶端向正確節(jié)點(diǎn)發(fā)起連接請求
查表法方案優(yōu)點(diǎn)是可以靈活制定路由策略,如果我們發(fā)現(xiàn)有的分片已經(jīng)成為熱點(diǎn)則修改路由策略。缺點(diǎn)是多一次查詢路由表操作增加耗時(shí),而且路由表如果是單點(diǎn)也可能會(huì)有單點(diǎn)問題。
4.2.3 哈希分片
相較于范圍分片,哈希分片可以較為均勻?qū)?shù)據(jù)分散在數(shù)據(jù)庫中。我們現(xiàn)在將訂單庫拆分為4個(gè)庫編號(hào)為[0,3],每個(gè)庫包含3張表編號(hào)為[0,2],如下圖如所示:

我們選擇使用orderId作為ShardingKey,那么orderId=100這個(gè)訂單會(huì)保存在哪張表?因?yàn)槭欠謳旆直?,第一步確定路由到哪一個(gè)庫,取模計(jì)算結(jié)果表示庫表序號(hào):
db_index = 100 % 4 = 0
第二步確定路由到哪一張表:
table_index = 100 % 3 = 1
第三步數(shù)據(jù)路由到0號(hào)庫1號(hào)表:

在實(shí)際開發(fā)中路由邏輯并不需要我們手動(dòng)實(shí)現(xiàn),因?yàn)橛性S多開源框架通過配置就可以實(shí)現(xiàn)路由功能,例如ShardingSphere、TDDL框架等等。
5 文章總結(jié)
復(fù)雜問題不過是簡單問題的疊加、組合和交織,橫向和縱向兩個(gè)維度拆分問題不失為一種好方法。縱向做隔離是指將不同業(yè)務(wù)形態(tài)進(jìn)行隔離,能力池之間進(jìn)行隔離,能力之間也進(jìn)行隔離。橫向做編排是指從能力池中靈活選擇出能力,進(jìn)行組合和編排,形成形態(tài)各異的業(yè)務(wù)流程,希望本文對大家有所幫助。
JAVA前線
歡迎大家關(guān)注公眾號(hào)「JAVA前線」查看更多精彩分享,主要內(nèi)容包括源碼分析、實(shí)際應(yīng)用、架構(gòu)思維、職場分享、產(chǎn)品思考等等,同時(shí)也非常歡迎大家加我微信「java_front」一起交流學(xué)習(xí)
