基于Spring WebFlux响应式框架的后台缓存管理工具

13 篇文章 0 订阅

业务背景

笔者所负责的项目中存在多个相似模块,需要与对应的外部API进行交互,并且该交互具备如下特点:

  • 高频交互(超过10个并发/负载*s)
  • 需要定期更新token\session
  • 单个token访问并发存在限制,需要同时使用多个token

在传统业务流程中,我们通常遵循如下流程对token进行检查

开始请求
是否存在token
是否可用
获取新的token
使用Token
将新token存入缓存
调用API
返回结果
j结束

这样做的缺陷很明显,即每个请求都会在是否存在token和是否可用上面做判断,增加开销的同时降低了接口的效率。同时如果token过期,请求又会额外进行koken获取的工作。这个通常来说是阻塞操作,也进一步降低了效率,提高了接口相应的时间。而且在缓存过期的同时发起的请求,会进行并发获取token的操作,也引发了一些不必要的风险产生。

所以,当我们使用了Caffeine这样的本地缓存时,利用缓存过期的特性,可以简化上述步骤,同时避免出现并发获取token的场景发生

开始请求
是否存在token
使用Token
获取新的token
将新token存入缓存
调用API
返回结果
结束

但是我们也只是解决了部分的问题。

实现目标

那有没有一种办法可以达到如下目的:

  • 异步进行token检查,并完成token更新
  • 多Token管理,并对Token进行请求分散化处理(Token级别的负载均衡)
  • 多场景中可以进行代码复用

“只要思想不滑坡,办法总比困难多”。这样的需求对于各位久经沙场的读者而言方案自然手到擒来。比如说:进行token的hash分布管理达到负载均衡的目的;使用工厂类保证代码可以被复用;利用异步线程池,使用run()方法实现异步的token检查工作。等等~

得益于WebFlux所基于的Reactive Stream框架,上述需求得以从不同的思路中轻松实现。

运行环境

  • Java11
  • SpringBoot 2.3.4.RELEASE
  • Spring WebFlux
    注:虽然使用了Java11 但是其中的API在Java8中都是支持的,可以直接迁移使用。

基础知识说明

WebFlux&Reactor

进这篇文章的各位多少对WebFlux有一定的认识。如果小伙伴对相关知识点还有些模糊,建议先参考如下这篇文章《探究WebFlux之WebFlux 的秘密》

doOnNext

作为Reactor中的一个重要操作符,doOnNext可以非常便捷得帮助我们完成一些异步操作,从而达到我们期望达到的项目目标。
对于Reactor相关操作符还不是非常熟悉的童鞋,可以参看Reactor官方文档

CAS

CAS通过CPU原生的指令对内存数据进行原子操作,在JAVA中具体使用的是Atomic类来进行实现。在这个项目中,Atomic类的相关实现被用作对应Token更新的原子锁。

基础流程图

我们遵循如下流程图。核心是利用其高并发,及响应式无阻塞特性,将Token更新尽量在后台完成,将其对实时请求的影响降到最低

doOnNext
开始请求
是否存在可用token
获取原有Token
使用Token
获取新的token
将新token存入缓存
调用API
返回结果
结束
是否已过更新有效期
异步线程结束
获取新token
将新token存入缓存

这个流程图最大的改变是如果有可用Token,哪怕过了“更新有效期”需要进行更新,也不会再主线程中处理,又doOnNext进行一个独立的“流”操作。我们再通过CAS机制在token更新时对token加锁,过滤同一时期共同请求更新Token的线程请求,这样保证获取token的Api不会被集中调用应发灾难后果。

代码

接口

接口定义多个方法,用于不同场景下的使用

package com.ly.train.sb.service.cache;

import reactor.core.publisher.Mono;

/**
 * BackgroundRefreshCacheService
 * 后台更新缓存
 * <p>
 * 在许多业务场景下(如获取xx的Token\Cookie等),缓存在业务
 * 调用时检测有效性并进行更新是一件非常损耗性能的事儿:及对应请
 * 求需要等待缓存更新完毕后才能使用,直接拉长了请求。而当前实现
 * 为了解决这一问题。及在高并发的场景下,通过设定一个异步更新流
 * 来处理缓存值的更新问题
 * <p>
 * 在这个缓存工具中,有如下几个特点:
 * 1、内部都为了实现同一个数据而存在(如token等),所以不存在
 * Key的概念,只有Value一个泛型
 * 2、全部使用响应式接口,用于配合响应式编程使用
 * 3、在具体的接口实现中,可以使用配置的方式配置多个缓存值,使
 * 用负载均衡逻辑平均每个值的使用频次。
 *
 * @author John Chen
 * @since 2021/2/4
 */
public interface BackgroundRefreshCacheService<V> {
    /**
     * 获取值
     * 在这个方法下,后方逻辑在缓存存在且有效的情况下给出缓存值,
     * 并给出一个异步流(doOnNext())用于检查缓存是否需要更新。
     * 当缓存已经无效时,进行直接更新
     *
     * @return 返回对应值。如果无法获取值,则会抛出异常
     */
    Mono<V> get();


    /**
     * 获取指定索引位置的值
     * 同{@link #get()}的基础逻辑一样,不同的是,通过index参数,
     * 后端会给出清单中指定位置的缓存值
     * 注意:建议通过{@link #get()}方法获取缓存
     *
     * @param index 缓存序列索引
     * @return 返回指定索引位置的值,如果无法获取值,会抛出异常
     */
    Mono<V> get(Mono<Integer> index);

    /**
     * 手动刷新缓存
     * 调用该方法会手动将所有进入了更新周期的缓存刷新一遍。可以
     * 用于方法并未被频繁访问时,手动更新缓存
     */
    void refresh();

    /**
     * 手动刷新缓存
     * 需要注意的是,同{@link #refresh()}不同,指定位索引位置
     * 会被强制刷新,并不会判断是否已经过期
     *
     * @param index 缓存的索引位置
     */
    void refresh(Integer index);

    /**
     * 强制刷新所有缓存
     * 非常消耗性能的方法,使用时要格外注意!
     */
    void refreshAll();
}

工厂类

工厂类几乎实现了所有接口中的方法,并完成了整个需求目标的主逻辑。实际场景中只要继承当前工厂类,完成对应的token获取逻辑,即可完成接入

package com.ly.train.sb.service.cache;

import com.ly.train.sb.common.config.properties.BackGroundRefreshConfigProperties;
import com.ly.train.sb.common.utils.RandomUtils;
import com.google.gson.Gson;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import reactor.core.Disposable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Predicate;
import java.util.function.Supplier;

/**
 * BaseBackgroundRefreshCacheServiceFactory
 * 用于构建{@link BackgroundRefreshCacheService}实现类的工厂类
 *
 * @author John Chen
 * @since 2021/2/5
 */
@Slf4j
public abstract class BaseBackgroundRefreshCacheServiceFactory<V> implements BackgroundRefreshCacheService<V> {
    /**
     * 用于标识对应索引位置的缓存正在执行更新的标志位
     * 使用true代表正在更新,使用false代表无任务正在更新
     * 在运行前,先通过CAS机制,将值变更为true
     * 利用{@link AtomicInteger}的CAS机制,当更新对应标志位成功时,对应任务则获取到执行许可(相当于一个乐观锁)
     * 而CAS更新失败时,则证明有其它任务(线程)正在更新。则放弃当前任务的更新操作。从而实现无阻塞的后台更新机制
     */
    private final Map<Integer, AtomicBoolean> cacheChangingSingleMap = new ConcurrentHashMap<>();
    /**
     * 用于存储真正缓存值的位置
     */
    private final Map<Integer, CacheEntity> cacheMap = new ConcurrentHashMap<>();

    /**
     * 用于获得新的缓存值的函数
     * 由于获取缓存新值可能是一个阻塞过程。所以使用Mono进行输出
     */
    private final Supplier<Mono<V>> getCacheValue;
    /**
     * 当前类型的功能专用配置类
     */
    private final Supplier<BackGroundRefreshConfigProperties.ConfigEntity> getConfigEntity;

    /**
     * 用于测试对应的缓存是否需要进行更新的函数
     */
    private final Predicate<CacheEntity> cacheNeedRefresh;

    private final Predicate<CacheEntity> cacheCanBeUsed;

    private final Gson gson;


    protected BaseBackgroundRefreshCacheServiceFactory(Supplier<Mono<V>> getCacheValue, Supplier<BackGroundRefreshConfigProperties.ConfigEntity> getConfigEntity, Gson gson, Predicate<CacheEntity> cacheNeedRefresh, Predicate<CacheEntity> cacheCanBeUsed) {
        this.getCacheValue = getCacheValue;
        this.getConfigEntity = getConfigEntity;
        this.gson = gson;
        this.cacheNeedRefresh = cacheNeedRefresh;
        this.cacheCanBeUsed = cacheCanBeUsed;
    }

    /**
     * 不指定判断缓存是否需要刷新的方法,使用默认方法
     *
     * @param getCacheValue   用于获得新的缓存值的函数
     * @param getConfigEntity 当前类型的功能专用配置类
     */
    protected BaseBackgroundRefreshCacheServiceFactory(Supplier<Mono<V>> getCacheValue, Supplier<BackGroundRefreshConfigProperties.ConfigEntity> getConfigEntity, Gson gson) {
        this.getCacheValue = getCacheValue;
        this.getConfigEntity = getConfigEntity;
        this.gson = gson;
        BackGroundRefreshConfigProperties.ConfigEntity configEntity = getConfigEntity.get();
        this.cacheNeedRefresh = data -> defaultCacheNeedRefresh(data.getDataUpdateTime(), configEntity);
        this.cacheCanBeUsed = data -> defaultCacheCanBeUsed(data.getDataUpdateTime(), configEntity);
    }

    /**
     * 判断缓存是否需要刷新(不包含缓存是否可用判断)
     * 在实际使用中,应当先判断缓存是否可用,再判断缓存是否需要刷新
     *
     * @param dataUpdateTime 数据生成时间
     * @param configEntity   对应的配置实体
     * @return 缓存是否需要刷新。true则需要刷新;false则无需刷新
     */
    protected static boolean defaultCacheNeedRefresh(long dataUpdateTime, BackGroundRefreshConfigProperties.ConfigEntity configEntity) {
        //已过去的秒数
        long passTimeSec = (System.currentTimeMillis() - dataUpdateTime) / 1000;
        return passTimeSec > configEntity.getNeedRefreshSec();
    }

    /**
     * 默认的用于判断缓存是否可用的方法
     *
     * @param dataUpdateTime 数据生成时间
     * @param configEntity   对应的配置实体
     * @return 返回缓存是否可用。true缓存可用;false缓存不可用
     */
    protected static boolean defaultCacheCanBeUsed(long dataUpdateTime, BackGroundRefreshConfigProperties.ConfigEntity configEntity) {
        //已过去的秒数
        long passTimeSec = (System.currentTimeMillis() - dataUpdateTime) / 1000;
        return passTimeSec < configEntity.getMaxValidPeriod();
    }


    /**
     * 初始化方法
     * 用于启动时进行数据预热
     * 会刷新全部缓存
     */
    @PostConstruct
    public void init() {
        BackGroundRefreshConfigProperties.ConfigEntity configEntity = getConfigEntity.get();
        log.info("init star.config:{}", configEntity);
        Disposable disposable = Flux.range(0, configEntity.getCacheCount())
                //元素延迟
                .delayElements(configEntity.getBatchRefreshDelay())
                .log("initting")
                //从入参中逐个进行刷新
                .subscribe(this::refresh);
        while (true) {
            if (disposable.isDisposed()) {
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
        }
        log.info("init finished");
    }

    /**
     * 获取值
     * 在这个方法下,后方逻辑在缓存存在且有效的情况下给出缓存值,
     * 并给出一个异步流(doOnNext())用于检查缓存是否需要更新。
     * 当缓存已经无效时,进行直接更新
     *
     * @return 返回对应值。如果无法获取值,则会抛出异常
     */
    @Override
    public Mono<V> get() {
        return get(Mono.just(RandomUtils.randomIntFromMax(getConfigEntity.get().getCacheCount() - 1)));
    }

    /**
     * 获取指定索引位置的值
     * 同{@link #get()}的基础逻辑一样,不同的是,通过index参数,
     * 后端会给出清单中指定位置的缓存值
     * 注意:建议通过{@link #get()}方法获取缓存
     *
     * @param index 缓存序列索引
     * @return 返回指定索引位置的值,如果无法获取值,会抛出异常
     */
    @Override
    public Mono<V> get(Mono<Integer> index) {
        return index.flatMap(i -> {
            CacheEntity cacheEntity = cacheMap.get(i);
            //判断缓存是否存在
            if (cacheEntity != null) {
                //如果缓存存在且还处于可用状态,则返回缓存值,同时进行缓存刷新的判断和操作
                return cacheCanBeUsed.test(cacheEntity) ?
                        //缓存处于可用状态,则首先返回值,并检查缓存是否需要更新
                        Mono.just(cacheEntity.getData())
                                //检查是否需要更新
                                .doOnNext(v -> {
                                    if (cacheNeedRefresh.test(cacheEntity)) {
                                        log.info("index:{} need refresh", index);
                                        //需要被更新,则进行更新
                                        this.refresh(i);
                                    }
                                })
                        :
                        //缓存处于不可用状态,则进行强制刷新后返回数据
                        getNewValue(i);

            } else {
                //如果缓存不存在或不可用,则进行刷新处理
                return getNewValue(i);
            }
        });
    }

    /**
     * 手动刷新缓存
     * 调用该方法会手动将所有进入了更新周期的缓存刷新一遍。可以
     * 用于方法并未被频繁访问时,手动更新缓存
     */
    @Override
    public void refresh() {
        BackGroundRefreshConfigProperties.ConfigEntity configEntity = getConfigEntity.get();
        Integer cacheCount = configEntity.getCacheCount();
        List<Integer> refreshIndexes = new ArrayList<>(cacheCount);
        for (int i = 0; i < cacheCount; i++) {
            CacheEntity entity = cacheMap.get(i);
            /*
            需要更新的3中情况:
            1-entity不存在
            2-cacheNeedRefresh=true
            3-cacheCanBeUsed=false
             */
            if (entity == null || cacheNeedRefresh.test(entity) || !cacheCanBeUsed.test(entity)) {
                //如果需要更新,则上锁后放入更新队列
                refreshIndexes.add(i);
            }
        }
        //最后,进行刷新
        refreshAll(Flux.fromIterable(refreshIndexes));
    }

    /**
     * 手动刷新缓存
     * 需要注意的是,同{@link #refresh()}不同,指定位索引位置
     * 会被强制刷新,并不会判断是否已经过期
     * 这里更新之前,建议都上一下锁
     *
     * @param index 缓存的索引位置
     */
    @Override
    public void refresh(Integer index) {
        if (lock(index)) {
            //上锁成功则进行更新
            getNewValue(index).subscribe(v -> log.info("refresh index:{} completed.value:{}", index, gson.toJson(v)));
        }

    }

    /**
     * 强制刷新所有缓存
     * 非常消耗性能的方法,使用时要格外注意!
     */
    @Override
    public void refreshAll() {
        int cacheCount = getConfigEntity.get().getCacheCount();
        refreshAll(Flux.range(0, cacheCount));
    }

    /**
     * 根据索引进行刷新
     * 调用刷新之前,记得都上一下锁
     *
     * @param refreshIndexes 需要被刷新的索引清单
     */
    private void refreshAll(Flux<Integer> refreshIndexes) {
//        refreshIndexes
        refreshIndexes
                //元素延迟
                .delayElements(getConfigEntity.get().getBatchRefreshDelay())
                //从入参中逐个进行刷新
                .subscribe(this::refresh);
    }

    /**
     * 获取一个新值
     * 作为整个功能中,真正刷新缓存的并给出新值的地方
     * 完成了数据获取及解锁的工作
     * 相当于强制刷新缓存,同时返回流
     * <p>
     * 内部会再次进行上锁并执行更新缓存的操作,而忽略
     * CAS的状态。至于是否要根据CAS的状态判断是否要更
     * 新,应当由外部进行实现
     *
     * @param index 索引
     * @return 返回一个新的值
     */
    private Mono<V> getNewValue(Integer index) {
        /*
        这里的CAS锁使用机制说明:
        由于在这个位置是进行强制刷新的,所以无论原来的状态是否被锁,这里都需要进行上锁
        所以在此处直接使用了set方法,而无视原有状态
         */
        lockEnforce(index);
        return getCacheValue.get()
                //获取到值后,将内容对缓存进行更新
                .doOnNext(v -> {
                    //将值放入缓存中
                    cacheMap.put(index, new CacheEntity(System.currentTimeMillis(), v));
                })
                //通过Finally方式进行解锁
                .doFinally(signalType ->
                        unlock(index)
                );
    }

    /**
     * 上锁操作
     *
     * @param index 对应的索引位置
     * @return 返回是否上锁成功
     */
    private boolean lock(int index) {
        return cacheChangingSingleMap.computeIfAbsent(index, v -> new AtomicBoolean(false))
                //获取到的锁通过CAS机制进行变更。如果变更失败,则说明已经有逻辑在进行更新了
                .compareAndSet(false, true);
    }

    /**
     * 强制上锁操作
     *
     * @param index 对应的索引位置
     */
    private void lockEnforce(int index) {
        cacheChangingSingleMap
                //如果没有,则生成一个新的
                //这里直接构建一个带有true值的变量
                .computeIfAbsent(index, v -> new AtomicBoolean(true)).set(true);
    }

    /**
     * 解锁操作
     *
     * @param index 对应的索引位置
     */
    private void unlock(int index) {
        cacheChangingSingleMap
                //如果没有,则生成一个新的
                //这里直接构建一个带有true值的变量
                .computeIfAbsent(index, v -> new AtomicBoolean(false)).set(false);
    }

    /**
     * 缓存实体,包含了一个时间标志位,用于记录缓存数据产生时间,判断有效性
     */
    @AllArgsConstructor
    @Getter
    public class CacheEntity {
        private final long dataUpdateTime;
        private final V data;
    }

}

配置类

配合工厂类中各个功能所需的配置,我们使用了一个配置类来完成配置的调控

package com.ly.train.sb.common.config.properties;

import com.ly.train.sb.common.constants.config.ConfigPropertiesPrefixConstants;
import lombok.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;

import java.time.Duration;

/**
 * BackGroundRefreshConfigProperties
 * 服务于BaseBackgroundRefreshCacheServiceFactory的配置类
 *
 * @author John Chen
 * @since 2021/2/5
 */
@Setter
@Slf4j
@ConfigurationProperties(ConfigPropertiesPrefixConstants.Application.BACKGROUND_REFRESH)
public class BackGroundRefreshConfigProperties {
    /**
     * 默认Config,用于配置异常,拿不到有效数据时
     */
    private final static ConfigEntity DEFAULT_CONFIG_ENTITY = new ConfigEntity(60L, 6000L, 1, Duration.ofMillis(1));


    /**
     * C的Cookies缓存
     */
    private ConfigEntity cCookies;


    public ConfigEntity getCCookies() {
        return czpcCookies == null ? DEFAULT_CONFIG_ENTITY : czpcCookies;
    }

    @AllArgsConstructor
    @NoArgsConstructor
    @Setter
    @ToString
    public static class ConfigEntity {
        /**
         * 缓存刷新周期(秒)
         * 超过当前数值的缓存需要进行刷新
         */
        private Long needRefreshSec;
        /**
         * 最长有效期(秒)
         * 超过maxValidPeriod配置的缓存,无法使用,需要刷新后才能继续使用
         * 通常来说,该数值需要配置>needRefreshSec值
         */
        private Long maxValidPeriod;
        /**
         * 缓存值个数
         */
        private Integer cacheCount;
        /**
         * 当需要批量刷新的时候,每个刷新任务的执行间隔
         * 注意,实际使用时,参数通过毫秒数控制,则最小为1ms间隔。那么1s最多能够进行1000次刷新
         * 一定要在实际使用中注意这一点!即批量刷新的性能上线必须满足实际刷新频次需求
         */
        private Duration batchRefreshDelay;

        /**
         * 通过毫秒数配置batchRefreshDelay参数
         *
         * @param batchRefreshDelayMillis 批量刷新间隔秒数
         */
        public void setBatchRefreshDelay(Long batchRefreshDelayMillis) {
            this.batchRefreshDelay = Duration.ofMillis(batchRefreshDelayMillis);
        }

        public Long getNeedRefreshSec() {
            if (needRefreshSec == null || needRefreshSec < 1) {
                log.error("needRefreshSec is null or error,use default config.");
                return DEFAULT_CONFIG_ENTITY.needRefreshSec;
            } else {
                return needRefreshSec;
            }
        }

        public Long getMaxValidPeriod() {
            if (maxValidPeriod == null || maxValidPeriod < 1) {
                log.error("maxValidPeriod is null or error,use default config.");
                return DEFAULT_CONFIG_ENTITY.maxValidPeriod;
            } else {
                return maxValidPeriod;
            }
        }

        public Integer getCacheCount() {
            if (cacheCount == null || cacheCount < 1) {
                log.error("cacheCount is null or error,use default config.");
                return DEFAULT_CONFIG_ENTITY.cacheCount;
            } else {
                return cacheCount;
            }
        }

        public Duration getBatchRefreshDelay() {
            if (batchRefreshDelay == null || batchRefreshDelay.toMillis() < 1) {
                log.error("batchRefreshDelay is null or error,use default config.");
                return DEFAULT_CONFIG_ENTITY.batchRefreshDelay;
            } else {
                return batchRefreshDelay;
            }
        }
    }
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术流奶爸奶爸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值