璧勬繁鐮佸啘鑰佸惔 (鐮佸啘鑰佸惔)

大家好,欢迎关注极客架构师,极客架构师,专注架构师成长,我是码农老吴。

本期是《架构师基本功之设计模式》的第7期,我将基于享元模式,重构我在京东曾经参与的千万级手机号号池核心系统模块,用以突显享元模式在节省系统资源方面的突出表现。

题外话

说起享元模式(flyweight pattern),我以前还闹过一个笑话,零几年的时候,那个时候我刚开始接触设计模式,看到享元模式,由于粗心(粗心都是借口,主要是语文没学好),把“享元”模式,看成了“亨元”模式,“享”与“亨”就差一横,我还纳闷,亨元,是个啥东东,咱们中国没这个词语啊,不管三七二十一,先记住再说,“亨元”模式,被我说的很溜。最后,在一个好心的,大胆的同事的提醒下,我才意识到自己认错字了。直到现在,我还留下了后遗症,遇到这个模式,我的心总是很紧张,如果不停顿一下,脱口而出,就读错了,如果后面的视频中读错了,请大家见谅。

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

何谓“享元”,这个词中国文化里面也没有,但是如果你理解了这个模式的宗旨之后,不得不佩服翻译《Design Patterns: Elements of Reusable Object-Oriented Software》这本书的那几个作者,翻译得好,翻译的妙。

flyweight,fly是苍蝇,weight是重量,合在一起是苍蝇的重量,老外用来特指(拳击或其他比赛中的)特轻量级选手,次最轻量级选手(体重48至51公斤) 。直译过来,和享元二字也没有任何关系,能翻译成享元,说明翻译人员确实是学编程的,并且领悟了这个模式的宗旨。这个模式的宗旨是啥,我们后面再说。

所谓“享元”,就是共享元对象的缩写。元者,万物之起源也,蕴含于万物之中,被万物共享。

在该模式中,共享元对象,就意味着元对象包含的属性或者方法被多个相关的对象共享,以达到节省系统资源的目的(可以是内存,也可以是CPU)。所以这个名字,翻译得非常传神。

结论先行

基于REIS分析模型,享元模式,包含四种角色,分别是元角色,元工厂角色,细粒度角色,客户方角色,享元模式的宗旨是从细粒度角色中,分离出元角色,由元工厂负责元角色的创建和缓存,实现元角色的共享,以达到节省系统资源的目的。

基本思路

案例介绍:京东千万级手机号号池模块

第一版代码:普通方式(载入988万个手机号)

第二版代码:基于享元模式+代理模式 (载入949万个手机号)

第三版代码:基于享元模式+多态 (载入1186万个手机号)

第四版代码:基于享元模式+guava cache缓存组件 (载入1186万个手机号)

享元模式的定义

REIS模型分析享元模式

享元模式需要思考的点

享元模式通用类图和代码

案例介绍:京东千万级手机号号池

我在前面的分享中,提到过,我曾经在京东的运营商业务部门工作过多年,参与了不少运营商相关的项目,其中,有一个很重要的业务,就是号卡业务,俗称卖手机号。手机号相关业务,有个特点,就是量级大。所谓量变引起质量,在这里就是很好的体现。

量级大体现在两个方面,一个是业务量大,购买的人多,所以系统并发量大;另外一个就是手机号的量也非常大,至少是千万级的,全量缓存所有相关的信息,需要的内存比较大。

以号卡的售卖流程为例,如下图。

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

如上图所示,用户在购买手机号时,有一个很重要的环节,就是选择手机号。页面每次显示一定量的手机号,点击刷新以后,重新显示一批手机号。这个环节,一方面是速度要快,另一方面就是对于同一个用户,显示过的手机号,不能再显示,已经被别的用户选中的,或者已经下单购买的,也不能显示。要达到这些要求,对于千万级的手机号,直接访问数据库,是不可能完成的,数据库会崩溃。所以,我们通常会在后端,基于分布式缓存服务器,建立千万级的手机号号池。缓存服务器,用的不是主流的redis,而是京东基础架构部门基于redis扩展的自己的缓存服务器。

另外,对于手机号,在缓存里面存储时,不仅仅只存储一个手机号码信息,为了查询,过滤方便,还需要存储手机号所属的省份,城市,运营商,售卖状态等信息,如下图所示,是其中的一部分信息。

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

系统挑战

虽然现在内存的价格一直在跌,使用内存的成本也大大降低了,但是也还没有到完全可以忽略成本的地步。即使将来内存的成本达到可以忽略的地步,但是在架构设计时,不浪费内存,不造成内存漏洞,仍然是软件开发人员,或者架构师的应尽职责。所以缓存数据时,我们还是要根据业务需要,对缓存信息进行精心设计,降低对缓存的浪费。

为了案例简单,更为了突显享元模式的价值,我们的案例,先采用HashMap来充当缓存池,接着采用轻量级的guava cache缓存组件,实现一个千万级别的手机号号池,JVM的内存大小限制在2G,主要是防止案例测试时,运行时间过长。一个完整的手机号号池,功能比较复杂。我们这里的手机号号池只需要实现三个功能。

1,添加手机号

2,判断手机号是否存在

3,根据手机号,获取手机号详细信息。

如下图:

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

下面我们看第一版代码,以普通的方式实现。

第一版代码,普通方式 (载入988万个手机号)

UML类图

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

类图中关键接口和类如下:

IMobilePool:手机号号池接口

MobilePool:手机号号池实现类

IMobileInfo:手机号信息接口

MobileInfo:手机号信息实体类

Area:地区信息实体类

AreaJson:地区信息测试数据

TestBase:测试类的基类

TestMobilePool:测试类

IMobilePool:手机号号池接口

package com.geekarchitect.patterns.demo0301;

/**
 * 手机号号池接口
 *
 * @author 极客架构师@吴念
 * @createTime 2022/4/29
 */
public interface IMobilePool {


    /**
     * 判断手机号是否存在
     *
     * @param mobile
     * @return
     */
    boolean exists(String mobile);

    /**
     * 添加新号码
     *
     * @author: 极客架构师@吴念
     * @date: 2022/4/30
     * @param: [mobileInfo]
     * @return: void
     */
    void add(IMobileInfo mobileInfo);

    /**
     * @author: 极客架构师@吴念
     * @date: 2022/4/30
     * @param: [mobile]
     * @return: boolean
     */
    IMobileInfo get(String mobile);

}

MobilePool:手机号号池实现类

package com.geekarchitect.patterns.demo0301;

import java.util.HashMap;
import java.util.Map;

/**
 * 手机号号池实现类
 *
 * @author 极客架构师@吴念
 * @createTime 2022/4/29
 */
public class MobilePool implements IMobilePool {
    private static final Map<String, IMobileInfo> mobileInfoMap = new HashMap<>();
    private static final MobilePool mobilePool = new MobilePool();

    private MobilePool() {
    }

    public static MobilePool getInstance() {
        return mobilePool;
    }

    @Override
    public boolean exists(String mobile) {
        return mobileInfoMap.containsKey(mobile);
    }

    @Override
    public void add(IMobileInfo mobileInfo) {
        mobileInfoMap.put(mobileInfo.getMobile(), mobileInfo);
    }

    @Override
    public IMobileInfo get(String mobile) {
        return mobileInfoMap.get(mobile);
    }

}

IMobileInfo:手机号信息接口

package com.geekarchitect.patterns.demo0301;

import java.util.Date;

/**
 * 手机号详细信息接口
 *
 * @author 极客架构师@吴念
 * @createTime 2022/5/3
 */
public interface IMobileInfo {
    long getId();

    void setId(long id);

    String getMobile();

    void setMobile(String mobile);

    long getProvinceCode();

    void setProvinceCode(long provinceCode);

    String getProvinceName();

    void setProvinceName(String provinceName);

    long getProvinceId();

    void setProvinceId(long provinceId);

    long getCityCode();

    void setCityCode(long cityCode);

    String getCityName();

    void setCityName(String cityName);

    long getCityId();

    void setCityId(long cityId);

    Date getAddDate();

    void setAddDate(Date addDate);
}

MobileInfo:手机号信息实体类

package com.geekarchitect.patterns.demo0301;

import lombok.Data;

import java.util.Date;

/**
 * 手机号详细信息
 *
 * @author 极客架构师@吴念
 * @createTime 2022/4/29
 */
@Data
public class MobileInfo implements IMobileInfo {
    /**
     * 编号
     */
    private long id;
    /**
     * 手机号码
     */
    private String mobile;
    /**
     * 省份编码
     */
    private long provinceCode;
    /**
     * 省份名称
     */
    private String provinceName;
    /**
     * 省份序号
     */
    private long provinceId;

    /**
     * 城市编号
     */
    private long cityCode;

    /**
     * 城市名称
     */
    private String cityName;

    /**
     * 城市序号(不全局唯一)
     */
    private long cityId;

    /**
     * 添加日期
     */
    private Date addDate;


}

Area:地区信息实体类

package com.geekarchitect.patterns.demo0301;

import lombok.Data;

/**
 * @author 极客架构师@吴念
 * @createTime 2022/5/3
 */
@Data
public class Area {
    private long cityCode;
    private String cityName;
    private long cityId;
    private long provinceCode;
    private String provinceName;
    private long provinceId;
}

AreaJson:地区信息测试数据

只包含地区信息测试数据,这里就不展示了,github上有完整的。

TestBase:测试类的基类

package com.geekarchitect.patterns.demo0301;

import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

/**
 * 测试基类
 * @author 极客架构师@吴念
 * @createTime 2022/5/6
 */
public abstract class TestBase {
    private static final Logger LOG = LoggerFactory.getLogger(TestBase.class);

    public TestBase() {
    }
    /**
     * 生成手机号
     * @param id
     * @param mobile
     * @return
     */
    public abstract IMobileInfo generateMobile(Long id, String mobile);

    /**
     * 返回版本信息
     *
     * @return
     */
    public abstract String getVersion();

    /**
     * 载入手机号:这里使用了模板方法模式
     *
     * @param
     */
    public void loadMobile(int maxNumber) {
        LOG.info(getVersion());
        long startMobile = 18700000001L;
        long mobileId = 1L;
        if (maxNumber > 0) {
            LOG.info("预计导入{}个手机号码", maxNumber);
        } else {
            LOG.info("预计导入海量手机号,直至内存溢出");
        }
        while (true) {
            IMobileInfo mobileInfo = generateMobile(mobileId, String.valueOf(startMobile));
            MobilePool.getInstance().add(mobileInfo);
            if (mobileId % 1000 == 0) {
                LOG.info("手机信息{}", mobileInfo.toString());
                LOG.info("已载入{}个手机号", mobileId);
            }
            if (maxNumber > 0 && mobileId >= maxNumber) {
                break;
            }
            startMobile++;
            mobileId++;
        }
        LOG.info("号码导入完成,共导入{}个号码。", maxNumber);
    }
}

TestMobilePool:测试类

package com.geekarchitect.patterns.demo0301;

import java.util.Date;
import java.util.Random;

/**
 * 第一版代码:普通方式 测试类
 *
 * @author 极客架构师@吴念
 * @createTime 2022/4/29
 */
public class TestMobilePool extends TestBase {
    public TestMobilePool() {
    }

    public static void main(String[] args) {
        int maxNumber = 10000;
        if (null != args && args.length > 0) {
            maxNumber = Integer.parseInt(args[0]);
        }
        //maxNumber=0;
        TestMobilePool testMobilePool = new TestMobilePool();
        testMobilePool.loadMobile(maxNumber);
    }
    @Override
    public IMobileInfo generateMobile(Long id, String mobile) {
        IMobileInfo mobileInfo = new MobileInfo();
        mobileInfo.setId(id);
        mobileInfo.setMobile(mobile);
        mobileInfo.setAddDate(new Date());
        //初始化省份及城市信息
        Area area = AreaJson.randomArea();
        mobileInfo.setProvinceId(area.getProvinceId());
        mobileInfo.setProvinceName(area.getProvinceName());
        mobileInfo.setProvinceCode(area.getProvinceCode());

        mobileInfo.setCityId(area.getCityId());
        mobileInfo.setCityCode(area.getCityCode());
        mobileInfo.setCityName(area.getCityName());
        return mobileInfo;
    }

    @Override
    public String getVersion() {
        return "第一版代码:普通方式";
    }
}

运行结果(本机测试-10000个手机号)

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

运行结果(阿里云服务器测试-已载入9886000个手机号,内存溢出)

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

头脑风暴

这一版,我们实现了普通方式的手机号号池,我们需要从中分析出哪些地方需要优化。其中非常明显的,对内存浪费比较大的,就是手机号详细信息中,地区信息,重复率比较高,如果都直接存在内存中,造成了内存的浪费,下面我们采用享元模式,对这方面进行优化。

第二版代码,基于享元模式+代理模式 (载入949万个手机号)

UML类图

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

新增接口和类如下:

FlyweightFactory:元工厂类

MobileInfoProxy:手机号信息代理类

TestMobilePoolV2:测试类第二版

FlyweightFactory:元工厂类

采用了单例模式,确保元工厂类只创建一个对象。

采用HashMap作为地区信息缓存池。

package com.geekarchitect.patterns.demo0302;

import com.geekarchitect.patterns.demo0301.Area;
import com.geekarchitect.patterns.demo0301.AreaJson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

/**
 * 元工厂
 *
 * @author 极客架构师@吴念
 * @createTime 2022/5/3
 */
public class FlyweightFactory {
    private static final Logger LOG = LoggerFactory.getLogger(FlyweightFactory.class);
    private static final FlyweightFactory flyweightFactory = new FlyweightFactory();
    private static final Map<Long, Area> AREA_MAP = new HashMap<>();

    private FlyweightFactory() {
    }

    public static FlyweightFactory getInstance() {
        return flyweightFactory;
    }

    public Area getArea(Area area) {
        if (AREA_MAP.containsKey(area.getCityCode())) {
            return AREA_MAP.get(area.getCityCode());
        }
        area= AreaJson.getAreaByCityCode(area.getCityCode());
        LOG.info("城市{}首次访问,加入缓存", area);
        AREA_MAP.put(area.getCityCode(), area);
        return area;
    }

    public Area getArea(Long cityCode) {
        Area area=new Area();
        area.setCityCode(cityCode);
        return getArea(area);
    }

}

MobileInfoProxy:手机号信息代理类

该类采用了代理模式,确保代理类MobileInfoProxy与被代理类MobileInfo保持一致的对外接口。

但是它里面并没有存储地区信息,而是在用户访问时,才通过工厂类从缓存池中查询出相应信息。实现了地区信息的共享。节省了内存的占用。

package com.geekarchitect.patterns.demo0302;

import com.geekarchitect.patterns.demo0301.MobileInfo;

/**
 * 手机详细信息代理类
 *
 * @author 极客架构师@吴念
 * @createTime 2022/5/3
 */
public class MobileInfoProxy extends MobileInfo {
    private final long areaCode;

    public MobileInfoProxy(long areaCode) {
        this.areaCode = areaCode;
    }

    @Override
    public long getProvinceCode() {
        return FlyweightFactory.getInstance().getArea(areaCode).getProvinceCode();
    }

    @Override
    public String getProvinceName() {
        return FlyweightFactory.getInstance().getArea(areaCode).getProvinceName();
    }

    @Override
    public long getProvinceId() {
        return FlyweightFactory.getInstance().getArea(areaCode).getProvinceId();
    }

    @Override
    public long getCityCode() {
        return FlyweightFactory.getInstance().getArea(areaCode).getCityCode();
    }

    @Override
    public String getCityName() {
        return FlyweightFactory.getInstance().getArea(areaCode).getCityName();
    }

    @Override
    public long getCityId() {
        return FlyweightFactory.getInstance().getArea(areaCode).getCityId();
    }
}

TestMobilePoolV2:测试类第二版

package com.geekarchitect.patterns.demo0302;

import com.geekarchitect.patterns.demo0301.Area;
import com.geekarchitect.patterns.demo0301.AreaJson;
import com.geekarchitect.patterns.demo0301.IMobileInfo;
import com.geekarchitect.patterns.demo0301.TestBase;

import java.util.Date;
import java.util.Random;

/**
 * 第二版代码:基于享元模式 测试类
 *
 * @author 极客架构师@吴念
 * @createTime 2022/4/29
 */
public class TestMobilePoolV2 extends TestBase {
    public TestMobilePoolV2() {
    }

    public static void main(String[] args) {
        int maxNumber = 10000;
        if (null != args && args.length > 0) {
            maxNumber = Integer.parseInt(args[0]);
        }
        //maxNumber=0;
        TestMobilePoolV2 testMobilePool = new TestMobilePoolV2();
        testMobilePool.loadMobile(maxNumber);
    }
    @Override
    public IMobileInfo generateMobile(Long id, String mobile) {
        //初始化省份及城市信息
        IMobileInfo mobileInfo = new MobileInfoProxy(AreaJson.randomArea().getCityCode());
        mobileInfo.setId(id);
        mobileInfo.setMobile(mobile);
        mobileInfo.setAddDate(new Date());
        return mobileInfo;
    }

    @Override
    public String getVersion() {
        return "第二版代码:基于享元模式+代理模式";
    }

}

运行结果(本机测试-10000个手机号)

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

运行结果(阿里云服务器测试-已载入9491000个手机号,内存溢出)

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

头脑风暴

在这版代码中,我们引入了工厂类和代理类,实现了地区信息的共享,理论上相同的内存,号码池应该可以保持更多的手机号。但是,上面在阿里云服务器,运行的结果出乎我们的意料,在2G内存下,这种方案并没有增加手机号的数量,反而少了40万左右的手机号,我们忙活了半天,好像失败了。出现这种情况的原因如下:

第一个原因:

由于我们在缓存池中,存放的是手机信息代理类,也就是MobileInfoProxy,这个类采用了继承,导致这个类在内存中,比MobileInfo类,需要占用更多的空间,我在JVM内存结构的相关分享中,会给大家分享这部分内容。

第二个原因:

就是地区信息比较简单,节省的内存,还没有继承增加的内存大。

以上两个原因,导致我们的手机号号池,并没有新增更多的手机号。但是这并不能完全否认这种方案的可行性,这种方案,依然是首先方案。只是在特定情景下,得不偿失。所以,实践中,我们一定要进行方案验证,不能想当然。

第三版代码,基于享元模式+多态 (载入1186万个手机号)

UML类图

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

新增接口和类:

MobileInfoV2:手机号码信息第二版

TestMobilePoolV3:测试类第三版

MobileInfoV2:手机号码信息第二版

这个类,与MobileInfo类执行同样的接口IMobileInfo。但是并没有相关地区信息属性,除了cityCode,其他地区信息,都是通过工厂类,从缓存池中获取。所以它的内存占用情况,肯定比MobileInfo要低。

package com.geekarchitect.patterns.demo0303;

import com.geekarchitect.patterns.demo0301.IMobileInfo;
import com.geekarchitect.patterns.demo0302.FlyweightFactory;
import lombok.Data;

import java.util.Date;

/**
 * 手机号详细信息第二版
 *
 * @author 极客架构师@吴念
 * @createTime 2022/4/29
 */
@Data
public class MobileInfoV2 implements IMobileInfo {
    /**
     * 编号
     */
    private long id;
    /**
     * 手机号码
     */
    private String mobile;

    /**
     * 城市编号
     */
    private long cityCode;
    /**
     * 添加日期
     */
    private Date addDate;

    public MobileInfoV2(long cityCode) {
        this.cityCode = cityCode;
    }

    @Override
    public long getProvinceCode() {
        return FlyweightFactory.getInstance().getArea(cityCode).getProvinceCode();
    }

    @Override
    public void setProvinceCode(long provinceCode) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String getProvinceName() {
        return FlyweightFactory.getInstance().getArea(cityCode).getProvinceName();
    }

    @Override
    public void setProvinceName(String provinceName) {
        throw new UnsupportedOperationException();
    }

    @Override
    public long getProvinceId() {
        return FlyweightFactory.getInstance().getArea(cityCode).getProvinceId();
    }

    @Override
    public void setProvinceId(long provinceId) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String getCityName() {
        return FlyweightFactory.getInstance().getArea(cityCode).getCityName();
    }

    @Override
    public void setCityName(String cityName) {
        throw new UnsupportedOperationException();
    }

    @Override
    public long getCityId() {
        return FlyweightFactory.getInstance().getArea(cityCode).getCityId();
    }

    @Override
    public void setCityId(long cityId) {
        throw new UnsupportedOperationException();
    }
}

TestMobilePoolV3:测试类第三版

package com.geekarchitect.patterns.demo0303;

import com.geekarchitect.patterns.demo0301.*;
import com.geekarchitect.patterns.demo0302.FlyweightFactory;

import java.util.Date;
import java.util.Random;

/**
 * 第二版代码:基于享元模式 测试类
 *
 * @author 极客架构师@吴念
 * @createTime 2022/4/29
 */
public class TestMobilePoolV3 extends TestBase {

    public TestMobilePoolV3() {
    }

    public static void main(String[] args) {
        int maxNumber = 10000;
        if (null != args && args.length > 0) {
            maxNumber = Integer.parseInt(args[0]);
        }
        //maxNumber=0;
        TestMobilePoolV3 testMobilePool = new TestMobilePoolV3();
        testMobilePool.loadMobile(maxNumber);
    }
    @Override
    public IMobileInfo generateMobile(Long id, String mobile) {
        //初始化省份及城市信息
        IMobileInfo mobileInfo = new MobileInfoV2(AreaJson.randomArea().getCityCode());
        mobileInfo.setId(id);
        mobileInfo.setMobile(mobile);
        mobileInfo.setAddDate(new Date());
        return mobileInfo;
    }
    @Override
    public String getVersion() {
        return "第三版代码:基于享元模式+多态";
    }

}

运行结果(本机测试-10000个手机号)

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

运行结果(阿里云服务器测试-已载入11864000个手机号,内存溢出)

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

头脑风暴

这版代码,是从第三步的基础上改造而来,没有采用代理模式,而是建立新的实体类,与原来的实体类执行同样的接口。确实降低了内存占用,从上面的运行结果看,2G的内存,多存储了大约200万个手机号,达到了1186万的手机号,也达到了咱们期望的,千万级手机号号池的要求。

这个方法虽然好,但是有一个弊端,导致这个方案,有可能在有些场景下不能使用,最大的问题,就是原来的实体类,如果没有执行执行接口,而且你没有修改这个实体类的权限,就无法使用这个方案,所以,面向接口很重要,任何情况下,都要优先面向接口编程,为后期的扩展留有余地。

第四版代码,享元模式+guava cache(载入1186万个手机号)

UML类图

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

接口和类如下:

FlyweightFactoryV2:工厂类,采用guava cache作为缓存组件

MobileInfoV3:手机号信息第三版

TestMobilePoolV4:测试类

POM文件

导入guava cache


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>DesignPatterns</artifactId>
        <groupId>com.geekarchitect.patterns</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>flyweight-pattern</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>23.0</version>
        </dependency>
    </dependencies>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

</project>

FlyweightFactoryV2:工厂类,采用guava cache作为缓存组件

package com.geekarchitect.patterns.demo0304;

import com.geekarchitect.patterns.demo0301.Area;
import com.geekarchitect.patterns.demo0301.AreaJson;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

/**
 * 元对象工厂-guava
 *
 * @author 极客架构师@吴念
 * @createTime 2022/5/3
 */
public class FlyweightFactoryV2 {
    private static final Logger LOG = LoggerFactory.getLogger(FlyweightFactoryV2.class);
    private static final FlyweightFactoryV2 flyweightFactory = new FlyweightFactoryV2();
    private static final Cache<Long, Area> AREA_CACHE;

    static {
        AREA_CACHE = CacheBuilder.newBuilder().build();
    }

    private FlyweightFactoryV2() {
    }

    public static FlyweightFactoryV2 getInstance() {
        return flyweightFactory;
    }

    public Area getArea(Area area) {
        Area cachedArea = null;
        try {
            cachedArea = AREA_CACHE.get(Long.valueOf(area.getCityCode()), new Callable<Area>() {

                @Override
                public Area call() throws Exception {
                    Area cachedCity= AreaJson.getAreaByCityCode(area.getCityCode());
                    LOG.info("城市{}首次访问,加入缓存", cachedCity);
                    return cachedCity;
                }
            });
        } catch (ExecutionException e) {
            LOG.error(e.getMessage(), e);
        }
        return cachedArea;
    }

    public Area getArea(long cityCode) {
        Area area=new Area();
        area.setCityCode(cityCode);
        return getArea(area);
    }

}

MobileInfoV3:手机号信息第三版

package com.geekarchitect.patterns.demo0304;

import com.geekarchitect.patterns.demo0301.IMobileInfo;
import com.geekarchitect.patterns.demo0304.FlyweightFactoryV2;
import lombok.Data;

import java.util.Date;

/**
 * 手机号详细信息第三版
 *
 * @author 极客架构师@吴念
 * @createTime 2022/4/29
 */
@Data
public class MobileInfoV3 implements IMobileInfo {
    /**
     * 编号
     */
    private long id;
    /**
     * 手机号码
     */
    private String mobile;

    /**
     * 城市编号
     */
    private long cityCode;
    /**
     * 添加日期
     */
    private Date addDate;

    public MobileInfoV3(long cityCode) {
        this.cityCode = cityCode;
    }

    @Override
    public long getProvinceCode() {
        return FlyweightFactoryV2.getInstance().getArea(cityCode).getProvinceCode();
    }

    @Override
    public void setProvinceCode(long provinceCode) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String getProvinceName() {
        return FlyweightFactoryV2.getInstance().getArea(cityCode).getProvinceName();
    }

    @Override
    public void setProvinceName(String provinceName) {
        throw new UnsupportedOperationException();
    }

    @Override
    public long getProvinceId() {
        return FlyweightFactoryV2.getInstance().getArea(cityCode).getProvinceId();
    }

    @Override
    public void setProvinceId(long provinceId) {
        throw new UnsupportedOperationException();
    }

    @Override
    public String getCityName() {
        return FlyweightFactoryV2.getInstance().getArea(cityCode).getCityName();
    }

    @Override
    public void setCityName(String cityName) {
        throw new UnsupportedOperationException();
    }

    @Override
    public long getCityId() {
        return FlyweightFactoryV2.getInstance().getArea(cityCode).getCityId();
    }

    @Override
    public void setCityId(long cityId) {
        throw new UnsupportedOperationException();
    }
}

TestMobilePoolV4:测试类

package com.geekarchitect.patterns.demo0304;

import com.geekarchitect.patterns.demo0301.AreaJson;
import com.geekarchitect.patterns.demo0301.IMobileInfo;
import com.geekarchitect.patterns.demo0301.TestBase;

import java.util.Date;

/**
 * 第四版代码:基于享元模式+guava cache 测试类
 *
 * @author 极客架构师@吴念
 * @createTime 2022/4/29
 */
public class TestMobilePoolV4 extends TestBase {

    public TestMobilePoolV4() {
    }

    public static void main(String[] args) {
        int maxNumber = 10000;
        if (null != args && args.length > 0) {
            maxNumber = Integer.parseInt(args[0]);
        }
        //maxNumber=0;
        TestMobilePoolV4 testMobilePool = new TestMobilePoolV4();
        testMobilePool.loadMobile(maxNumber);
    }

    @Override
    public IMobileInfo generateMobile(Long id, String mobile) {
        //初始化省份及城市信息
        IMobileInfo mobileInfo = new MobileInfoV3(AreaJson.randomArea().getCityCode());
        mobileInfo.setId(id);
        mobileInfo.setMobile(mobile);
        mobileInfo.setAddDate(new Date());
        return mobileInfo;
    }

    @Override
    public String getVersion() {
        return "第四版代码:基于享元模式+guava cache";
    }

}

运行结果(本机测试-10000个手机号)

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

运行结果(阿里云服务器测试-已载入11864000个手机号,内存溢出)

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

头脑风暴

这版代码,效果和第三版代码基本一致,这里只是展示一个guava cache组件的使用。如果使用了这个组件,就可以使用它的其他附加功能,比如过期管理等,在实战项目是首选方案。

下面我们开始享元模式定义的讲解。

享元模式(Flyweight Pattern)定义

Use sharing to support large numbers of fine-grained objects efficiently.

—— Gof《Design Patterns: Elements of Reusable Object-Oriented Software》

中文解释如下:

运用共享技术,有效地支持大量细粒度的对象。

这个定义里面我们要注意的关键词有两个,共享技术,细粒度对象

细粒度对象

我们先看细粒度对象,它的关键点在于细,细,往往意味着量大(虽然不是绝对的)。如果一个系统中,某个对象的量级很多,往往说明这个对象,描述的是比较细节的信息。因为量大,占用的系统资源就比较多。容易形成系统瓶颈,所以需要考虑如何降低系统资源开销,提高使用效率。

系统资源占用大,在面向对象的编程中,可以从“内存”和“cpu”两个角度看。

从内存角度看,如果某个类,在JVM中,要生成大量的对象,这些对象又包含了不少重复的属性值,就会造成内存的浪费比较大。

从CPU角度,如果某个类,在JVM中,要不断生成大量短生命周期的对象,也就是这些对象创建后,只使用一小段时间,就需要被销毁,同一时刻,堆里面存在的对象量级可能并不大。但是因为有大量的对象要不断的创建和销毁,对CPU等系统资源占用是非常高的。因为,在JVM中,创建一个对象,属于重量级操作,耗费的系统资源比较大,这也是线程池,数据库连接池比较受欢迎的原因。

而节省系统资源的一个途径,就是看这些大量存在的细粒度对象,是否可以分离出可以共享的元对象。如果可以,通过下面要说的共享技术,就可以大大降低细粒度对象的资源占用情况。

共享技术

共享谁?

在享元模式的语境下,共享的就是从细粒度对象中,抽象出来的,可以共享的元对象。

如何共享?

共享对象,在面向对象中,通常使用“池”技术,也就是对象缓存池,具体用什么技术实现,可以有很多选择,轻量级的,可以使用集合框架,List,Set,Map这些都可以,重量级的可以选择开源的,或者商业的缓存池组件,或者分布式缓存服务器,如redis等,我们这里使用的是开源的guava cache缓存组件。

总结一下,享元模式, 就是从细粒度对象中,分离出元对象,然后通过缓存池,实现元对象的共享,以达到节省系统资源的目的。

下面我们用REIS模型,对享元模式进行分析,大家就会理解的更透彻一些。

REIS模型分析享元模式

REIS模型是我总结的分析设计模式的一种方*论法**,主要包括场景(scene),角色(role),交互(interaction),效果(effect)四个要素,后面我会专门分享一下这套方*论法**。

场景(Scene)

场景,也就是我们在什么情况下,遇到了什么问题,需要使用某个设计模式。

对于享元模式,当出现以下情况时,可能需要使用享元模式。

  1. 从业务角度看:

这个模式,主要目的是降低系统资源的,所以,一般不从业务角度看,而是从技术角度看。

  1. 从技术角度看:

当一个系统中,需要把某个类对象(细粒度对象),大量的加载到内存中使用时,而这类对象内部,有重复的数据出现时(可以分离为元对象),为了降低内存浪费,可以考虑享元模式;另外,如果某个类的对象,虽然同一时刻,内存中存在的数量可能并不大,但是它的生命周期比较短,需要不断的创建和销毁,造成大量的CPU资源浪费,可以考虑使用享元模式。

角色(Role)

角色,一般为设计模式出现的类,或者对象。每种角色有自己的职责。

在享元模式中,包含三种角色,元角色,元工厂角色,细粒度角色,客户方角色。

元角色(flyweight role):元角色,就是从定义里面所说的细粒度角色中提取出来的角色,它的职责如下。

  1. 提供元属性:元属性,也称为内部状态,是从细粒度角色里面抽取出来的,可以共享使用的属性。
  2. 提供元方法:元方法,有可能是从细粒度角色里面抽取出来的,也有可能是元角色自身的方法。元方法的参数,通常是外部状态,这样元对象就拥有了内部状态和外部状态。

元工厂角色(flyweight factory role):元工厂角色,用来生产元对象的工厂类,它的职责。

  1. 提供缓存池:元工厂,需要实现元对象的共享,通常是使用缓存池来实现的,这个缓存池,可以是工厂类自己管理,也可以用外部的缓存池,但一般都是由工厂负责管理。
  2. 提供工厂方法:细粒度角色,或者客户方角色,需要元对象时,不能自己new一个,这样就无法共享了,而需要通过元工厂里面的工厂方法来获取。工厂方法里面的逻辑一般表现为,当这个元角色还不存在时,new一个或者从外部数据库加载一个,放入缓存池,反之,则直接从缓存池取出,返回给调用方。

细粒度角色( fine-grained role):细粒度角色,就是系统中,数量比较多,需要进行资源优化的对象,可以从它里面,抽取出元对象,它属于被改造的对象,它的职责是:

1,分权:原来由它提供的属性,或者方法,被元对象提供了,它就不需要再提供了。这样才能体现元对象的价值。

2,提供外部状态:细粒度对象里面的状态,被元对象拿走的,属于内部状态,留下了的属于外部状态。

客户方角色(client role):这个角色,一般情况下没什么意义,但是,上面的三种角色,在交互时,一般情况是,细粒度角色,通过元工厂角色,获取共享的元角色,然后统一对外提供服务,也就是以细粒度对象为主角。

但是,实际项目中,还存在另外一种交互方式。由客户方角色,从细粒度角色获取外部状态,又通过元工厂角色,获取元对象,然后把外部状态,传输给元对象里面的方法,进行业务处理。也就是以元角色为主角。

在上面的第二版到第四版代码,基于享元模式实现的手机号号池中。

接口,类对应的角色如下

Area:元角色

FlyweightFactory:元工厂角色

MobileInfo:细粒度角色

TestMobilePool测试类:可以看成客户方角色

交互(interaction)

交互,是指设计模式中,各种角色是如何交互的,一般用UML中的序列图,活动图来表示。简单的说就是角色之间是如何配合,完成设计模式的使命的。

享元模式,交互有点特别。

一般情况下,我们是从细粒度角色中,抽取出元角色,所以,细粒度角色应该是主角。但是,从享元模式的起源看,却是以元角色为主角。这两种情况是有所侧重的。

以细粒度角色为主角,侧重的是节省内存资源

细粒度角色在系统中本来量级就很大,从它里面分离出元角色后,并没有降低细粒度角色的对象数量,但是由于它们共享元角色,所以内存占用将会大大降低。

以元角色为主角,侧重的是节省CPU资源

如果以元角色为主角,由于元角色存放在缓存池中,所以数量非常有限,但是元角色,缺少细粒度角色中存放的外部状态信息,所以需要客户端提供给它。这种情况下,客户方角色会反复使用这些元对象。这样就避免了创建和销毁大量的对象,进而节省了大量系统资源,特别是CPU资源。线程池和数据库连接池,都是基于这种模式。

相关交互图,在后面的通用类图和代码中查看。

综上所述,基于REIS分析模型,享元模式,包含三种角色,分别是元角色,元工厂角色,细粒度角色,客户方角色,享元模式的宗旨是从细粒度角色中,分离出元角色,由元工厂负责元角色的创建和缓存,实现元角色的共享,以达到降低系统资源的目的。

效果(effect)

效果,使用该设计模式之后,达到了什么效果,有何意义,当然,也可以说说它的缺点,或者风险。

从我们前面的案例可以看出,享元模式达到了以下效果。

积极效果

  1. 降低了内存资源的浪费:当以细粒度对象为主角,共享元角色,可以有效降低内存浪费。
  2. 降低了CPU资源的浪费:当以元角色为主角,重复使用这些对象,可以减少创建和销毁对象的数量,降低了系统资源,特别是CPU资源的浪费。
  3. 抽象的粒度更细了:通过面向对象的设计思路,从细粒度角色中,抽象出了新的,可以复用的元角色。

消极效果

  1. 项目结构复杂了:增加了元工厂角色和缓存池,这些都是比较重量级的对象,维护需要花费一定的成本。
  2. 元角色的生命周期容器失控:由于元角色,被多个细粒度对象共享使用,导致元角色自身的生命周期不容易控制,容易造成一定的内存漏洞,但是往往可以忽略不计。

享元模式需要思考的点

缓存池的方案

缓存池如何实现,这个问题可以脱离享元模式而独立考虑,常见的解决方案如下:

轻量级的:Java语言里面的集合,List,Set,Map等及常用缓存组件guava cache,Ehcache等

重量级的分布式缓存:redis,memcache等。

如果还满足不了需求,就只能自己造轮子了。

元角色的生命周期问题

元角色,只要被使用一次,就被元工厂加载到内存中,如果不明确删除,就会一直存在,即使使用它的所有细粒度对象都不在了,它也一直存在,某种程度上,这是一种浪费。这个问题应该是缓存池要考虑的一个问题,属于缓存过期问题。如果我们使用轻量级的解决缓存池方案,这个问题,我们就需要自己解决了。一般情况下,我们会不在乎这一点点内存的。如果确实需要解决,可以提供一种重新加载的机制。这些都属于缓存池的设计问题,如果大家感兴趣,我们后面可以分享。

细粒度角色的改造问题

当我们从细粒度角色中,分离出元对象之后,原来的细粒度角色,当客户方,需要访问细粒度对象里面被分离出去的元属性。通常有以下解决方案。

代理方式:新建代理类,继承原来的细粒度角色对应的类,覆盖相关方法。这个我们前面的案例里面已经演示过。

多态方式:新建代理类,与元细粒度角色执行同样的接口。代替细粒度角色的相关功能。

喧宾夺主的方式:这个方案比较有趣,在原著里面也提到过,就是在元角色里面定义相关方法,客户方本来要访问细粒度角色的方法,转而访问元角色里面的方法,如果元角色里面缺少其他的数据,需要同时把细粒度对象里面的外部状态传递过去,有点喧宾夺主的味道。

通用类图和代码

下面我们看看享元模式的通用UML类图和代码。大家注意,我讲的和普通设计模式书籍上的,是不太一样的,看仔细了。

UML类图

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

接口及类:

FineGrainedRole:细粒度角色

FineGrainedRoleProxy:细粒度角色代理类

FlyweightRole:元角色

UnsharedFlyweightRole:非共享的元角色

FlyweightFactoryRole:元工厂角色

ClientRole:客户方角色

TestFlyweight:测试类

UML序列图-细粒度对象为主角

以细粒度对象为主角,一般是为了节省内存。

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

UML序列图-元对象为主角

以元对象为主角,一般是为了节省CPU资源

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

FineGrainedRole:细粒度角色

package com.geekarchitect.patterns.demo0305;

import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 细粒度角色:原本包含了所有状态,抽取了元角色之后,它只负责外部状态。
 *
 * @author 极客架构师@吴念
 * @createTime 2022/5/9
 */
@Data
public class FineGrainedRole {
    private static final Logger LOG = LoggerFactory.getLogger(FineGrainedRole.class);
    private String extrinsicState1;
    private String extrinsicState2;
    private String intrinsicState1;
    private String intrinsicState2;

    public void server() {
        LOG.info("内部状态:{} 外部状态:{}", getIntrinsicState1() + getIntrinsicState2(), getExtrinsicState1() + getExtrinsicState2());
    }
}

FineGrainedRoleProxy:细粒度角色代理类

package com.geekarchitect.patterns.demo0305;

import lombok.Data;

/**
 * 细粒度角色代理类:
 * 继承细粒度角色
 * 保持元角色在缓存中的key
 * 内部状态,一般不提供set方法
 * @author 极客架构师@吴念
 * @createTime 2022/5/9
 */
@Data
public class FineGrainedRoleProxy extends FineGrainedRole {
    /**
     * 元角色缓存key
     */
    private String flyweightKey;

    public FineGrainedRoleProxy(String flyweightKey) {
        this.flyweightKey = flyweightKey;
    }

    @Override
    public String getIntrinsicState1() {
        return FlyweightFactoryRole.getInstance().getFlyweightRole(flyweightKey).getIntrinsicState1();
    }

    @Override
    public String getIntrinsicState2() {
        return FlyweightFactoryRole.getInstance().getFlyweightRole(flyweightKey).getIntrinsicState2();
    }
}

FlyweightRole:元角色

package com.geekarchitect.patterns.demo0305;

import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 元角色:只保持内部状态信息
 *
 * @author 极客架构师@吴念
 * @createTime 2022/5/7
 */
@Data
public class FlyweightRole {
    private static final Logger LOG = LoggerFactory.getLogger(FlyweightRole.class);
    private String intrinsicState1;
    private String intrinsicState2;

    public void server(FineGrainedRole fineGrainedRole) {
        LOG.info("内部状态:{} 外部状态:{}", intrinsicState1 + intrinsicState2, fineGrainedRole.getExtrinsicState1() + fineGrainedRole.getExtrinsicState2());
    }
}

UnsharedFlyweightRole:非共享的元角色

package com.geekarchitect.patterns.demo0305;

import lombok.Data;

/**
 * 非共享的元角色:它里面包含所有状态,所以无法共享,与细粒度对象fineGrainedRole作用完全一样,不同之处是,它强调了非共享性。
 * 一般很少使用。
 *
 * @author 极客架构师@吴念
 * @createTime 2022/5/7
 */
@Data
public class UnsharedFlyweightRole {
    private String intrinsicState1;
    private String intrinsicState2;
    private FineGrainedRole fineGrainedRole;

    public UnsharedFlyweightRole(FineGrainedRole fineGrainedRole) {
        this.fineGrainedRole = fineGrainedRole;
    }

    public void server() {
    }
}

FlyweightFactoryRole:元工厂角色

package com.geekarchitect.patterns.demo0305;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

/**
 * 元工厂角色:
 * 因为它里面管理了缓存池,所以一般采用单例模式
 * 获取元角色的方法(getFlyweightRole)是重点,它里面通常包含初始化缓存池对象的代码。
 *
 * @author 极客架构师@吴念
 * @createTime 2022/5/7
 */
public class FlyweightFactoryRole {
    private static final Logger LOG = LoggerFactory.getLogger(FlyweightFactoryRole.class);
    private static final FlyweightFactoryRole flyweightFactoryRole = new FlyweightFactoryRole();
    private static final Map<String, FlyweightRole> flyweightRoleMap = new HashMap<>();

    private FlyweightFactoryRole() {
    }

    public static FlyweightFactoryRole getInstance() {
        return flyweightFactoryRole;
    }

    public FlyweightRole getFlyweightRole(String flyweightKey) {
        LOG.info("调用工厂方法getFlyweightRole");
        FlyweightRole flyweightRole = null;
        if (!flyweightRoleMap.containsKey(flyweightKey)) {
            flyweightRole = new FlyweightRole();
            //初始化操作
            flyweightRole.setIntrinsicState1("内部状态1");
            flyweightRole.setIntrinsicState2("内部状态2");
            LOG.info("初始化内部状态");
            flyweightRoleMap.put(flyweightKey, flyweightRole);
        }
        return flyweightRoleMap.get(flyweightKey);
    }

    public UnsharedFlyweightRole getUnsharedFlyweightRole(FineGrainedRole fineGrainedRole) {
        LOG.info("调用工厂方法getUnsharedFlyweightRole");
        UnsharedFlyweightRole unsharedFlyweightRole = new UnsharedFlyweightRole(fineGrainedRole);
        //初始化操作
        unsharedFlyweightRole.setIntrinsicState1("内部状态1");
        unsharedFlyweightRole.setIntrinsicState2("内部状态2");
        return unsharedFlyweightRole;
    }
}

ClientRole:客户方角色

package com.geekarchitect.patterns.demo0305;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 客户方角色
 * @author 极客架构师@吴念
 * @createTime 2022/5/9
 */
public class ClientRole {
    private static final Logger LOG = LoggerFactory.getLogger(ClientRole.class);

    /**
     * 以细粒度对象作为主角,可以节省内存资源
     */
    public void fineGrainedRolePlayLead() {
        LOG.info("细粒度对象做为主角");
        String flyweightKey = "key1";
        FineGrainedRole fineGrainedRole = new FineGrainedRoleProxy(flyweightKey);
        fineGrainedRole.setExtrinsicState1("外部状态1");
        fineGrainedRole.setExtrinsicState2("外部状态2");
        fineGrainedRole.server();
    }

    /**
     * 以元角色对象作为主角,可以节省CPU资源
     */
    public void flyweightPlayLead() {
        LOG.info("元对象做为主角");
        String flyweightKey = "key1";
        FineGrainedRole fineGrainedRole = new FineGrainedRoleProxy(flyweightKey);
        fineGrainedRole.setExtrinsicState1("外部状态1");
        fineGrainedRole.setExtrinsicState2("外部状态2");
        FlyweightRole flyweightRole = FlyweightFactoryRole.getInstance().getFlyweightRole(flyweightKey);
        flyweightRole.server(fineGrainedRole);

    }

}

TestFlyweight:测试类

package com.geekarchitect.patterns.demo0305;

/**
 * 享元模式-测试类
 *
 * @author 极客架构师@吴念
 * @createTime 2022/5/9
 */
public class TestFlyweight {
    public static void main(String[] args) {
        TestFlyweight testFlyweight = new TestFlyweight();
        testFlyweight.fineGrainedRolePlayLeadTest();
        testFlyweight.flyweightPlayLeadTest();

    }

    public void fineGrainedRolePlayLeadTest() {
        ClientRole clientRole = new ClientRole();
        clientRole.fineGrainedRolePlayLead();
    }

    public void flyweightPlayLeadTest() {
        ClientRole clientRole = new ClientRole();
        clientRole.flyweightPlayLead();
    }

}

运行结果

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

鐮佸啘鑰佸惔,璧勬繁鐮佸啘鑰佸惔

至此,享元模式以细粒度对象为主角的案例,我们就讲解完毕。

后面,对于享元模式,我会寻找一个合适的案例,讲解以元对象为主角,节省CPU资源的案例。

本期我们就分享到这里,关注我,我将持续分享更多架构师的相关文章和视频,我们下期见。