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

做者:小傅哥
博客:https://bugstack.cnhtml

沉澱、分享、成長,讓本身和他人都能有所收穫!😄

1、前言

程序員👨‍💻‍的上下文是什麼?java

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

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

你是否有感覺太小時候在什麼都還不會的時候接受知識的能力很強,但隨着咱們開始長大後,慢慢學習能力、處事方式、性格品行,每每會固定。一方面是造成了各自的性格特徵,一方面是圈子已經固定。但也正由於這樣的故步,而不多願意聽取別人的意見,就像即便看到了一整片內容,在視覺盲區下也會過掉到80%,就在眼前也看不見,也所以致使了能力再也不有較大的提高。數據庫

編程能力怎樣會成長的最快編程

工做內容每每有些像在工廠🏭擰螺絲,大部份內容是重複的,也能夠想象過去的一年你有過多少創新和學習了新的技能。那麼這時候通常爲了多學些內容會買一些技術書籍,但!技術類書籍和其餘書籍不一樣,只要不去用看了也就只是輕描淡寫,很難接納和理解。就像設計模式,雖然可能看了幾遍,可是在實際編碼中仍然不多會用,大部分緣由仍是沒有認認真真的跟着實操。事必躬親纔是學習編程的最好是方式。設計模式

2、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,能夠經過關注公衆號bugstack蟲洞棧,回覆源碼下載獲取(打開獲取的連接,找到序號18)
工程 描述
itstack-demo-design-11-01 使用一坨代碼實現業務需求
itstack-demo-design-11-02 經過設計模式優化代碼結構,減小內存使用和查詢耗時

3、享元模式介紹

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

享元模式,主要在於共享通用對象,減小內存的使用,提高系統的訪問效率。而這部分共享對象一般比較耗費內存或者須要查詢大量接口或者使用數據庫資源,所以統一抽離做爲共享對象使用。緩存

另外享元模式能夠分爲在服務端和客戶端,通常互聯網H5和Web場景下大部分數據都須要服務端進行處理,好比數據庫鏈接池的使用、多線程線程池的使用,除了這些功能外,還有些須要服務端進行包裝後的處理下發給客戶端,由於服務端須要作享元處理。但在一些遊戲場景下,不少都是客戶端須要進行渲染地圖效果,好比;樹木、花草、魚蟲,經過設置不一樣元素描述使用享元公用對象,減小內存的佔用,讓客戶端的遊戲更加流暢。安全

在享元模型的實現中須要使用到享元工廠來進行管理這部分獨立的對象和共享的對象,避免出現線程安全的問題。微信

4、案例場景模擬

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

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

你是否經歷過一個商品下單的項目從最初的日均十幾單到一個月後每一個時段秒殺量破十萬的項目。通常在最初若是沒有經驗的狀況下可能會使用數據庫行級鎖的方式下保證商品庫存的扣減操做,可是隨着業務的快速發展秒殺的用戶愈來愈多,這個時候數據庫已經扛不住了,通常都會使用redis的分佈式鎖來控制商品庫存。

同時在查詢的時候也不須要每一次對不一樣的活動查詢都從庫中獲取,由於這裏除了庫存之外其餘的活動商品信息都是固定不變的,以此這裏通常你們會緩存到內存中。

這裏咱們模擬使用享元模式工廠結構,提供活動商品的查詢。活動商品至關於不變的信息,而庫存部分屬於變化的信息。

5、用一坨坨代碼實現

邏輯很簡單,就怕你寫亂。一片片的固定內容和變化內容的查詢組合,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中獲取活動的庫存,而不是從庫中,不然將形成數據不統一的問題。

6、享元模式重構代碼

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

享元模式通常狀況下使用此結構在平時的開發中並不太多,除了一些線程池、數據庫鏈接池外,再就是遊戲場景下的場景渲染。另外這個設計的模式思想是減小內存的使用提高效率,與咱們以前使用的原型模式經過克隆對象的方式生成複雜對象,減小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部分的庫存是一直在變化的,其餘部分是活動信息,是固定的,因此咱們使用享元模式來將這樣的結構進行拆分。

7、總結

  • 關於享元模式的設計能夠着重學習享元工廠的設計,在一些有大量重複對象可複用的場景下,使用此場景在服務端減小接口的調用,在客戶端減小內存的佔用。是這個設計模式的主要應用方式。
  • 另外經過map結構的使用方式也能夠看到,使用一個固定id來存放和獲取對象,是很是關鍵的點。並且不僅是在享元模式中使用,一些其餘工廠模式、適配器模式、組合模式中均可以經過map結構存放服務供外部獲取,減小ifelse的判斷使用。
  • 固然除了這種設計的減小內存的使用優勢外,也有它帶來的缺點,在一些複雜的業務處理場景,很不容易區分出內部和外部狀態,就像咱們活動信息部分與庫存變化部分。若是不能很好的拆分,就會把享元工廠設計的很是混亂,難以維護。

8、推薦閱讀

相關文章
相關標籤/搜索