享元模式
一. 说明
享元模式主要是为了降低对象创建的数量,减少内存空间和提高性能,它是结构型模式的一种。
当我们在大量创建创建对象时,可能会有内存溢出的风险,我们把对象中可以外部化的部分抽象出去,在再次需要使用对象时直接返回内存中的对象,以达到对象可以重复利用的目的。
享元模式将对象的结构分为内部状态和外部状态,对象可以根据内部状态进行分组,将外部状态的部分从对象中剔除,这样每一组对象都可以只用一个对象表示。例如我们在画圆形时,圆形具有颜色和坐标两个属性,颜色可以是它的内部状态,而坐标可以外部化,这样我们可以将圆形根据颜色分组,一个颜色一个对象,同一个颜色的圆形却可以画在不同坐标上。
二.应用场景
- 开发中常接触的数据库连接池,线程池等池化技术就是享元模式的应用
- 游戏中的场景渲染例如儿时玩的红白机游戏一个图片反复进行变形移位形成动画场景
- 秒杀时需要读取商品信息和库存,商品信息一般是不变的为了降低数据库压力可以将它缓存到本地map中,而库存是一直变化的可以将它写入redis
- Java中的String类就是用的享元模式减少字符串对象的创建
三.代码示例
用我们实际项目开发中的例子来说明享元模式的应用
我们在写某某平台服务端项目时,数据库设计上通常会有一张字典表放在系统基础服务中,该表用来存储字典配置项和整个平台的配置项,整个表结构就是key-value的关系,如下
CREATE TABLE `tb_dictionary` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`parent_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '父级id',
`name` varchar(50) NOT NULL COMMENT '键名',
`value` text NOT NULL COMMENT '键值',
`status` int(1) NOT NULL DEFAULT '1' COMMENT '是否启用 是1 否0',
`remark` varchar(255) DEFAULT '' COMMENT '注释',
`create_time` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `IDX_NAME` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=289 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='字典表';
这张表承载着整个平台的常用配置项,所以也会提供给用户服务、订单服务、消息服务等其他微服务调用查询配置值,最初的代码是这样的:
@Service
public class DictionaryServiceImpl implements DictionaryService {
@Resource
private DictionaryMapper dictionaryMapper;
@Override
public ConfigVO queryConfig(String key) {
if (!StringUtils.hasText(key)) {
return null;
}
DictionaryDO dictionaryDO = dictionaryMapper.selectByName(key);
return ConfigVO.convert(dictionaryDO);
}
}
项目刚上线时,用户量和访问量都不太大,直接查询数据库还能支撑。随着项目的推广和迭代,用户逐渐增多,日活越来越大时,直接访问数据库会给数据库造成很大压力,我们需要优化它。
从业务场景上分析,配置项一般是不太变化的,而每次查询都去读取数据库返回,这里会有两个问题,一个是数据库端有压力,一个是JVM层也会造成很多无用对象即使它们很快会被回收。我们可以像秒杀案例一样,给键值对象做一层本地缓存并设置缓存过期时间,就可以减少对象创建和数据库压力了,这里我们用的是google.guava包中的Cache机制。
改造后的代码是这样的:
@Service
public class DictionaryServiceImpl implements DictionaryService {
@Resource
private DictionaryMapper dictionaryMapper;
//使用guava的cache本地缓存
private Cache<String, ConfigVO> dictionaryCache =
CacheBuilder.newBuilder().maximumSize(10000).expireAfterWrite(5, TimeUnit.MINUTES).build();
@Override
public ConfigVO queryConfig(String key) {
if (!StringUtils.hasText(key)) {
return null;
}
ConfigVO configVO = dictionaryCache.getIfPresent(key);
if (configVO == null) {
// 高并发锁
synchronized (key) {
configVO = dictionaryCache.getIfPresent(key);
// double check
if (configVO == null) {
dictionaryDO = dictionaryMapper.selectByName(key);
if (dictionaryDO == null) {
// 防止缓存穿透设置默认值
dictionaryDO = new DictionaryDO();
}
dictionaryCache.put(key, ConfigVO.convert(dictionaryDO));
}
}
}
return configVO;
}
}
我们使用了本地缓存,将配置项缓存一遍,每次请求过来都是先读本地缓存,没有则读数据库后再次缓存5分钟,这里用到了double check的思想。这样问题就解决了。
四. 总结
享元模式在项目开发中可能会无意的使用到它的思想:就是减少对象数量降低内存开销。在合适的场景下合理分离对象的内外状态来运用享元模式。