侧边栏壁纸
博主头像
qingtian博主等级

喜欢是一件细水流长的事,是永不疲惫的双向奔赴~!

  • 累计撰写 94 篇文章
  • 累计创建 42 个标签
  • 累计收到 1 条评论

设计模式之享元模式

qingtian
2020-12-21 / 0 评论 / 0 点赞 / 585 阅读 / 8,499 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2020-12-31,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

设计模式之享元模式

来源自《重学Java设计模式》链接提取码:ytc3

享元模式

image-20201128124323248

享元模式,主要在于共享通用对象,减少内存的使用,提升系统的访问效率。二者部分共享对象通常比较耗费内存或者需要大量查询接口或者使用数据库资源,因此统一抽离作为共享对象使用。

另外享元模式可以分为在服务端和客户端,一般在互联网H5和Web场景下大部分数据都需要服务端进行处理,比如数据库连接池的使用,多线程线程池的使用,除了这些功能外,还有些需要服务端进行包装后的处理下发给客户端,因为服务端需要做享元处理。但在一些游戏场景中,很多都是客户端需要进行渲染地图效果,比如:树木,花草、鱼虫、通过设置不同元素描述使⽤享元公⽤对象,减少内存的占⽤,让客户端的游戏更加流畅。

在享元模式中需要使用到享元工厂来进行管理这部分独立的对象和共享的对象,避免出现线程安全的问题。

案例场景模拟

image-20201212151300768

在这个案例中我们模拟在商品秒杀场景下使⽤享元模式查询优化你是否经历过⼀个商品下单的项⽬从最初的⽇均⼗⼏单到⼀个⽉后每个时段秒杀量破⼗万的项⽬。⼀般在最初如果没有经验的情况下可能会使⽤数据库⾏级锁的⽅式下保证商品库存的扣减操作,但是随着业务的快速发展秒杀的⽤户越来越多,这个时候数据库已经扛不住了,⼀般都会使⽤redis的分布式锁来控制商品库存。

同时在查询的时候也不需要每⼀次对不同的活动查询都从库中获取,因为这⾥除了库存以外其他的活动商品信息都是固定不变的,以此这⾥⼀般⼤家会缓存到内存中。

这⾥我们模拟使⽤享元模式⼯⼚结构,提供活动商品的查询。活动商品相当于不变的信息,⽽库存部分属于变化的信息。

常规实现

代码实现

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的调用,都是此类思想。

享元模式模型结构

image-20201212212117279

  • 以上是我们模拟的查询活动场景的类图结构,左侧构建的是享元工厂,提供固定活动数据的查询,右侧是Redis存放的库存数据。
  • 最终交给活动控制器来查询操作,并提供活动的所有信息和库存。因为库存是变化的,所以我们模拟的RedisUtils中设置了定时任务使用库存。

代码实现

活动信息

public class Activity {

    private Long id;        // 活动ID
    private String name;    // 活动名称
    private String desc;    // 活动描述
    private Date startTime; // 开始时间
    private Date stopTime;  // 结束时间
    private Stock stock;    // 活动库存

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public Date getStartTime() {
        return startTime;
    }

    public void setStartTime(Date startTime) {
        this.startTime = startTime;
    }

    public Date getStopTime() {
        return stopTime;
    }

    public void setStopTime(Date stopTime) {
        this.stopTime = stopTime;
    }

    public Stock getStock() {
        return stock;
    }

    public void setStock(Stock stock) {
        this.stock = stock;
    }
}
  • 这里的对象类比较简单,只是一个活动的基础信息:id、名称、描述、时间和库存。

库存信息

public class Stock {

    private int total; // 库存总量
    private int used;  // 库存已用

    public Stock(int total, int used) {
        this.total = total;
        this.used = used;
    }

    public int getTotal() {
        return total;
    }

    public void setTotal(int total) {
        this.total = total;
    }

    public int getUsed() {
        return used;
    }

    public void setUsed(int used) {
        this.used = used;
    }
}

享元工厂

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中,可以按需选择。

模拟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的操作工具外,还提供了一个定时任务用于模拟库存的使用,这样方便我们在测试时可以观察到库存的变化。

活动控制类

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;
    }

}
  • 在活动控制类中使用了享元工厂获取活动信息,查询后将库存信息再补充上。因为库存信息时变化的,而活动信息时固定不变的。
  • 最终通过统一的控制器就可以把完整包装后的活动信息返回给调用方。

测试验证

编写测试类
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 循环的操作下查询了⼗次活动信息,同时为了保证库存定时任务的变化,加了睡眠操作,实际的开发中不会有这样的睡眠。

result:

21:52:44.123 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:10001 {"desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1607781163696,"stock":{"total":1000,"used":0},"stopTime":1607781163696}
21:52:45.365 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:10001 {"desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1607781163696,"stock":{"total":1000,"used":17},"stopTime":1607781163696}
21:52:46.568 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:10001 {"desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1607781163696,"stock":{"total":1000,"used":29},"stopTime":1607781163696}
21:52:47.768 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:10001 {"desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1607781163696,"stock":{"total":1000,"used":41},"stopTime":1607781163696}
21:52:48.968 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:10001 {"desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1607781163696,"stock":{"total":1000,"used":53},"stopTime":1607781163696}
21:52:50.169 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:10001 {"desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1607781163696,"stock":{"total":1000,"used":65},"stopTime":1607781163696}
21:52:51.369 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:10001 {"desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1607781163696,"stock":{"total":1000,"used":77},"stopTime":1607781163696}
21:52:52.570 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:10001 {"desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1607781163696,"stock":{"total":1000,"used":89},"stopTime":1607781163696}
21:52:53.770 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:10001 {"desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1607781163696,"stock":{"total":1000,"used":101},"stopTime":1607781163696}
21:52:54.971 [main] INFO  org.itstack.demo.design.test.ApiTest - 测试结果:10001 {"desc":"图书优惠券分享激励分享活动第二期","id":10001,"name":"图书嗨乐","startTime":1607781163696,"stock":{"total":1000,"used":113},"stopTime":1607781163696}

  • 可以仔细看下 stock 部分的库存是⼀直在变化的,其他部分是活动信息,是固定的,所以我们使

    ⽤享元模式来将这样的结构进行拆分。

总结

  • 关于享元模式的设计可以着重学习享元工厂的设计,在⼀些有大量重复对象可复用的场景下,使用此场景再服务端减少接口的调用,在客户端减少内存的使用。是这个设计模式的主要应用方式。
  • 另外通过map结构的使用方式可以看到,使用一个固定id来存放和获取对象,是非常关键的点。而且不只是在享元模式中使用,一些其他工厂模式、适配器模式、组合模式中都可以通过map结构存放服务供外部获取,减少ifelse的判断使用。
  • 当然除了这种设计的减少内存的使用有点外,也有它的缺点,在一些复杂的业务处理场景,很不容易区分出内部和外部状态,就像我们活动信息部分和库存变化部分。如果不能很好的拆分,就会把享元工厂设计的非常混乱,难以维护。
0

评论区