高频数据实现本地缓存

背景:针对于数据库量少但是查询频率特别高的表做本地缓存,降低访问db次数,释放资源给其他请求

定义缓存接口,定义了一些方法

public interface LocalCacheRefreshSupport<T> {

    Long getId();

    LocalDateTime getLastChangeTime();

    /**
     * 更新缓存里的对象数据
     * @param t 来自数据源的新对象
     * @implNote 若实现该方法,则表示将缓存的<b>老对象</b>中的字段属性更新,而不是替换为<b>新对象</b>
     * @return 该方法的实现类需要返回一个非null对象
     */
    default T updateData(T t){
        return null;
    }
}

定义缓存实体实现接口缓存接口,重写里面的方法

@Data
public class TestCacheModel implements LocalCacheRefreshSupport<TestCacheModel> {

    /**
     * 主键ID
     */
    private Long id;

    /**
     * 上级id
     */
    private Long parentId;

    /**
     * 名称
     */
    private String name;

    /**
     * 简称
     */
    private String shortName;

    /**
     * 类型 
     */
    private ResourceTypeEnum type;

    /**
     * 应用端资源编码
     */
    private String code;

    /**
     * 外部系统兼容的code
     */
    private String resKey;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 创建人(userId)
     */
    private Long createUserId;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;

    /**
     * 更新人(userId)
     */
    private Long updateUserId;

    /**
     * 是否删除
     */
    private Boolean isDeleted;

    /**
     * 数据变动时间(没有业务含义)
     */
    private LocalDateTime dataChangeTime;

    @Override
    public LocalDateTime getLastChangeTime() {
        return dataChangeTime;
    }

    @Override
    public TestCacheModel updateData(TestCacheModel testCacheModel) {
        if (Objects.nonNull(testCacheModel) && this.getId().equals(testCacheModel.getId())) {
            WrappedBeanCopier.copyProperties(testCacheModel, this);
        }
        return this;
    }
}

定义缓存抽象类,实现Runnable接口,异步进行维护数据

@Slf4j
public abstract class LocalCache<T extends LocalCacheRefreshSupport<T>> implements Runnable{

    protected static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    protected static final long DEFAULT_RERESH_SECONDS_INTERVAL = 10;

    private static final AtomicLong REJECT_NUM = new AtomicLong(0L);;

    private static final ExecutorService REFRESH_EXECUTOR = new ThreadPoolExecutor(2, 4, 60L,TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(20), new DefaultThreadFactory("localcache-refresh-pool-"),
                (r, executor1) -> log.warn("LOCALCACHE_REFRESH_EXECUTOR reject task, current num:" + REJECT_NUM.addAndGet(1)));;

    private final AtomicBoolean shouldInitLock = new AtomicBoolean(true);

    private final AtomicBoolean canRefreshLock = new AtomicBoolean(false);

    //服务器需要执行更新的时间(LastExecuteTime + refreshSecondsInterval)
    private volatile LocalDateTime needExecuteTime;

    //数据库最新一条数据更新的时间,由于服务器时间有可能与数据库时间不一致,所以需要该字段单独维护数据库的时间
    protected volatile LocalDateTime dbLastChangeTime = LocalDateTime.MIN;

    //主缓存
    protected Map<Long, T> idMap;

    private void updateDBLastChangeTime(T t) {
        LocalDateTime lastChangeTime = t.getLastChangeTime();
        if (lastChangeTime != null && lastChangeTime.isAfter(dbLastChangeTime)) dbLastChangeTime = lastChangeTime;
    }

    @PostConstruct
    private void init(){
        REFRESH_EXECUTOR.execute(this);
    }

    @Override
    public final void run() {
    // 获取 shouldInitLock值
        if(shouldInitLock.get()){
        // 原子性修改其值
            if(shouldInitLock.compareAndSet(true, false)){
                idMap = doInit();
                idMap.values().forEach(e -> updateDBLastChangeTime(e));
                needExecuteTime = LocalDateTime.now().plusSeconds(getRefreshSecondsInterval());
                canRefreshLock.set(true);
            }
            return;
        }
        try {
            Collection<T> datas = getsByLastChangeTimeAfter();
            for (T t: datas) {
                updateDBLastChangeTime(t);
                Long id = t.getId();
                if(idMap.containsKey(id)){
                    doUpdate(t);
                    T memData = idMap.get(id);
                    if(memData.updateData(t) == null){
                        idMap.put(id, t);
                    }
                } else {
                    doAdd(t);
                    idMap.put(id, t);
                }
            }
            needExecuteTime = LocalDateTime.now().plusSeconds(getRefreshSecondsInterval());
        } catch (Exception e){
           
        } finally {
            //开锁
            boolean success = canRefreshLock.compareAndSet(false, true);
            if(!success){
                log.warn("MemoryCacheAdaptor thread compareAndSet CanUpdate(false->true) failed!");
            }
        }
    }

// 每次进行查询需要调用此方法来获取数据库最新数据到缓存中,注意因为是异步更新的,所以有可能第一次调用查询方法缓存会没有更新,所以在一些更新完需要立即获取变更后的数据的地方不建议使用缓存
    protected final void tryRefresh(LocalDateTime now){
        //为保证并发安全,这里必须提前校验锁状态
        if(!canRefreshLock.get()){
            return;
        }
        if(now == null){
            now = LocalDateTime.now();
        }
        //最新更新时间小于更新周期,则不触发更新(尽量使用外部传入的LocalDateTime对象,避免频繁创建LocalDateTime的消耗)
        if(now.isBefore(needExecuteTime)){
            return;
        }
        //单粒度锁竞争
        if(canRefreshLock.get() && canRefreshLock.compareAndSet(true, false)){
            REFRESH_EXECUTOR.execute(this);
        }
    }

    protected long getRefreshSecondsInterval(){
        return DEFAULT_RERESH_SECONDS_INTERVAL;
    }

    protected abstract Map<Long, T> doInit();

    protected abstract Collection<T> getsByLastChangeTimeAfter();

    protected abstract void doAdd(T t);

    protected abstract void doUpdate(T t);

具体缓存实现类,继承此抽象类,重新抽象方法

@Component
@Slf4j
public class TestCache extends LocalCache<TestCacheModel> {

    public static final String CODE_SPLIT_STR = "|";

    public static final String REGEXED_CODE_SPLIT_STR = "\\|";

    /**
     * 缓存: code -> TestCacheModel
     */
    private static final Map<String,TestCacheModel> CODE_MAP = new HashMap<>();


    private static TestCache This;


    /**
     * 缓存刷新时间默认 10秒
     */
    Long resourceCacheRefreshSecondsInterval = 10L;

    @Autowired
    private TestDAO testDAO;

    @Override
    protected long getRefreshSecondsInterval() {
        return resourceCacheRefreshSecondsInterval;
    }

    @Override
    protected Map<Long, TestCacheModel> doInit() {
        long beginInit = System.currentTimeMillis();
        //查询需要缓存表的所有数据
        List<TestDO> list = testDAO.selectAll();
        List<TestCacheModel> cacheList = WrappedBeanCopier.copyPropertiesOfList(list, TestCacheModel.class);
        Map<Long, TestCacheModel> idMap = new HashMap<>(list.size());
        Map<String, Long> appMap = new HashMap<>(40);
        // 初始化几个需要用到的本地缓存关系保存到Map中,比如id->缓存对象,后面就可以直接通过id去Map里面去对应值
        for (TestCacheModel testCacheModel : cacheList) {
            idMap.put(testCacheModel .getId(), testCacheModel );
            CODE_MAP.put(testCacheModel .getCode(), testCacheModel );
        }
        // 这里用于在实际业务场景中可以直接用类名.具体静态方法,不需要注入缓存实体
        This = this;
        return idMap;
    }


    @Override
    protected Collection<TestCacheModel> getsByLastChangeTimeAfter() {
        // 这里需要再数据库新增一个字段值,这个字段设置为数据变更就自动更新时间,类似于updateTime,dbLastChangeTime这个时间是上一次记录的最后变更时间, selectByDataChangeTimeAfter 这个sql就直接写 updateTime >  dbLastChangeTime 这个时间的就行
        List<TestDO> resourceDOList =testDAO.selectByDataChangeTimeAfter(dbLastChangeTime);
        if (CollectionUtil.isEmpty(resourceDOList)) {
            return Collections.emptyList();
        }
        List<TestCacheModel> resourceCacheModelList = WrappedBeanCopier.copyPropertiesOfList(resourceDOList, TestCacheModel.class);
        return resourceCacheModelList;
    }

    @Override
    protected void doAdd(TestCacheModel testCacheModel) {
        // 重写add 方法,如果数据库新增,则新增资源code缓存
        CODE_MAP.put(testCacheModel.getCode(), testCacheModel);
    }

    @Override
    protected void doUpdate(TestCacheModel testCacheModel) {
        // 获取出老资源
        TestCacheModel oldModel = This.idMap.get(testCacheModel.getId());
        // 判断是否修改code 如果是就把老资源的移除添加新资源,其他字段在updateData 中修改,引用类型在一个地方变更所有引用的地方都发生改变
        if (!oldModel.getCode().equals(testCacheModel.getCode()) && CODE_MAP.containsKey(oldModel.getCode())) {
            TestCacheModel  oldModelCache = CODE_MAP.get(oldModel.getCode());
            CODE_MAP.remove(oldModel.getCode());
            oldModelCache.setCode(testCacheModel.getCode());
            CODE_MAP.put(oldModel.getCode(), oldModelCache);
        }
    }

    //---------------------对外查询方法----------------------------------------------


    /**
     * 通过资源id获取资源
     *
     * @param ids 资源ids
     * @param commonStatusEnum 状态枚举
     * @param filterIsDeleted 是否过滤已删除 true 是 false 否
     * @param resourceTypeEnums 资源类型
     * @return
     */
    public static List<TestCacheModel> getByIds(Collection<Long> ids, CommonStatusEnum commonStatusEnum, boolean filterIsDeleted, List<ResourceTypeEnum> resourceTypeEnums) {
        if (CollectionUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        This.tryRefresh(LocalDateTime.now());
        List<TestCacheModel> resultList = new ArrayList<>(ids.size());
        Set<Long> idSet = new HashSet<>(ids);
        for (Long id : idSet) {
            if (This.idMap.containsKey(id)) {
                resultList.add(This.idMap.get(id));
            }
        }
        return filterIsDeletedAndStatus(resultList, filterIsDeleted, commonStatusEnum, resourceTypeEnums);
    }



   
    /**
     * 通过code批量查询资源数据
     * @param codes codes集合
     * @param resourceTypeEnums 资源类型
     * @param commonStatusEnum 状态枚举
     * @param filterIsDeleted 是否过滤已删除 true 是 false 否
     * @return
     */

    public static List<TestCacheModel> getByCodes(Collection<String> codes, List<ResourceTypeEnum> resourceTypeEnums, CommonStatusEnum commonStatusEnum, boolean filterIsDeleted) {
        if (CollectionUtil.isEmpty(codes)) {
            return Collections.emptyList();
        }
        This.tryRefresh(LocalDateTime.now());

        List<TestCacheModel> resultList = new ArrayList<>(codes.size());
        Set<String> codeSet = new HashSet<>(codes);
        for (String code : codeSet) {
            if (CODE_MAP.containsKey(code)) {
                resultList.add(CODE_MAP.get(code));
            }
        }
        return filterIsDeletedAndStatus(resultList, filterIsDeleted, commonStatusEnum, resourceTypeEnums);
    }


    /**
     * 通过codeList前缀匹配
     *
     * @param codeListPrefix   前缀集合
     * @param resourceTypes    资源类型
     * @param commonStatusEnum 状态枚举
     * @param filterIsDeleted  是否过滤已删除的 true 是 false 否
     * @return List<ResourceModel>
     */
    public static List<TestCacheModel> getByCodeListPrefix(Collection<String> codeListPrefix, List<ResourceTypeEnum> resourceTypes, CommonStatusEnum commonStatusEnum, boolean filterIsDeleted) {
        if (CollectionUtil.isEmpty(codeListPrefix)) {
            return Collections.emptyList();
        }
        This.tryRefresh(LocalDateTime.now());
        Set<TestCacheModel> result = new HashSet<>();
        for (String codePrefix : codeListPrefix) {
            CODE_MAP.forEach((code, testCacheModel) -> {
                if (code.startsWith(codePrefix)) {
                    result.add(testCacheModel);
                }
            });
        }
        return filterIsDeletedAndStatus(new ArrayList<>(result), filterIsDeleted, commonStatusEnum, resourceTypes);
    }




    /**
     * 过滤掉已删除的数据
     *
     * @param list            资源集合
     * @param filterIsDeleted 是否过滤已删除 ture 是, false 否
     * @param commonStatusEnum 状态枚举
     * @param commonStatusEnum 状态
     * @return List<ResourceModel>
     */
    public static List<TestCacheModel> filterIsDeletedAndStatus(List<TestCacheModel> list, boolean filterIsDeleted, CommonStatusEnum commonStatusEnum, List<ResourceTypeEnum> enumList) {
        if (CollectionUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        // 更具条件过滤数据
        return list.stream()
                .filter(resourceModel -> !filterIsDeleted || !resourceModel.getIsDeleted())
                .filter(resourceModel -> commonStatusEnum == null || resourceModel.getStatus().equals(commonStatusEnum.getValue()))
                .filter(resourceModel -> CollectionUtil.isEmpty(enumList) || enumList.contains(resourceModel.getType()))
                .collect(Collectors.toList());
    }

    private String[] fullCodeSplit(String code) {
        String[] parts = code.split(REGEXED_CODE_SPLIT_STR);
        int length = parts.length;
        String[] codes = new String[length];
        codes[0] = parts[0];
        for (int i = 1; i < length; i++) {
            codes[i] = codes[i - 1] + CODE_SPLIT_STR + parts[i];
        }
        return codes;
    }
}

注意事项

1.数据库需要新增一个时间字段,用于记录数据发生变更的时间,这个字段要设置成,数据变更自动更新此时间,数据库可以设置。

2.因为初始化缓存和缓存更新都是异步的,所以可能会有时效问题,在数据变更后需要立即展示出来的地方不建议使用缓存。

3.需要提供两个sql,一个是用于初始化加载所有数据到缓存中,一个是用于获取最新变更的数据。

4.如果表数据量过大不建议使用内存缓存,防止导致服务崩溃。

5.updateData 重写的此方法是直接更新的值,因为是引用类型,所以一处修改所有Map维护缓存的地方都更新了,如果不使用此方案可以不重新此方法,然后自己在add,update 中添加新对象

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值