JetCache源码解析——缓存处理

这篇具有很好参考价值的文章主要介绍了JetCache源码解析——缓存处理。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

在Java技术体系中,如果想要在不改变已有代码逻辑的情况下,对已有的函数进行功能增强,一般可以使用两种方式,如AOP(Aspect Oriented Programming),即面向切面编程,以及代理模式,如使用JDK 动态代理或 CGLIB 动态代理。例如Mybatis的源码中同时使用了JDK 动态代理或 CGLIB 动态代理,但是对于Mapper接口是使用JDK动态代理模式。在JetCache则使用AOP的方式,在spring环境下,使用@Cached注解可以为一个方法添加缓存,@CacheUpdate用于更新缓存,@CacheInvalidate用于移除缓存元素。注解可以加在接口上也可以加在类上,加注解的类必须是一个spring bean,例如:

public interface UserService {
    @Cached(name="userCache.", key="#userId", expire = 3600)
    User getUserById(long userId);

    @CacheUpdate(name="userCache.", key="#user.userId", value="#user")
    void updateUser(User user);

    @CacheInvalidate(name="userCache.", key="#userId")
    void deleteUser(long userId);
}

Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spring创建代理的规则为:

  1. 默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了
  2. 当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB

CacheAdvisor类 

JetCache源码解析——缓存处理,缓存

切面选择AbstractBeanFactoryPointcutAdvisor实现,切点是通过StaticMethodMatcherPointcut匹配包含Cached、CacheUpdate和CacheInvalidate注解的方法。通过实现MethodInterceptor接口实现操作相应函数的增强逻辑。CacheAdvisor是AbstractBeanFactoryPointcutAdvisor的子类,源码如下:

/**
 * 缓存顾问类,继承自AbstractBeanFactoryPointcutAdvisor。
 */
public class CacheAdvisor extends AbstractBeanFactoryPointcutAdvisor {

    /**
     * 缓存顾问的bean名称。
     */
    public static final String CACHE_ADVISOR_BEAN_NAME = "jetcache2.internalCacheAdvisor";

    @Autowired
    private ConfigMap cacheConfigMap; // 注入缓存配置映射

    private String[] basePackages; // 基础包路径

    /**
     * 获取Pointcut对象。
     *
     * @return Pointcut对象
     */
    @Override
    public Pointcut getPointcut() {
        CachePointcut pointcut = new CachePointcut(basePackages); // 创建CachePointcut对象
        pointcut.setCacheConfigMap(cacheConfigMap); // 设置CachePointcut的缓存配置映射
        return pointcut;
    }

    /**
     * 设置缓存配置映射。
     *
     * @param cacheConfigMap 缓存配置映射
     */
    public void setCacheConfigMap(ConfigMap cacheConfigMap) {
        this.cacheConfigMap = cacheConfigMap;
    }

    /**
     * 设置基础包路径。
     *
     * @param basePackages 基础包路径
     */
    public void setBasePackages(String[] basePackages) {
        this.basePackages = basePackages;
    }
}

在上面的类中会设置JetCache扫描的基础包路径,缓存配置信息。并实现了PointcutAdvisor接口的getPointcut()函数,PointcutAdvisor接口是Advisor的子接口,Advisor是Spring AOP的顶层抽象,用来管理Advice和Pointcut,所以毫无疑问PointcutAdvisor接口也是用来管理Advice和Pointcut的。PointcutAdvisor源码如下:

/**
 * 所有由切点驱动的Advisor的超接口。
 * 这涵盖了几乎所有除了引入Advisor之外的Advisor,
 * 对于方法级别的匹配不适用。
 *
 * @author Rod Johnson
 */
public interface PointcutAdvisor extends Advisor {

    /**
     * 获取驱动该顾问的Pointcut。
     */
    Pointcut getPointcut();

}

CachePointcut类

在上面的CacheAdvisor类中实现了PointcutAdvisor接口的getPointcut()函数,并创建了CachePointcut类的实例。接下来,我们需要先看一下CachePointcut的类图。

JetCache源码解析——缓存处理,缓存

 Pointcut接口

Pointcut接口有两个接口方法,分别用于加载ClassFilter和MethodMatcher接口实例,并通过这两个实例实现切入点的逻辑功能。

/**
 * 核心Spring切点抽象类。
 *
 * <p>切点由{@link ClassFilter}和{@link MethodMatcher}组成。
 * 这些基本概念和一个Pointcut可以通过{@link org.springframework.aop.support.ComposablePointcut}组合起来。
 *
 * @author Rod Johnson
 * @see ClassFilter
 * @see MethodMatcher
 * @see org.springframework.aop.support.Pointcuts
 * @see org.springframework.aop.support.ClassFilters
 * @see org.springframework.aop.support.MethodMatchers
 */
public interface Pointcut {

    /**
     * 返回该点切的ClassFilter。
     * @return ClassFilter(永远不会为null)
     */
    ClassFilter getClassFilter();

    /**
     * 返回该点切的MethodMatcher。
     * @return MethodMatcher(永远不会为null)
     */
    MethodMatcher getMethodMatcher();

    /**
     * 常规Pointcut实例,始终匹配。
     */
    Pointcut TRUE = TruePointcut.INSTANCE;

}

 ClassFilter接口

ClassFilter的matches方法定义判断某个类是否需要被纳入切面,源码如下:

/**
 * 过滤器,用于限制目标类集中对切点或引入的匹配。
 *
 * <p>可用于作为{@link Pointcut}的一部分或针对整个{@link IntroductionAdvisor}的目标。
 *
 * <p>此类的具体实现通常应提供适当的{@link Object#equals(Object)}和{@link Object#hashCode()}
 * 的实现,以便允许在缓存场景中使用过滤器 - 例如,在由CGLIB生成的代理中。
 *
 * @author 罗德·约翰逊
 * @see Pointcut
 * @see MethodMatcher
 */
@FunctionalInterface
public interface ClassFilter {

    /**
     * 是否应应用切点到给定的接口或目标类?
     * @param clazz 候选目标类
     * @return 是否应应用给定的目标类
     */
    boolean matches(Class<?> clazz);


    /**
     * 匹配所有类的类过滤器的规范实例。
     */
    ClassFilter TRUE = TrueClassFilter.INSTANCE;
}

MethodMatcher接口 

 MethodMatcher的matches方法则是定义判断某个方法是否需要被纳入切面。MethodMatcher源码如下:

/**
 * 是`Pointcut`的一部分:用于检查目标方法是否有资格应用建议。
 *
 * <p>MethodMatcher可以静态地(`statically`)或动态地(`dynamically`)进行评估(`evaluated`)。
 * 静态匹配涉及方法和(可能)方法属性。动态匹配还提供了特定调用的参数,并且在运行Pointcut上的先前建议时产生的任何效果。
 *
 * <p>如果实现从其`#isRuntime()`方法返回`false`,则可以进行静态评估,并且此方法的所有调用的参数将相同,无论其参数如何。这意味着如果`#isRuntime()`方法返回`false`,则3个参数的`#matches(java.lang.reflect.Method, Class, Object[])`方法将永远不会被调用。
 *
 * <p>如果实现从其2个参数的`#matches(java.lang.reflect.Method, Class)`方法返回`true`并且`#isRuntime()`方法返回`true`,则会在每个相关建议的潜在执行之前立即调用3个参数的`#matches(java.lang.reflect.Method, Class, Object[])`方法,以决定是否应运行建议。之前运行的所有建议,例如拦截器链中的早期拦截器,都已经运行,因此他们在参数或线程局部状态中产生的任何状态更改都将可用。
 *
 * <p>此接口的 concrete 实现通常应适当地提供`Object#equals(Object)`和`Object#hashCode()`,以便允许matcher用作缓存场景中的matcher——例如,在由CGLIB生成的代理中。
 *
 * @author Rod Johnson
 * @since 11.11.2003
 * @see Pointcut
 * @see ClassFilter
 */
public interface MethodMatcher {

    /**
     * 执行静态检查,以确定给定方法是否匹配。
     * <p>如果此方法返回`false`或`#isRuntime()`方法返回`false`,则不会进行运行时检查(即不会调用`#matches(java.lang.reflect.Method, Class, Object[])`)。
     * @param method 候选方法
     * @param targetClass 目标类
     * @return 如果此方法静态匹配,则返回true
     */
    boolean matches(Method method, Class<?> targetClass);

    /**
     * 是否为MethodMatcher动态,即必须在2个参数的matches方法返回true的情况下,最终调用`#matches(java.lang.reflect.Method, Class, Object[])`方法进行运行时检查?
     * <p>可以在创建AOP代理时调用,无需在每次方法调用之前再次调用。
     * @return 如果静态匹配通过,则在3个参数的`#matches(java.lang.reflect.Method, Class, Object[])`方法上的运行时匹配是否需要
     */
    boolean isRuntime();

    /**
     * 检查此方法是否有运行时(动态)匹配,该方法必须静态匹配。
     * <p>仅在2个参数的matches方法针对给定方法和目标类返回true,并且`#isRuntime()`方法返回true时调用该方法。在建议运行之前立即调用,之前在建议链中的任何早期建议已经运行。
     * @param method 候选方法
     * @param targetClass 目标类
     * @param args 方法的方法
     * @return 是否存在运行时匹配
     * @see MethodMatcher#matches(Method, Class)
     */
    boolean matches(Method method, Class<?> targetClass, Object... args);


    /**
     * 与所有方法都匹配的规范实例。
     */
    MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;

}

在上面的接口中分别支持对类的匹配和函数的匹配,那我们先看一下CachePointcut中对于类的匹配,源码如下:

public class CachePointcut extends StaticMethodMatcherPointcut implements ClassFilter {

    private static final Logger logger = LoggerFactory.getLogger(CachePointcut.class);

    private ConfigMap cacheConfigMap;
    private String[] basePackages;

    /**
     * 构造函数
     *
     * @param basePackages 基础包名数组
     */
    public CachePointcut(String[] basePackages) {
        setClassFilter(this);
        this.basePackages = basePackages;
    }

    /**
     * 判断类是否匹配
     *
     * @param clazz 类对象
     * @return 是否匹配
     */
    @Override
    public boolean matches(Class clazz) {
        boolean b = matchesImpl(clazz);
        logger.trace("check class match {}: {}", b, clazz);
        return b;
    }

    /**
     * 实现类是否匹配
     *
     * @param clazz 类对象
     * @return 是否匹配
     */
    private boolean matchesImpl(Class clazz) {
        if (matchesThis(clazz)) {
            return true;
        }
        Class[] cs = clazz.getInterfaces();
        if (cs != null) {
            for (Class c : cs) {
                if (matchesImpl(c)) {
                    return true;
                }
            }
        }
        if (!clazz.isInterface()) {
            Class sp = clazz.getSuperclass();
            if (sp != null && matchesImpl(sp)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 判断类是否匹配
     *
     * @param clazz 类对象
     * @return 是否匹配
     */
    public boolean matchesThis(Class clazz) {
        String name = clazz.getName();
        if (exclude(name)) {
            return false;
        }
        return include(name);
    }

    /**
     * 判断类是否包含在基础包名数组中
     *
     * @param name 类名
     * @return 是否包含
     */
    private boolean include(String name) {
        if (basePackages != null) {
            for (String p : basePackages) {
                if (name.startsWith(p)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * 判断类是否排除
     *
     * @param name 类名
     * @return 是否排除
     */
    private boolean exclude(String name) {
        if (name.startsWith("java")) {
            return true;
        }
        if (name.startsWith("org.springframework")) {
            return true;
        }
        if (name.indexOf("$$EnhancerBySpringCGLIB$$") >= 0) {
            return true;
        }
        if (name.indexOf("$$FastClassBySpringCGLIB$$") >= 0) {
            return true;
        }
        return false;
    }

}

上面的代码逻辑还是比较简单的,主要是判断类的包名是否以配置的JetCache扫描的包路径开头,如果以配置的JetCache扫描的包路径开头就匹配上了。

接下来的matches(Method, Class)函数用于匹配对应类的相应函数,主要是带有注解Cached、CacheUpdate和CacheInvalidate的方法,源码如下:

    /**
     * 判断给定的方法是否与目标类匹配
     *
     * @param method 待判断的方法
     * @param targetClass 目标类
     * @return 如果匹配则返回true,否则返回false
     */
    @Override
    public boolean matches(Method method, Class targetClass) {
        boolean b = matchesImpl(method, targetClass);
        if (b) {
            if (logger.isDebugEnabled()) {
                logger.debug("check method match true: method={}, declaringClass={}, targetClass={}",
                        method.getName(),
                        ClassUtil.getShortClassName(method.getDeclaringClass().getName()),
                        targetClass == null ? null : ClassUtil.getShortClassName(targetClass.getName()));
            }
        } else {
            if (logger.isTraceEnabled()) {
                logger.trace("check method match false: method={}, declaringClass={}, targetClass={}",
                        method.getName(),
                        ClassUtil.getShortClassName(method.getDeclaringClass().getName()),
                        targetClass == null ? null : ClassUtil.getShortClassName(targetClass.getName()));
            }
        }
        return b;
    }

    /**
     * 实现方法匹配的逻辑
     *
     * @param method 待判断的方法
     * @param targetClass 目标类
     * @return 如果匹配则返回true,否则返回false
     */
    private boolean matchesImpl(Method method, Class targetClass) {
        if (!matchesThis(method.getDeclaringClass())) {
            return false;
        }
        if (exclude(targetClass.getName())) {
            return false;
        }
        String key = getKey(method, targetClass);
        CacheInvokeConfig cac = cacheConfigMap.getByMethodInfo(key);
        if (cac == CacheInvokeConfig.getNoCacheInvokeConfigInstance()) {
            return false;
        } else if (cac != null) {
            return true;
        } else {
            cac = new CacheInvokeConfig();
            CacheConfigUtil.parse(cac, method);

            String name = method.getName();
            Class<?>[] paramTypes = method.getParameterTypes();
            parseByTargetClass(cac, targetClass, name, paramTypes);

            if (!cac.isEnableCacheContext() && cac.getCachedAnnoConfig() == null &&
                    cac.getInvalidateAnnoConfigs() == null && cac.getUpdateAnnoConfig() == null) {
                cacheConfigMap.putByMethodInfo(key, CacheInvokeConfig.getNoCacheInvokeConfigInstance());
                return false;
            } else {
                cacheConfigMap.putByMethodInfo(key, cac);
                return true;
            }
        }
    }

在上面的matchesImpl函数中会执行如下几个步骤:

1、调用matchesThis函数判断指定的类的包名是否以配置的JetCache扫描的包路径开头,这是为了排除没有JetCache不会扫描的类。

2、调用exclude函数用于排除Java和Spring自带的类名,以及使用CGLIB动态代理生成的类。

3、根据类和函数信息生成唯一键Key,然后查询缓存cacheConfigMap中对应的缓存调用配置信息。

4、如果cacheConfigMap缺少指定类的指定函数的缓存调用配置信息,则构建CacheInvokeConfig实例,并解析函数的注解信息。

CacheConfigUtil.parse函数会解析函数上的Cached、CacheUpdate和CacheInvalidate注解上的信息。其源码如下:

    /**
     * 解析方法上的注解,并将解析结果存入CacheInvokeConfig对象
     * @param cac CacheInvokeConfig对象,用于存储解析结果
     * @param method 待解析的方法
     * @return 是否存在有效的注解
     */
    public static boolean parse(CacheInvokeConfig cac, Method method) {
        boolean hasAnnotation = false;
        CachedAnnoConfig cachedConfig = parseCached(method);
        if (cachedConfig != null) {
            cac.setCachedAnnoConfig(cachedConfig);
            hasAnnotation = true;
        }
        boolean enable = parseEnableCache(method);
        if (enable) {
            cac.setEnableCacheContext(true);
            hasAnnotation = true;
        }
        List<CacheInvalidateAnnoConfig> invalidateAnnoConfigs = parseCacheInvalidates(method);
        if (invalidateAnnoConfigs != null) {
            cac.setInvalidateAnnoConfigs(invalidateAnnoConfigs);
            hasAnnotation = true;
        }
        CacheUpdateAnnoConfig updateAnnoConfig = parseCacheUpdate(method);
        if (updateAnnoConfig != null) {
            cac.setUpdateAnnoConfig(updateAnnoConfig);
            hasAnnotation = true;
        }

        if (cachedConfig != null && (invalidateAnnoConfigs != null || updateAnnoConfig != null)) {
            throw new CacheConfigException("@Cached不能同时存在@CacheInvalidate或@CacheUpdate: " + method);
        }

        return hasAnnotation;
    }

在上面的方法中,第一步会调用函数parseCached来解析注解@Cached中的信息,源码如下:

    private static CachedAnnoConfig parseCached(Method m) {
        Cached anno = m.getAnnotation(Cached.class);
        if (anno == null) {
            return null;
        }
        CachedAnnoConfig cc = new CachedAnnoConfig();
        cc.setArea(anno.area());
        cc.setName(anno.name());
        cc.setCacheType(anno.cacheType());
        cc.setSyncLocal(anno.syncLocal());
        cc.setEnabled(anno.enabled());
        cc.setTimeUnit(anno.timeUnit());
        cc.setExpire(anno.expire());
        cc.setLocalExpire(anno.localExpire());
        cc.setLocalLimit(anno.localLimit());
        cc.setCacheNullValue(anno.cacheNullValue());
        cc.setCondition(anno.condition());
        cc.setPostCondition(anno.postCondition());
        cc.setSerialPolicy(anno.serialPolicy());
        cc.setKeyConvertor(anno.keyConvertor());
        cc.setKey(anno.key());
        cc.setDefineMethod(m);

        CacheRefresh cacheRefresh = m.getAnnotation(CacheRefresh.class);
        if (cacheRefresh != null) {
            RefreshPolicy policy = parseRefreshPolicy(cacheRefresh);
            cc.setRefreshPolicy(policy);
        }

        CachePenetrationProtect protectAnno = m.getAnnotation(CachePenetrationProtect.class);
        if (protectAnno != null) {
            PenetrationProtectConfig protectConfig = parsePenetrationProtectConfig(protectAnno);
            cc.setPenetrationProtectConfig(protectConfig);
        }

        return cc;
    }

异步调用缓存保护注解@CachePenetrationProtect和缓存刷新注解@CacheRefresh仅支持使用@Cached的函数上可用,所以JetCache就将其在一个函数中实现了,不过分开实现可能会更合理。

第二步就会调用parseEnableCache函数解析@EnableCache注解,如果相应的函数存在@EnableCache注解,则CacheInvokeConfig实例的enableCacheContext字段为true。如果在需要使用缓存的函数上加了@Cached注解后再加@EnableCache注解可能会嫌麻烦,实际上不在函数上加注解@EnableCache也能启用缓存,后面再详细介绍。

第三步开始调用parseCacheInvalidates函数解析@CacheInvalidate注解和@CacheInvalidateContainer注解,@CacheInvalidateContainer注解中可以定义多个@CacheInvalidate。而一个函数只能有一个@CacheInvalidate注解,当一个函数的修改需要将多种缓存数据清除时就可以选择@CacheInvalidateContainer注解,当然@CacheInvalidate注解中的multi可以支持清空多个key的缓存,但需要在key字段中指定获取多个删除key的表达式。源码如下:

    public static List<CacheInvalidateAnnoConfig> parseCacheInvalidates(Method m) {
        List<CacheInvalidateAnnoConfig> annoList = null;
        CacheInvalidate ci = m.getAnnotation(CacheInvalidate.class);
        if (ci != null) {
            annoList = new ArrayList<>(1);
            annoList.add(createCacheInvalidateAnnoConfig(ci, m));
        } else {
            CacheInvalidateContainer cic = m.getAnnotation(CacheInvalidateContainer.class);
            if (cic != null) {
                CacheInvalidate[] cacheInvalidates = cic.value();
                annoList = new ArrayList<>(cacheInvalidates.length);
                for (CacheInvalidate cacheInvalidate : cacheInvalidates) {
                    annoList.add(createCacheInvalidateAnnoConfig(cacheInvalidate, m));
                }
            }
        }
        return annoList;
    }

第四步就是调用parseCacheUpdate函数解析@CacheUpdate注解,源码如下:


    private static CacheUpdateAnnoConfig parseCacheUpdate(Method m) {
        CacheUpdate anno = m.getAnnotation(CacheUpdate.class);
        if (anno == null) {
            return null;
        }
        CacheUpdateAnnoConfig cc = new CacheUpdateAnnoConfig();
        cc.setArea(anno.area());
        cc.setName(anno.name());
        if (cc.getName() == null || cc.getName().trim().equals("")) {
            throw new CacheConfigException("name is required for @CacheUpdate: " + m.getClass().getName() + "." + m.getName());
        }
        cc.setKey(anno.key());
        cc.setValue(anno.value());
        if (cc.getValue() == null || cc.getValue().trim().equals("")) {
            throw new CacheConfigException("value is required for @CacheUpdate: " + m.getClass().getName() + "." + m.getName());
        }
        cc.setCondition(anno.condition());
        cc.setMulti(anno.multi());
        cc.setDefineMethod(m);
        return cc;
    }

5、调用parseByTargetClass函数获取指定类的父类或接口上的缓存配置信息。源码如下:


    private void parseByTargetClass(CacheInvokeConfig cac, Class<?> clazz, String name, Class<?>[] paramTypes) {
        if (!clazz.isInterface() && clazz.getSuperclass() != null) {
            parseByTargetClass(cac, clazz.getSuperclass(), name, paramTypes);
        }
        Class<?>[] intfs = clazz.getInterfaces();
        for (Class<?> it : intfs) {
            parseByTargetClass(cac, it, name, paramTypes);
        }

        boolean matchThis = matchesThis(clazz);
        if (matchThis) {
            Method[] methods = clazz.getDeclaredMethods();
            for (Method method : methods) {
                if (methodMatch(name, method, paramTypes)) {
                    CacheConfigUtil.parse(cac, method);
                    break;
                }
            }
        }
    }

6、将上面解析到缓存函数配置信息保存到缓存cacheConfigMap中。如果函数带有Cached、CacheUpdate和CacheInvalidate等注解时matchesImpl函数就返回true,否则返回false。

至此,针对带有Cached、CacheUpdate和CacheInvalidate等注解的函数就可以纳入到Spring的切面,当对应的函数被调用时就会调用CacheAdvisor类的getAdvice()函数获取对应的切面Advice,接下来就是要定义一个Advice增强器用于提供加载缓存、更新缓存和删除缓存数据的能力。

JetCacheInterceptor类

MethodInterceptor是AOP项目中的拦截器,它拦截的目标是方法。实现MethodInterceptor拦截器大致也分为两种,一种是实现MethodInterceptor接口,另一种利用AspectJ的注解或配置。JetCache采用的是使用JetCacheInterceptor类来实现MethodInterceptor接口。源码如下:

/**
 * @author huangli
 */
public class JetCacheInterceptor implements MethodInterceptor, ApplicationContextAware {

    private static final Logger logger = LoggerFactory.getLogger(JetCacheInterceptor.class);

    @Autowired
    private ConfigMap cacheConfigMap;  // 缓存配置映射

    private ApplicationContext applicationContext;  // 上下文对象

    private GlobalCacheConfig globalCacheConfig;  // 全局缓存配置

    ConfigProvider configProvider;  // 配置提供者

    CacheManager cacheManager;  // 缓存管理器

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;  // 设置上下文对象
    }

    @Override
    public Object invoke(final MethodInvocation invocation) throws Throwable {
        if (configProvider == null) {
            configProvider = applicationContext.getBean(ConfigProvider.class);  // 获取配置提供者
        }
        if (configProvider != null && globalCacheConfig == null) {
            globalCacheConfig = configProvider.getGlobalCacheConfig();  // 获取全局缓存配置
        }
        if (globalCacheConfig == null || !globalCacheConfig.isEnableMethodCache()) {
            return invocation.proceed();  // 执行目标方法
        }
        if (cacheManager == null) {
            cacheManager = applicationContext.getBean(CacheManager.class);  // 获取缓存管理器
            if (cacheManager == null) {
                logger.error("There is no cache manager instance in spring context");  // 输出错误日志
                return invocation.proceed();  // 执行目标方法
            }
        }

        Method method = invocation.getMethod();  // 获取目标方法
        Object obj = invocation.getThis();  // 获取目标对象
        CacheInvokeConfig cac = null;  // 缓存调用配置
        if (obj != null) {
            String key = CachePointcut.getKey(method, obj.getClass());  // 获取缓存键
            cac  = cacheConfigMap.getByMethodInfo(key);  // 获取对应的缓存配置
        }

        /*
        if(logger.isTraceEnabled()){
            logger.trace("JetCacheInterceptor invoke. foundJetCacheConfig={}, method={}.{}(), targetClass={}",
                    cac != null,
                    method.getDeclaringClass().getName(),
                    method.getName(),
                    invocation.getThis() == null ? null : invocation.getThis().getClass().getName());
        }
        */

        if (cac == null || cac == CacheInvokeConfig.getNoCacheInvokeConfigInstance()) {
            return invocation.proceed();  // 执行目标方法
        }

        CacheInvokeContext context = configProvider.newContext(cacheManager).createCacheInvokeContext(cacheConfigMap);  // 创建缓存调用上下文
        context.setTargetObject(invocation.getThis());  // 设置目标对象
        context.setInvoker(invocation::proceed);  // 设置调用者
        context.setMethod(method);  // 设置目标方法
        context.setArgs(invocation.getArguments());  // 设置参数
        context.setCacheInvokeConfig(cac);  // 设置缓存调用配置
        context.setHiddenPackages(globalCacheConfig.getHiddenPackages());  // 设置隐藏包
        return CacheHandler.invoke(context);  // 执行缓存处理
    }

    public void setCacheConfigMap(ConfigMap cacheConfigMap) {
        this.cacheConfigMap = cacheConfigMap;  // 设置缓存配置映射
    }

}

在上面的JetCacheInterceptor类中,cacheConfigMap会缓存每个缓存实例的配置信息,就是是通过上面解析函数中带有的@Cached、@CacheUpdate和@CacheInvalidate等注解中定义的配置信息。JetCacheInterceptor类的invoke函数主要实现如下几个步骤:

1、获取全局缓存配置实例globalCacheConfig,如果全局配置中配置了不启用缓存,就执行目标方法。启用缓存的全局配置就是在例如Application类上加一个@EnableMethodCache注解,设置一下JetCache扫描的包名前缀。

2、获取MethodInvocation类型参数的目标方法和目标对象信息,然后根据目标类和目标方法获取目标方法的缓存配置信息。如果目标方法不存在缓存配置信息,即没有@Cached、@CacheUpdate或@CacheInvalidate注解,则执行执行目标方法。源码如下:

        Method method = invocation.getMethod();  // 获取目标方法
        Object obj = invocation.getThis();  // 获取目标对象
        CacheInvokeConfig cac = null;  // 缓存调用配置
        if (obj != null) {
            String key = CachePointcut.getKey(method, obj.getClass());  // 获取缓存键
            cac  = cacheConfigMap.getByMethodInfo(key);  // 获取对应的缓存配置
        }

        if (cac == null || cac == CacheInvokeConfig.getNoCacheInvokeConfigInstance()) {
            return invocation.proceed();  // 执行目标方法
        }

3、构建缓存执行上下文,执行CacheHandler类的静态方法invoke(CacheInvokeContext),源码如下:

        CacheInvokeContext context = configProvider.newContext(cacheManager).createCacheInvokeContext(cacheConfigMap);  // 创建缓存调用上下文
        context.setTargetObject(invocation.getThis());  // 设置目标对象
        context.setInvoker(invocation::proceed);  // 设置调用者
        context.setMethod(method);  // 设置目标方法
        context.setArgs(invocation.getArguments());  // 设置参数
        context.setCacheInvokeConfig(cac);  // 设置缓存调用配置
        context.setHiddenPackages(globalCacheConfig.getHiddenPackages());  // 设置隐藏包
        return CacheHandler.invoke(context);  // 执行缓存处理

在进一步分析 CacheHandler类的静态方法invoke之前,我们需要看一下JetCache是如何创建JetCacheInterceptor和CacheAdvisor的Bean,实现逻辑很简单,就是在JetCacheProxyConfiguration中直接实现的,源码如下:

    /**
     * 创建一个名为CACHE_ADVISOR_BEAN_NAME的CacheAdvisor Bean,并设置其角色为BeanDefinition.ROLE_INFRASTRUCTURE。
     * 使用JetCacheInterceptor创建CacheAdvisor的缓存拦截器。
     * 设置CacheAdvisor的缓存拦截器为jetCacheInterceptor。
     * 设置CacheAdvisor的基包为enableMethodCache.getStringArray("basePackages")。
     * 设置CacheAdvisor的顺序为enableMethodCache.getNumber("order")。
     * 返回创建的CacheAdvisor Bean。
     */
    @Bean(name = CacheAdvisor.CACHE_ADVISOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public CacheAdvisor jetcacheAdvisor(JetCacheInterceptor jetCacheInterceptor) {
        CacheAdvisor advisor = new CacheAdvisor();
        advisor.setAdvice(jetCacheInterceptor);
        advisor.setBasePackages(this.enableMethodCache.getStringArray("basePackages"));
        advisor.setOrder(this.enableMethodCache.<Integer>getNumber("order"));
        return advisor;
    }

    /**
     * 创建一个名为jetCacheInterceptor的JetCacheInterceptor Bean,并设置其角色为BeanDefinition.ROLE_INFRASTRUCTURE。
     * 返回创建的JetCacheInterceptor Bean。
     */
    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public JetCacheInterceptor jetCacheInterceptor() {
        return new JetCacheInterceptor();
    }

至此,当目标函数被调用时,Spring就会执行JetCacheInterceptor类的invoke函数,接下来,我们就需要针对CacheHandler类的静态方法invoke(CacheInvokeContext)做进一步的分析。源码如下:

    public static Object invoke(CacheInvokeContext context) throws Throwable {
        if (context.getCacheInvokeConfig().isEnableCacheContext()) {
            try {
                CacheContextSupport._enable();
                return doInvoke(context);
            } finally {
                CacheContextSupport._disable();
            }
        } else {
            return doInvoke(context);
        }
    }

上面的方法会首先判断缓存执行上下文是否启用缓存上下文,只有目标函数上带有@EnableCache注解时,context .getCacheInvokeConfig( ).isEnableCacheContext()才会返回true。当启用缓存上下文时就会在函数doInvoke执行前后分别调用CacheContextSupport类的_enable()和_disable()方法。否则就会直接调用doInvoke函数继续进行处理,doInvoke源码如下:

private static Object doInvoke(CacheInvokeContext context) throws Throwable {
        CacheInvokeConfig cic = context.getCacheInvokeConfig();
        CachedAnnoConfig cachedConfig = cic.getCachedAnnoConfig();
        if (cachedConfig != null && (cachedConfig.isEnabled() || CacheContextSupport._isEnabled())) {
            return invokeWithCached(context);
        } else if (cic.getInvalidateAnnoConfigs() != null || cic.getUpdateAnnoConfig() != null) {
            return invokeWithInvalidateOrUpdate(context);
        } else {
            return invokeOrigin(context);
        }
    }

invokeWithCached

如果目标函数的缓存调用配置实例不为空,即目标方法有@Cached注解,且开启缓存就会调用invokeWithCached方法。注意,CachedAnnoConfig中的isEnabled()函数是取自@Cached注解的enabled,默认为true。方法invokeWithCached的源码如下:

    /**
     * 使用缓存调用方法
     *
     * @param context 缓存调用上下文
     * @return 方法返回值
     * @throws Throwable 可抛出异常
     */
    private static Object invokeWithCached(CacheInvokeContext context) throws Throwable {
        CacheInvokeConfig cic = context.getCacheInvokeConfig();  // 获取缓存调用配置
        CachedAnnoConfig cac = cic.getCachedAnnoConfig();  // 获取缓存注解配置
        Cache cache = context.getCacheFunction().apply(context, cac);  // 获取缓存函数并应用得到缓存
        if (cache == null) {  // 判断缓存是否为空
            logger.error("no cache with name: " + context.getMethod());  // 记录错误日志
            return invokeOrigin(context);  // 调用原始方法
        }

        Object key = ExpressionUtil.evalKey(context, cic.getCachedAnnoConfig());  // 根据表达式计算缓存的key
        if (key == null) {  // 判断key是否为空
            return loadAndCount(context, cache, key);  // 加载并统计缓存
        }

        if (!ExpressionUtil.evalCondition(context, cic.getCachedAnnoConfig())) {  // 判断缓存条件是否满足
            return loadAndCount(context, cache, key);  // 加载并统计缓存
        }

        try {
            CacheLoader loader = new CacheLoader() {  // 创建缓存加载器
                @Override
                public Object load(Object k) throws Throwable {  // 加载缓存
                    Object result = invokeOrigin(context);  // 调用原始方法
                    context.setResult(result);  // 设置调用上下文的结果
                    return result;
                }

                @Override
                public boolean vetoCacheUpdate() {  // 判断是否禁止缓存更新
                    return !ExpressionUtil.evalPostCondition(context, cic.getCachedAnnoConfig());  // 根据表达式判断是否禁止缓存更新
                }
            };
            Object result = cache.computeIfAbsent(key, loader);  // 根据key计算缓存结果
            return result;  // 返回缓存结果
        } catch (CacheInvokeException e) {  // 捕获缓存调用异常
            throw e.getCause();  // 抛出异常原因
        }
    }

这个函数是一个私有的静态方法,用于在给定的缓存上下文中执行方法调用。它首先获取缓存配置和注解配置,然后根据配置获取缓存。如果缓存为空,它会记录错误并调用原始方法。接下来,它通过表达式计算缓存的键,并根据条件判断是否需要加载缓存。如果需要加载,它会创建一个缓存加载器,并使用该加载器计算缓存结果。最后,它会返回缓存结果。如果发生缓存调用异常,它会抛出该异常的原因。

上面的函数主要的核心逻辑概括起来,可以用下面的源码来解释:

Cache cache = context.getCacheFunction().apply(context, cac);  // 获取缓存函数并应用得到缓存
CacheLoader loader = new CacheLoader() {  // 创建缓存加载器
    @Override
    public Object load(Object k) throws Throwable {  // 加载缓存
        Object result = invokeOrigin(context);  // 调用原始方法
        context.setResult(result);  // 设置调用上下文的结果
        return result;
    }

    @Override
    public boolean vetoCacheUpdate() {  // 判断是否禁止缓存更新
       return !ExpressionUtil.evalPostCondition(context, cic.getCachedAnnoConfig());  // 根据表达式判断是否禁止缓存更新
    }
};
Object result = cache.computeIfAbsent(key, loader);  // 根据key计算缓存结果
return result;  // 返回缓存结果

上面的代码逻辑相对比较简单,我们继续看一下computeIfAbsent函数,该函数会先查询Cache对象中是否存在指定的缓存,如果不存在就会调用load函数获取数据并写入Cache对象中。computeIfAbsent函数源码如下:

    @Override
    public V computeIfAbsent(K key, Function<K, V> loader) {
        return computeIfAbsent(key, loader, config().isCacheNullValue());
    }

    @Override
    public V computeIfAbsent(K key, Function<K, V> loader, boolean cacheNullWhenLoaderReturnNull) {
        return AbstractCache.computeIfAbsentImpl(key, loader, cacheNullWhenLoaderReturnNull,
                0, null, this);
    }


    static <K, V> V computeIfAbsentImpl(K key, Function<K, V> loader, boolean cacheNullWhenLoaderReturnNull,
                                               long expireAfterWrite, TimeUnit timeUnit, Cache<K, V> cache) {
        // 获取具体的缓存实现类
        AbstractCache<K, V> abstractCache = CacheUtil.getAbstractCache(cache);
        // 创建带有缓存监听的缓存加载器
        CacheLoader<K, V> newLoader = CacheUtil.createProxyLoader(cache, loader, abstractCache::notify);
        // 获取缓存获取结果
        CacheGetResult<V> r;
        if (cache instanceof RefreshCache) {
            // 如果缓存是 RefreshCache 类型,则通过该类型的具体方法获取缓存值,并添加刷新任务
            RefreshCache<K, V> refreshCache = ((RefreshCache<K, V>) cache);
            r = refreshCache.GET(key);
            refreshCache.addOrUpdateRefreshTask(key, newLoader);
        } else {
            // 否则直接通过缓存的 GET 方法获取缓存值
            r = cache.GET(key);
        }
        if (r.isSuccess()) {
            // 如果获取成功,则返回获取到的缓存值
            return r.getValue();
        } else {
            // 如果获取失败,则根据条件执行加载和更新逻辑
            Consumer<V> cacheUpdater = (loadedValue) -> {
                // 判断是否需要更新缓存
                if(needUpdate(loadedValue, cacheNullWhenLoaderReturnNull, newLoader)) {
                    // 若需要更新,则根据过期时间和时间单位调用缓存的 PUT 方法
                    if (timeUnit != null) {
                        cache.PUT(key, loadedValue, expireAfterWrite, timeUnit).waitForResult();
                    } else {
                        cache.PUT(key, loadedValue).waitForResult();
                    }
                }
            };

            // 加载值
            V loadedValue;
            if (cache.config().isCachePenetrationProtect()) {
                // 如果开启了缓存穿透保护,则同步加载并更新缓存
                loadedValue = synchronizedLoad(cache.config(), abstractCache, key, newLoader, cacheUpdater);
            } else {
                // 否则直接通过缓存加载器加载值,并执行缓存更新逻辑
                loadedValue = newLoader.apply(key);
                cacheUpdater.accept(loadedValue);
            }

            // 返回加载的值
            return loadedValue;
        }
    }

invokeWithInvalidateOrUpdate

如果目标函数的缓存调用配置实例不为空,即目标方法有@CacheUpdate或@CacheInvalidate等注解,且开启缓存就会调用invokeWithInvalidateOrUpdate方法。invokeWithInvalidateOrUpdate方法的源码如下:

    private static Object invokeWithInvalidateOrUpdate(CacheInvokeContext context) throws Throwable {
        Object originResult = invokeOrigin(context);
        context.setResult(originResult);
        CacheInvokeConfig cic = context.getCacheInvokeConfig();

        if (cic.getInvalidateAnnoConfigs() != null) {
            doInvalidate(context, cic.getInvalidateAnnoConfigs());
        }
        CacheUpdateAnnoConfig updateAnnoConfig = cic.getUpdateAnnoConfig();
        if (updateAnnoConfig != null) {
            doUpdate(context, updateAnnoConfig);
        }

        return originResult;
    }

上面的方法首先会执行目标函数,获取相应的结果,然后开始根据目标方法上的@CacheUpdate或@CacheInvalidate注解更新或删除缓存数据。

如果目标方法存在注解@CacheInvalidate,就会调用doInvalidate方法删除缓存数据,源码如下:

    // 清除指定缓存
    private static void doInvalidate(CacheInvokeContext context, List<CacheInvalidateAnnoConfig> annoConfig) {
        for (CacheInvalidateAnnoConfig config : annoConfig) {
            doInvalidate(context, config);
        }
    }

    // 清除单个缓存
    private static void doInvalidate(CacheInvokeContext context, CacheInvalidateAnnoConfig annoConfig) {
        Cache cache = context.getCacheFunction().apply(context, annoConfig);  // 获取指定缓存
        if (cache == null) {
            return;
        }
        boolean condition = ExpressionUtil.evalCondition(context, annoConfig);  // 判断是否满足条件
        if (!condition) {
            return;
        }
        Object key = ExpressionUtil.evalKey(context, annoConfig);  // 获取缓存的键
        if (key == null) {
            return;
        }
        if (annoConfig.isMulti()) {  // 判断是否为多个键
            Iterable it = toIterable(key);  // 将键转为迭代器
            if (it == null) {
                logger.error("jetcache @CacheInvalidate key is not instance of Iterable or array: " + annoConfig.getDefineMethod());
                return;
            }
            Set keys = new HashSet();
            it.forEach(k -> keys.add(k));  // 将键添加到集合中
            cache.removeAll(keys);  // 清除所有指定键的缓存
        } else {
            cache.remove(key);  // 清除指定键的缓存
        }
    }

这段代码定义了两个私有的静态函数:doInvalidate。这两个函数的作用是根据传入的参数,对缓存进行清空操作。其中,doInvalidate函数接受一个CacheInvokeContext对象和一个CacheInvalidateAnnoConfig对象,通过遍历CacheInvalidateAnnoConfig对象的列表,依次调用doInvalidate函数进行清空操作。在doInvalidate函数中,首先通过CacheInvokeContext对象获取缓存对象,并根据CacheInvalidateAnnoConfig对象中的条件判断是否执行清空操作。若满足条件,则根据CacheInvalidateAnnoConfig对象中的配置,对缓存进行清空操作。如果配置中指定了多个key,则将多个key放入Set集合中进行清空操作;如果只指定了一个key,则直接进行清空操作。

如果目标方法存在注解@CacheUpdate,就会调用doUpdate方法更新缓存数据,源码如下:

    // 更新缓存
    private static void doUpdate(CacheInvokeContext context, CacheUpdateAnnoConfig updateAnnoConfig) {
        // 获取缓存
        Cache cache = context.getCacheFunction().apply(context, updateAnnoConfig);
        // 若缓存为空,直接返回
        if (cache == null) {
            return;
        }
        // 判断是否满足更新条件
        boolean condition = ExpressionUtil.evalCondition(context, updateAnnoConfig);
        if (!condition) {
            return;
        }
        // 获取键值
        Object key = ExpressionUtil.evalKey(context, updateAnnoConfig);
        Object value = ExpressionUtil.evalValue(context, updateAnnoConfig);
        // 若键值为空或者获取键值失败,直接返回
        if (key == null || value == ExpressionUtil.EVAL_FAILED) {
            return;
        }
        // 判断是否为批量更新
        if (updateAnnoConfig.isMulti()) {
            // 若值为空,直接返回
            if (value == null) {
                return;
            }
            // 将键值转换为可迭代对象
            Iterable keyIt = toIterable(key);
            Iterable valueIt = toIterable(value);
            // 若键为null,打印错误日志并返回
            if (keyIt == null) {
                logger.error("jetcache @CacheUpdate key is not instance of Iterable or array: " + updateAnnoConfig.getDefineMethod());
                return;
            }
            // 若值为null,打印错误日志并返回
            if (valueIt == null) {
                logger.error("jetcache @CacheUpdate value is not instance of Iterable or array: " + updateAnnoConfig.getDefineMethod());
                return;
            }
            // 转换为列表
            List keyList = new ArrayList();
            List valueList = new ArrayList();
            keyIt.forEach(o -> keyList.add(o));
            valueIt.forEach(o -> valueList.add(o));
            // 若键列表与值列表大小不一致,打印错误日志并返回
            if (keyList.size() != valueList.size()) {
                logger.error("jetcache @CacheUpdate key size not equals with value size: " + updateAnnoConfig.getDefineMethod());
                return;
            } else {
                // 构建键值对映射
                Map m = new HashMap();
                for (int i = 0; i < valueList.size(); i++) {
                    m.put(keyList.get(i), valueList.get(i));
                }
                // 批量更新缓存
                cache.putAll(m);
            }
        } else {
            // 批量更新缓存
            cache.put(key, value);
        }
    }

这个函数是一个用于更新缓存的方法。首先根据传入的参数获取缓存对象,如果缓存为空,则直接返回。然后通过判断条件来确定是否执行更新操作。接着通过调用ExpressionUtil的evalKey方法和evalValue方法来获取键和值。如果键或值获取失败,则直接返回。如果updateAnnoConfig.isMulti()为true,则表示是批量更新操作。这时需要将值转换为可迭代对象,并判断是否为空。接着将键和值分别转换为列表,并进行大小判断。如果不相等,则打印错误日志并返回。如果相等,则构建一个键值对映射,然后将该映射批量更新到缓存中。如果updateAnnoConfig.isMulti()为false,则表示是单个更新操作,直接将键值对更新到缓存中。文章来源地址https://www.toymoban.com/news/detail-790169.html

到了这里,关于JetCache源码解析——缓存处理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【分布式缓存】springboot整合jetcache使用详解

    目录 一、前言 二、多级缓存问题 2.1 缓存分类 2.1.1 本地缓存 2.1.2 分布式缓存

    2024年02月17日
    浏览(45)
  • SpringBoot中集成阿里开源缓存访问框架JetCache实现声明式实例和方法缓存

    SpringBoot中通过自定义缓存注解(AOP切面拦截)实现数据库数据缓存到Redis: SpringBoot中通过自定义缓存注解(AOP切面拦截)实现数据库数据缓存到Redis_霸道流氓气质的博客-CSDN博客 上面讲的通过自定义注解的方式实现查询数据库数据缓存,除此之外还有更好的轮子。 JetCache是由阿里

    2024年02月15日
    浏览(39)
  • Spring源码解析——IOC之循环依赖处理

    循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图所示: 注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。 最全面的Java面试网站

    2024年02月08日
    浏览(53)
  • Python实现数字图像处理算法及源码解析

    Python实现数字图像处理算法及源码解析 数字图像处理在计算机视觉、图像识别等领域中有着广泛的应用,Python作为一门易于上手且功能强大的编程语言,也成为了数字图像处理的首选语言之一。本篇文章将介绍数字图像处理中的常见算法,并提供相应的Python代码实现。 一、

    2024年02月08日
    浏览(48)
  • Spring源码解析(八):bean后置处理器CommonAnnotationBeanPostProcessor

    Spring源码系列文章 Spring源码解析(一):环境搭建 Spring源码解析(二):bean容器的创建、默认后置处理器、扫描包路径bean Spring源码解析(三):bean容器的刷新 Spring源码解析(四):单例bean的创建流程 Spring源码解析(五):循环依赖 Spring源码解析(六):bean工厂后置处理器ConfigurationCla

    2024年02月13日
    浏览(50)
  • Spring后置处理器BeanFactoryPostProcessor与BeanPostProcessor源码解析

    Spring有两种类型的后置处理器,分别是 BeanFactoryPostProcessor 和 BeanPostProcessor ,这里再贴出我画的 Spring 启动过程,可以看看这两种后置处理器在 Spring 启动过程中位置。 BeanFactoryPostProcessor 的 postProcessBeanFactory 方法在 Spring 容器启动时被调用,可以对整个容器中的 BeanDefinition (

    2024年02月13日
    浏览(53)
  • React源码解析18(11)------ 实现多次setState的批处理

    在React中,如果涉及到了多次setState,组件render几次。setState是同步的还是异步的。这是一个很常见的面试题。 而本篇文章,就是主要实现React中,对于这部分的性能优化,我们称之为批处理。例如当我有下面的JSX。 对于当前的点击事件来说,只有最后的setNum(num + 3)是有效的。

    2024年02月11日
    浏览(45)
  • (02)Cartographer源码无死角解析-(19) SensorBridge→雷达点云数据帧处理与坐标系变换(涉及函数重载)

    本人讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下: (02)Cartographer源码无死角解析- (00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/127350885   文 末 正 下 方 中 心 提 供 了 本 人 联 系 方 式

    2024年02月01日
    浏览(68)
  • 缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等名词解析

    一、缓存雪崩 我们可以简单的理解为:由于原有缓存失效,新缓存未到期间 (例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。

    2024年04月15日
    浏览(44)
  • 全面解析缓存应用经典问题

    随着互联网从简单的单向浏览请求,发展为基于用户个性信息的定制化以及社交化的请求,这要求产品需要做到以用户和关系为基础,对海量数据进行分析和计算。对于后端服务来说,意味着用户的每次请求都需要查询用户的个人信息和大量的关系信息,此外大部分场景还需

    2024年02月16日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包