详解

享元模式是一种结构型设计模式,其主要目的是通过共享尽可能多的相同部分来有效地支持大量细粒度的对象。它通过将对象的属性分为内在属性(可以共享、不随环境变化的部分)和外在属性(根据场景变化、不能共享的部分),从而减少内存占用和提高性能。

核心思想:

享元模式通过复用对象,避免重复创建大量相似对象,从而节省内存。它通常适用于创建大量相似对象且这些对象包含可以共享的相同状态的场景。

享元模式的组成部分:

  1. 享元接口(Flyweight Interface):定义享元对象的接口,供外部使用。
  2. 具体享元类(Concrete Flyweight):实现享元接口,并存储共享的内部状态。
  3. 享元工厂(Flyweight Factory):负责创建和管理享元对象,确保相同的对象只被创建一次。
  4. 客户端(Client):使用享元对象,并存储外部状态(即不共享的部分)。

使用场景:

享元模式适用于以下场景:

  1. 大量相似对象的创建:当系统中需要创建大量相似或相同的对象时,例如大量的小文本、按钮、图形等。
  2. 内存占用过高:如果这些对象占用大量内存,且其中一部分状态是共享的,可以通过享元模式减少内存消耗。
  3. 对象的状态可以分离:对象的状态可以分为内部状态(可以共享)和外部状态(不能共享),并且外部状态可以由客户端代码显式传递。

如图所示:

例如 这种比较通用的,大颗粒度的,精彩需要被调用的对象,我们就可以使用享元模式,将它缓存起来

简单享元示例

1. 享元模式实现

首先,我们定义一个享元对象和一个享元工厂。

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

// 享元接口
interface Flyweight {
    void operation(String extrinsicState);
}

// 具体享元实现
class ConcreteFlyweight implements Flyweight {
    private final String intrinsicState;

    public ConcreteFlyweight(String intrinsicState) {
        this.intrinsicState = intrinsicState;
    }

    @Override
    public void operation(String extrinsicState) {
        System.out.println("Intrinsic State: " + intrinsicState + ", Extrinsic State: " + extrinsicState);
    }
}

// 享元工厂
class FlyweightFactory {
    private final Map<String, Flyweight> flyweights = new HashMap<>();

    public Flyweight getFlyweight(String intrinsicState) {
        if (!flyweights.containsKey(intrinsicState)) {
            flyweights.put(intrinsicState, new ConcreteFlyweight(intrinsicState));
        }
        return flyweights.get(intrinsicState);
    }

    public void updateFlyweight(String intrinsicState, String newState) {
        // 此处可以实现更新逻辑
        System.out.println("Updating Flyweight with intrinsic state: " + intrinsicState + " to new state: " + newState);
        flyweights.put(intrinsicState, new ConcreteFlyweight(newState)); // 替换为新状态
    }
}

2. 自定义注解和 AOP 实现

接下来,我们使用 Spring AOP 在进行增删改操作时更新享元工厂。

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

// 定义自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UpdateFlyweight {
    String state();
}

// 切面类
@Aspect
@Component
class FlyweightUpdateAspect {
    @Autowired
    private FlyweightFactory flyweightFactory;

    @Pointcut("@annotation(updateFlyweight)")
    public void updatePointcut(UpdateFlyweight updateFlyweight) {}

    @AfterReturning(value = "updatePointcut(updateFlyweight)", argNames = "updateFlyweight", returning = "result")
    public void afterReturning(UpdateFlyweight updateFlyweight, Object result) {
        String intrinsicState = updateFlyweight.state();
        // 更新享元工厂
        flyweightFactory.updateFlyweight(intrinsicState, "New State Value");
    }
}

3. 使用示例

我们可以定义一个服务类,里面包含一些增删改操作,并使用自定义注解来标记。

import org.springframework.stereotype.Service;

@Service
public class DataService {
    // 增加数据
    @UpdateFlyweight(state = "exampleState")
    public void addData() {
        System.out.println("Data added.");
    }

    // 删除数据
    @UpdateFlyweight(state = "exampleState")
    public void deleteData() {
        System.out.println("Data deleted.");
    }

    // 修改数据
    @UpdateFlyweight(state = "exampleState")
    public void updateData() {
        System.out.println("Data updated.");
    }
}

4. 启动 Spring Boot 应用

最后,使用 Spring Boot 启动应用,并在 main 方法中进行bean的调用。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class FlyweightApplication {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(FlyweightApplication.class, args);
        DataService dataService = context.getBean(DataService.class);

        dataService.addData();    // 添加数据
        dataService.updateData();  // 修改数据
        dataService.deleteData();  // 删除数据
    }
}

场景实践

回到这张图,我想对大颗粒度的 这个对象做缓存,但是用户如果修改了 应发项 或者是 涉及到该domain对象的任何一项 我都要更新这个缓存,我还要给这个缓存设置缓存过期时间,并且重新读取缓存的代码 要易维护,毕竟这个设计者模式要通用,肯定不只是能为这一个domain对象服务

ok,我列一下,需要支持的功能:

1、缓存

2、缓存到期时间

3、数据的实时性,受到修改 每个实体要有自己的重新加载缓存策略

4、如果大颗粒度对象存太多,容易内存溢出需要加入限制,且每个实体限制 应根据业务场景调整

5、如果达到这个缓存上限 我应该踢掉一个,把新的放进来,该怎么踢

大家看到上面这些要求 是不是很苦恼,莫慌,我狂肝一整天 已经想到了解决方案 并且已经实现,请看如下结构:

调用示例

这样使用起来,是不是非常的简洁,清晰,明了


/**
 * @author shenwang
 * @description 缓存池使用范例
 * @date 2024-10-31 14:21
 */
@Component
public class LargeObjectCachePoolExample {

    /**
     * 想用哪个对象,注入哪个对象的pool,pool位置:cn.txbd.infrastructure.gatewayimpl.cache.pool.impl文件下
     */
    @Autowired
    private CommissionTemplateCommonDOPool commissionTemplateCommonDOPool;

    /**
     * 查询佣金模板domain对象
     */
    public void queryCommissionTemplateDO(String templateId){
        LargeObjectCacheResult<CommissionTemplateCommonDO> templateDO = commissionTemplateCommonDOPool.get(templateId);
    }

    /**
     * 修改佣金项
     * 修改了佣金项佣金模板基本信息会受到影响
     * @param templateId
     */
    public void updateCommissionItem(String templateId){
        //todo 修改佣金项逻辑,此处省略100行代码
        //重新加载缓存
        commissionTemplateCommonDOPool.reload(templateId);
    }
}

如何扩展

当然使用起来很方便,那扩展起来 那是更方便啊!!!!!!!

ok,光说没用,请看代码:


/**
 * @author shenwang
 * @description CommissionTemplateCommonDO对象缓存池
 * @date 2024-10-30 17:13
 */
@Component
public class CommissionTemplateCommonDOPool extends LargeObjectBasePool<CommissionTemplateCommonDO>  {

    public CommissionTemplateCommonDOPool() {
        super(LargeObjectCacheType.COMMISSION_TEMPLATE_COMMON_DO);
    }

    @Async
    @Override
    public void reload(String key) {

        //todo 重新查询 CommissionTemplateCommonDO domain对象的实现
        CommissionTemplateCommonDO commissionTemplateCommonDO = new CommissionTemplateCommonDO();
        commissionTemplateCommonDO.setTemplateId(key);
        commissionTemplateCommonDO.setTemplateName("测试模版");

        //更新缓存
        this.put(key,commissionTemplateCommonDO, LargeObjectCacheConstant.DEFAULT_DELAY_TIME,LargeObjectCacheConstant.DEFAULT_DELAY_TIME_UNIT);
    }
}

如上图所示,只需要继承LargeObjectBasePool,并且重写一下reload方法就好了

核心LargeObjectBasePool

该类提供了方法:

1、put      放入缓存,可以自定义缓存失效时间,如果到达上限则会穷举 踢掉一个缓存,腾出空间

2、get      获取缓存,如果没有获取到 会去走一遍reload方法 并且给上默认的失效时间

3、remove  移除缓存


/**
 * @author shenwang
 * @description 大对象缓存池
 * @date 2024-10-30 17:14
 */
@Slf4j
public abstract class LargeObjectBasePool<T extends Serializable> implements Serializable{

    private static final long serialVersionUID = 1L;

    /**
     * 定时任务调度(用于设置缓存对象的过去时间)
     */
    private ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    /**
     * 缓存的大对象类型
     */
    public LargeObjectCacheType cacheType;

    /**
     * 缓存池
     */
    public Map<String, T> pool;

    /**
     * 每个key被命中的次数
     */
    public Map<String, Integer> hitCountMapping;

    public LargeObjectBasePool(LargeObjectCacheType cacheType) {
        this.cacheType = cacheType;
        // 初始化pool,使用提供的limit作为初始容量
        this.pool = new HashMap<>(cacheType.getLimit());
        this.hitCountMapping = new HashMap<>(cacheType.getLimit());
    }

    /**
     * 重新加载缓存数据
     * 抽象方法,让子类重写
     * @param key
     * @return
     */
    public abstract void reload(String key);

    /**
     * 获取缓存池中的对象
     * @param key 缓存的键
     * @return 返回对应类型的 LargeObjectCacheResult
     */
    public LargeObjectCacheResult<T> get(String key) {

        // 从缓存池中获取对象
        T t = pool.get(key);
        if (ObjectUtil.isNotNull(t)) {

            // 命中次数+1
            hitCountIncrementOne(key);

            log.info("LargeObjectBasePool,类型:{}, 获取到了缓存数据, key:{},命中次数:{},当前keyValues:{}",
                    cacheType.getDesc(), key, hitCountMapping.get(key),JSONUtil.toJsonStr(pool.keySet()));

            return createResult(key, t);
        }

        // 如果在缓存池中没有拿到,则重新获取一次
        reload(key);
        t = pool.get(key);
        if (ObjectUtil.isNull(t)) {
            log.warn("LargeObjectBasePool,类型:{},未能重新加载缓存对象,key:{}", cacheType.getDesc(), key);
            return null;
        }

        // 将重新获取的对象放入缓存池
        this.put(key, t, LargeObjectCacheConstant.DEFAULT_DELAY_TIME,LargeObjectCacheConstant.DEFAULT_DELAY_TIME_UNIT);

        // 返回深拷贝对象
        return createResult(key, t);
    }

    /**
     * 放入缓存
     * @param key
     * @param obj
     */
    public synchronized  void put(String key, T obj, long delay, TimeUnit timeUnit) {
        // 如果超过了限制,则去移除一个缓存,腾出空间
        if (pool.size() >= cacheType.getLimit()) {

            //穷举出 命中率第二小的key,如果没有 则移除第一个key
            String removeKey = findSecondMinHitCountKey();
            if (StringUtil.verifyEmptyString(removeKey)){
                removeKey = pool.keySet().iterator().next();
            }

            //移除缓存,key 对应的命中次数
            pool.remove(removeKey);
            hitCountMapping.remove(removeKey);
        }
        pool.put(key, deepCopy(obj));
        hitCountMapping.put(key, 0);

        // 创建一个任务,在指定延迟后移除键值对
        scheduler.schedule(() -> {
            this.remove(key);
            log.info("被移除的key: {}",key);
        }, delay, timeUnit);
    }

    /**
     * 移除缓存
     * @param key
     */
    public synchronized void remove(String key){
        //移除缓存,key 对应的命中次数
        pool.remove(key);
        hitCountMapping.remove(key);
    }

    /**
     * 命中次数+1
     * @param key
     */
    private void hitCountIncrementOne(String key) {
        Integer hitCount = hitCountMapping.get(key);
        if (ObjectUtil.isNull(hitCount) || hitCount < 1) {
            hitCount = 1;
        } else {
            hitCount++;
        }
        hitCountMapping.put(key, hitCount);
    }

    /**
     * 穷举出命中率第二低的key
     * @return
     */
    private String findSecondMinHitCountKey() {
        // 如果没有足够的记录,返回null
        if (hitCountMapping.size() < 2) {
            return null;
        }

        // 初始化最小和第二小的hitCount为最大整数
        int minHitCount = Integer.MAX_VALUE;
        int secondMinHitCount = Integer.MAX_VALUE;
        String minKey = null;
        String secondMinKey = null;

        for (Map.Entry<String, Integer> entry : hitCountMapping.entrySet()) {
            int hitCount = entry.getValue();

            if (hitCount < minHitCount) {
                // 更新第二小和最小的hitCount
                secondMinHitCount = minHitCount;
                secondMinKey = minKey;
                minHitCount = hitCount;
                minKey = entry.getKey();
            } else if (hitCount < secondMinHitCount && hitCount != minHitCount) {
                // 更新第二小的hitCount
                secondMinHitCount = hitCount;
                secondMinKey = entry.getKey();
            }
        }

        return secondMinKey;
    }

    /**
     * 获取缓存类型
     * @return
     */
    public LargeObjectCacheType getCacheType() {
        return cacheType;
    }

    /**
     * 创建并返回 LargeObjectCacheResult 对象
     * @param key 缓存的键
     * @param obj 缓存的对象
     * @return 返回 LargeObjectCacheResult 对象
     */
    private LargeObjectCacheResult<T> createResult(String key, T obj) {
        return new LargeObjectCacheResult<>(key, deepCopy(obj), hitCountMapping.get(key));
    }

    /**
     * 深拷贝方法,使用序列化与反序列化实现深拷贝
     * @param obj
     * @return
     */
    @SuppressWarnings("unchecked")
    protected T deepCopy(T obj) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            out.writeObject(obj);
            out.flush();
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream in = new ObjectInputStream(bis);
            return (T) in.readObject();
        } catch (NotSerializableException e) {
            log.error("对象未实现Serializable接口: {}", e.getMessage());
            throw new RuntimeException("深拷贝失败: 对象未实现Serializable接口", e);
        } catch (IOException e) {
            log.error("IOException during deep copy: {}", e.getMessage());
            throw new RuntimeException("深拷贝失败: IO异常", e);
        } catch (ClassNotFoundException e) {
            log.error("ClassNotFoundException during deep copy: {}", e.getMessage());
            throw new RuntimeException("深拷贝失败: 类未找到", e);
        }
    }

}

并且采用深拷贝,避免使用缓存对象时,缓存对象被踢除

通用代码


/**
 * @author shenwang
 * @description 常量
 * @date 2024-10-31 14:45
 */
public class LargeObjectCacheConstant {

    /**
     * 默认过期时间
     */
    public static final long DEFAULT_DELAY_TIME = 2;

    /**
     * 默认过期时间单位
     */
    public static final TimeUnit DEFAULT_DELAY_TIME_UNIT = TimeUnit.HOURS;

}



/**
 * @author shenwang
 * @description 大对象缓存类型
 * @date 2024-10-30 16:42
 */
@Getter
@AllArgsConstructor
public enum LargeObjectCacheType {


    COMMISSION_TEMPLATE_COMMON_DO(0, CommissionTemplateCommonDO.class,10,"佣金模板共用对象(CommissionTemplateCommonDO.class)");

    /**
     * code 类型唯一标识
     */
    public Integer code;

    /**
     * 对应的类
     */
    public Class clazz;

    /**
     * 缓存上限
     */
    public Integer limit;

    /**
     * 描述
     */
    public String desc;

    public static LargeObjectCacheType getByCode(Integer code){
        for (LargeObjectCacheType type : LargeObjectCacheType.values()){
            if (type.getCode().intValue() == code.intValue()){
                return type;
            }
        }
        return null;
    }
}



/**
 * @author shenwang
 * @description 公用的大对象返回结果
 * @date 2024-10-31 09:50
 */
@Data
public class LargeObjectCacheResult<T> {

    private String key;

    private T obj;

    private Integer hitCount;

    public LargeObjectCacheResult(String key,T obj,Integer hitCount){
        this.key = key;
        this.obj = obj;
        this.hitCount = hitCount;
    }
}

OK,今天的分享就到这里喽,感兴趣的小伙伴 可以copy一下场景实践的代码自己去试试效果,本人亲测效果很不错,有问题,或者更好的解决方案可以在评论区留言哦

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部