大家好,欢迎关注极客架构师,极客架构师,专注架构师成长,我是码农老吴。
本期是《架构师基本功之设计模式》的第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)
场景,也就是我们在什么情况下,遇到了什么问题,需要使用某个设计模式。
对于享元模式,当出现以下情况时,可能需要使用享元模式。
- 从业务角度看:
这个模式,主要目的是降低系统资源的,所以,一般不从业务角度看,而是从技术角度看。
- 从技术角度看:
当一个系统中,需要把某个类对象(细粒度对象),大量的加载到内存中使用时,而这类对象内部,有重复的数据出现时(可以分离为元对象),为了降低内存浪费,可以考虑享元模式;另外,如果某个类的对象,虽然同一时刻,内存中存在的数量可能并不大,但是它的生命周期比较短,需要不断的创建和销毁,造成大量的CPU资源浪费,可以考虑使用享元模式。
角色(Role)
角色,一般为设计模式出现的类,或者对象。每种角色有自己的职责。
在享元模式中,包含三种角色,元角色,元工厂角色,细粒度角色,客户方角色。
元角色(flyweight role):元角色,就是从定义里面所说的细粒度角色中提取出来的角色,它的职责如下。
- 提供元属性:元属性,也称为内部状态,是从细粒度角色里面抽取出来的,可以共享使用的属性。
- 提供元方法:元方法,有可能是从细粒度角色里面抽取出来的,也有可能是元角色自身的方法。元方法的参数,通常是外部状态,这样元对象就拥有了内部状态和外部状态。
元工厂角色(flyweight factory role):元工厂角色,用来生产元对象的工厂类,它的职责。
- 提供缓存池:元工厂,需要实现元对象的共享,通常是使用缓存池来实现的,这个缓存池,可以是工厂类自己管理,也可以用外部的缓存池,但一般都是由工厂负责管理。
- 提供工厂方法:细粒度角色,或者客户方角色,需要元对象时,不能自己new一个,这样就无法共享了,而需要通过元工厂里面的工厂方法来获取。工厂方法里面的逻辑一般表现为,当这个元角色还不存在时,new一个或者从外部数据库加载一个,放入缓存池,反之,则直接从缓存池取出,返回给调用方。
细粒度角色( fine-grained role):细粒度角色,就是系统中,数量比较多,需要进行资源优化的对象,可以从它里面,抽取出元对象,它属于被改造的对象,它的职责是:
1,分权:原来由它提供的属性,或者方法,被元对象提供了,它就不需要再提供了。这样才能体现元对象的价值。
2,提供外部状态:细粒度对象里面的状态,被元对象拿走的,属于内部状态,留下了的属于外部状态。
客户方角色(client role):这个角色,一般情况下没什么意义,但是,上面的三种角色,在交互时,一般情况是,细粒度角色,通过元工厂角色,获取共享的元角色,然后统一对外提供服务,也就是以细粒度对象为主角。
但是,实际项目中,还存在另外一种交互方式。由客户方角色,从细粒度角色获取外部状态,又通过元工厂角色,获取元对象,然后把外部状态,传输给元对象里面的方法,进行业务处理。也就是以元角色为主角。
在上面的第二版到第四版代码,基于享元模式实现的手机号号池中。
接口,类对应的角色如下
Area:元角色
FlyweightFactory:元工厂角色
MobileInfo:细粒度角色
TestMobilePool测试类:可以看成客户方角色
交互(interaction)
交互,是指设计模式中,各种角色是如何交互的,一般用UML中的序列图,活动图来表示。简单的说就是角色之间是如何配合,完成设计模式的使命的。
享元模式,交互有点特别。
一般情况下,我们是从细粒度角色中,抽取出元角色,所以,细粒度角色应该是主角。但是,从享元模式的起源看,却是以元角色为主角。这两种情况是有所侧重的。
以细粒度角色为主角,侧重的是节省内存资源
细粒度角色在系统中本来量级就很大,从它里面分离出元角色后,并没有降低细粒度角色的对象数量,但是由于它们共享元角色,所以内存占用将会大大降低。
以元角色为主角,侧重的是节省CPU资源
如果以元角色为主角,由于元角色存放在缓存池中,所以数量非常有限,但是元角色,缺少细粒度角色中存放的外部状态信息,所以需要客户端提供给它。这种情况下,客户方角色会反复使用这些元对象。这样就避免了创建和销毁大量的对象,进而节省了大量系统资源,特别是CPU资源。线程池和数据库连接池,都是基于这种模式。
相关交互图,在后面的通用类图和代码中查看。
综上所述,基于REIS分析模型,享元模式,包含三种角色,分别是元角色,元工厂角色,细粒度角色,客户方角色,享元模式的宗旨是从细粒度角色中,分离出元角色,由元工厂负责元角色的创建和缓存,实现元角色的共享,以达到降低系统资源的目的。
效果(effect)
效果,使用该设计模式之后,达到了什么效果,有何意义,当然,也可以说说它的缺点,或者风险。
从我们前面的案例可以看出,享元模式达到了以下效果。
积极效果
- 降低了内存资源的浪费:当以细粒度对象为主角,共享元角色,可以有效降低内存浪费。
- 降低了CPU资源的浪费:当以元角色为主角,重复使用这些对象,可以减少创建和销毁对象的数量,降低了系统资源,特别是CPU资源的浪费。
- 抽象的粒度更细了:通过面向对象的设计思路,从细粒度角色中,抽象出了新的,可以复用的元角色。
消极效果
- 项目结构复杂了:增加了元工厂角色和缓存池,这些都是比较重量级的对象,维护需要花费一定的成本。
- 元角色的生命周期容器失控:由于元角色,被多个细粒度对象共享使用,导致元角色自身的生命周期不容易控制,容易造成一定的内存漏洞,但是往往可以忽略不计。
享元模式需要思考的点
缓存池的方案
缓存池如何实现,这个问题可以脱离享元模式而独立考虑,常见的解决方案如下:
轻量级的: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资源的案例。
本期我们就分享到这里,关注我,我将持续分享更多架构师的相关文章和视频,我们下期见。