重學 Java 設計模式:實戰享元模式「基於Redis秒殺,提供活動與庫存資訊查詢場景」

小傅哥發表於2020-06-15


作者:小傅哥
部落格:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

程式設計師?‍?‍的上下文是什麼?

很多時候一大部分程式設計開發的人員都只是關注於功能的實現,只要自己把這部分需求寫完就可以了,有點像被動的交作業。這樣的問題一方面是由於很多新人還不瞭解程式設計師的職業發展,還有一部分是對於程式設計開發只是工作並非興趣。但在程式設計師的發展來看,如果不能很好的處理上文(產品),下文(測試),在這樣不能很好的瞭解業務和產品發展,也不能編寫出很有體系結構的程式碼,日久天長,1到3年、3到5年,就很難跨越一個個技術成長的分水嶺。

擁有接受和學習新知識的能力

你是否有感受過小時候在什麼都還不會的時候接受知識的能力很強,但隨著我們開始長大後,慢慢學習能力、處事方式、性格品行,往往會固定。一方面是形成了各自的性格特徵,一方面是圈子已經固定。但也正因為這樣的故步,而很少願意聽取別人的意見,就像即使看到了一整片內容,在視覺盲區下也會過掉到80%,就在眼前也看不見,也因此導致了能力不再有較大的提升。

程式設計能力怎樣會成長的最快

工作內容往往有些像在工廠?擰螺絲,大部分內容是重複的,也可以想象過去的一年你有過多少創新和學習了新的技能。那麼這時候一般為了多學些內容會買一些技術書籍,但!技術類書籍和其他書籍不同,只要不去用看了也就只是輕描淡寫,很難接納和理解。就像設計模式,雖然可能看了幾遍,但是在實際編碼中仍然很少會用,大部分原因還是沒有認認真真的跟著實操。事必躬親才是學習程式設計的最好是方式。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回覆原始碼下載獲取(開啟獲取的連結,找到序號18)
工程 描述
itstack-demo-design-11-01 使用一坨程式碼實現業務需求
itstack-demo-design-11-02 通過設計模式優化程式碼結構,減少記憶體使用和查詢耗時

三、享元模式介紹

享元模式,圖片來自 refactoringguru.cn

享元模式,主要在於共享通用物件,減少記憶體的使用,提升系統的訪問效率。而這部分共享物件通常比較耗費記憶體或者需要查詢大量介面或者使用資料庫資源,因此統一抽離作為共享物件使用。

另外享元模式可以分為在服務端和客戶端,一般網際網路H5和Web場景下大部分資料都需要服務端進行處理,比如資料庫連線池的使用、多執行緒執行緒池的使用,除了這些功能外,還有些需要服務端進行包裝後的處理下發給客戶端,因為服務端需要做享元處理。但在一些遊戲場景下,很多都是客戶端需要進行渲染地圖效果,比如;樹木、花草、魚蟲,通過設定不同元素描述使用享元公用物件,減少記憶體的佔用,讓客戶端的遊戲更加流暢。

在享元模型的實現中需要使用到享元工廠來進行管理這部分獨立的物件和共享的物件,避免出現執行緒安全的問題。

四、案例場景模擬

場景模擬;秒殺場景下商品查詢

在這個案例中我們模擬在商品秒殺場景下使用享元模式查詢優化

你是否經歷過一個商品下單的專案從最初的日均十幾單到一個月後每個時段秒殺量破十萬的專案。一般在最初如果沒有經驗的情況下可能會使用資料庫行級鎖的方式下保證商品庫存的扣減操作,但是隨著業務的快速發展秒殺的使用者越來越多,這個時候資料庫已經扛不住了,一般都會使用redis的分散式鎖來控制商品庫存。

同時在查詢的時候也不需要每一次對不同的活動查詢都從庫中獲取,因為這裡除了庫存以外其他的活動商品資訊都是固定不變的,以此這裡一般大家會快取到記憶體中。

這裡我們模擬使用享元模式工廠結構,提供活動商品的查詢。活動商品相當於不變的資訊,而庫存部分屬於變化的資訊。

五、用一坨坨程式碼實現

邏輯很簡單,就怕你寫亂。一片片的固定內容和變化內容的查詢組合,CV的哪裡都是!

其實這部分邏輯的查詢在一般情況很多程式設計師都是先查詢固定資訊,在使用過濾的或者新增if判斷的方式補充變化的資訊,也就是庫存。這樣寫最開始並不會看出來有什麼問題,但隨著方法邏輯的增加,後面就越來越多重複的程式碼。

1. 工程結構

itstack-demo-design-11-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── ActivityController.java
  • 以上工程結構比較簡單,之後一個控制類用於查詢活動資訊。

2. 程式碼實現

/**
 * 部落格:https://bugstack.cn - 沉澱、分享、成長,讓自己和他人都能有所收穫!
 * 公眾號:bugstack蟲洞棧
 * Create by 小傅哥(fustack) @2020
 */
public class ActivityController {

    public Activity queryActivityInfo(Long id) {
        // 模擬從實際業務應用從介面中獲取活動資訊
        Activity activity = new Activity();
        activity.setId(10001L);
        activity.setName("圖書嗨樂");
        activity.setDesc("圖書優惠券分享激勵分享活動第二期");
        activity.setStartTime(new Date());
        activity.setStopTime(new Date());
        activity.setStock(new Stock(1000,1));
        return activity;
    }

}
  • 這裡模擬的是從介面中查詢活動資訊,基本也就是從資料庫中獲取所有的商品資訊和庫存。有點像最開始寫的商品銷售系統,資料庫就可以抗住購物量。
  • 當後續因為業務的發展需要擴充套件程式碼將庫存部分交給redis處理,那麼久需要從redis中獲取活動的庫存,而不是從庫中,否則將造成資料不統一的問題。

六、享元模式重構程式碼

接下來使用享元模式來進行程式碼優化,也算是一次很小的重構。

享元模式一般情況下使用此結構在平時的開發中並不太多,除了一些執行緒池、資料庫連線池外,再就是遊戲場景下的場景渲染。另外這個設計的模式思想是減少記憶體的使用提升效率,與我們之前使用的原型模式通過克隆物件的方式生成複雜物件,減少rpc的呼叫,都是此類思想。

1. 工程結構

itstack-demo-design-11-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── util
    │           │	└── RedisUtils.java	
    │           ├── Activity.java
    │           ├── ActivityController.java
    │           ├── ActivityFactory.java
    │           └── Stock.java
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

享元模式模型結構

享元模式模型結構

  • 以上是我們模擬查詢活動場景的類圖結構,左側構建的是享元工廠,提供固定活動資料的查詢,右側是Redis存放的庫存資料。
  • 最終交給活動控制類來處理查詢操作,並提供活動的所有資訊和庫存。因為庫存是變化的,所以我們模擬的RedisUtils中設定了定時任務使用庫存。

2. 程式碼實現

2.1 活動資訊

public class Activity {

    private Long id;        // 活動ID
    private String name;    // 活動名稱
    private String desc;    // 活動描述
    private Date startTime; // 開始時間
    private Date stopTime;  // 結束時間
    private Stock stock;    // 活動庫存
    
    // ...get/set
}
  • 這裡的物件類比較簡單,只是一個活動的基礎資訊;id、名稱、描述、時間和庫存。

2.2 庫存資訊

public class Stock {

    private int total; // 庫存總量
    private int used;  // 庫存已用
    
    // ...get/set
}
  • 這裡是庫存資料我們單獨提供了一個類進行儲存資料。

2.3 享元工廠

public class ActivityFactory {

    static Map<Long, Activity> activityMap = new HashMap<Long, Activity>();

    public static Activity getActivity(Long id) {
        Activity activity = activityMap.get(id);
        if (null == activity) {
            // 模擬從實際業務應用從介面中獲取活動資訊
            activity = new Activity();
            activity.setId(10001L);
            activity.setName("圖書嗨樂");
            activity.setDesc("圖書優惠券分享激勵分享活動第二期");
            activity.setStartTime(new Date());
            activity.setStopTime(new Date());
            activityMap.put(id, activity);
        }
        return activity;
    }

}
  • 這裡提供的是一個享元工廠?,通過map結構存放已經從庫表或者介面中查詢到的資料,存放到記憶體中,用於下次可以直接獲取。
  • 這樣的結構一般在我們的程式設計開發中還是比較常見的,當然也有些時候為了分散式的獲取,會把資料存放到redis中,可以按需選擇。

2.4 模擬Redis類

public class RedisUtils {

    private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

    private AtomicInteger stock = new AtomicInteger(0);

    public RedisUtils() {
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            // 模擬庫存消耗
            stock.addAndGet(1);
        }, 0, 100000, TimeUnit.MICROSECONDS);

    }

    public int getStockUsed() {
        return stock.get();
    }

}
  • 這裡處理模擬redis的操作工具類外,還提供了一個定時任務用於模擬庫存的使用,這樣方面我們在測試的時候可以觀察到庫存的變化。

2.4 活動控制類

public class ActivityController {

    private RedisUtils redisUtils = new RedisUtils();

    public Activity queryActivityInfo(Long id) {
        Activity activity = ActivityFactory.getActivity(id);
        // 模擬從Redis中獲取庫存變化資訊
        Stock stock = new Stock(1000, redisUtils.getStockUsed());
        activity.setStock(stock);
        return activity;
    }

}
  • 在活動控制類中使用了享元工廠獲取活動資訊,查詢後將庫存資訊在補充上。因為庫存資訊是變化的,而活動資訊是固定不變的。
  • 最終通過統一的控制類就可以把完整包裝後的活動資訊返回給呼叫方。

3. 測試驗證

3.1 編寫測試類

public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    private ActivityController activityController = new ActivityController();

    @Test
    public void test_queryActivityInfo() throws InterruptedException {
        for (int idx = 0; idx < 10; idx++) {
            Long req = 10001L;
            Activity activity = activityController.queryActivityInfo(req);
            logger.info("測試結果:{} {}", req, JSON.toJSONString(activity));
            Thread.sleep(1200);
        }
    }

}
  • 這裡我們通過活動查詢控制類,在for迴圈的操作下查詢了十次活動資訊,同時為了保證庫存定時任務的變化,加了睡眠操作,實際的開發中不會有這樣的睡眠。

3.2 測試結果

22:35:20.285 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":1},"stopTime":1592130919931}
22:35:21.634 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":18},"stopTime":1592130919931}
22:35:22.838 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":30},"stopTime":1592130919931}
22:35:24.042 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":42},"stopTime":1592130919931}
22:35:25.246 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":54},"stopTime":1592130919931}
22:35:26.452 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":66},"stopTime":1592130919931}
22:35:27.655 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":78},"stopTime":1592130919931}
22:35:28.859 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":90},"stopTime":1592130919931}
22:35:30.063 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":102},"stopTime":1592130919931}
22:35:31.268 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":114},"stopTime":1592130919931}

Process finished with exit code 0
  • 可以仔細看下stock部分的庫存是一直在變化的,其他部分是活動資訊,是固定的,所以我們使用享元模式來將這樣的結構進行拆分。

七、總結

  • 關於享元模式的設計可以著重學習享元工廠的設計,在一些有大量重複物件可複用的場景下,使用此場景在服務端減少介面的呼叫,在客戶端減少記憶體的佔用。是這個設計模式的主要應用方式。
  • 另外通過map結構的使用方式也可以看到,使用一個固定id來存放和獲取物件,是非常關鍵的點。而且不只是在享元模式中使用,一些其他工廠模式、介面卡模式、組合模式中都可以通過map結構存放服務供外部獲取,減少ifelse的判斷使用。
  • 當然除了這種設計的減少記憶體的使用優點外,也有它帶來的缺點,在一些複雜的業務處理場景,很不容易區分出內部和外部狀態,就像我們活動資訊部分與庫存變化部分。如果不能很好的拆分,就會把享元工廠設計的非常混亂,難以維護。

八、推薦閱讀

相關文章