模拟Spring缓存机制

发布时间:2023年12月27日
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

学习必须往深处挖,挖的越深,基础越扎实!

阶段1、深入多线程

阶段2、深入多线程设计模式

阶段3、深入juc源码解析

阶段4、深入jdk其余源码解析

阶段5、深入jvm源码解析

在日常开发中,缓存是提高系统吞吐量的常见手段。不论是使用Guava、Redis还是别的缓存,工程中都不可避免地要编写以下模板代码:

public User method(Long id, boolean useCache) {
    
	if (useCache) {
        String cacheKey = this.generateKey(id);
        String cachedValue = cache.get(cacheKey);
        if (StringUtils.isNotEmpty(cachedValue)) {
            // 已缓存,直接返回
            return JSON.parseObject(cachedValue, User.class);
        } else {
            // 执行并缓存
            User user = userService.getById(id);
            cache.put(cacheKey, JSON.toJSONString(user));
            return result;
        }
    }
    
    return userService.getById(id);
}

为此SpringBoot根据JSR107抽象出了一套缓存机制,其中大家最熟悉的可能是@Cacheable/@CachePut等注解。

今天我们再造一个轮子,模拟SpringBoot的这套缓存机制。

不使用缓存:

使用缓存:

设计思路

  • ApplicationContext:简单模拟Spring容器
  • HandlerFactory:增强工厂,这里主要进行缓存功能的增强
  • DefaultKeyGenerator:解析CacheKeyTemplate,比如把obm:member:{uid}:{platform}替换成obm:member:10086:1

模拟简单的Spring容器

public class ApplicationContext {

    @SneakyThrows
    public Object getBean(String name) {
        // 根据全类名,得到目标类的Class对象
        Class<?> clazz = Class.forName(name);

        // 根据Class反射创建目标对象
        Object target = clazz.newInstance();

        // 获取代理对象
        return Proxy.newProxyInstance(
                clazz.getClassLoader(),  // 1.类加载器
                clazz.getInterfaces(),   // 2.需要代理的接口
                HandlerFactory.getCacheInvocationHandler(target, new LocalCache()) // 3.增强逻辑
        );
    }

}

缓存代理增强

public class HandlerFactory {

    /**
     * 获取缓存增强
     *
     * @param target 目标对象
     * @param cache  缓存执行器
     * @return
     */
    public static InvocationHandler getCacheInvocationHandler(final Object target, final Cache cache) {
        return new InvocationHandler() {
            @Override
            public Object invoke(Object proxy1, Method method, Object[] args) throws Throwable {

                // 目标方法是否有@Cacheable
                Method targetMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
                boolean useCache = targetMethod.isAnnotationPresent(Cacheable.class);

                if (useCache) {
                    String cacheKey = DefaultKeyGenerator.generate(targetMethod, args);
                    String cachedValue = cache.get(cacheKey);
                    if (StringUtils.isNotEmpty(cachedValue)) {
                        // 已缓存,直接返回
                        return JSON.parseObject(cachedValue, targetMethod.getReturnType());
                    } else {
                        // 执行并缓存
                        Object result = Optional.ofNullable(method.invoke(target, args))
                                .orElse(createEmptyObject(method.getReturnType()));
                        cache.put(cacheKey, JSON.toJSONString(result));
                        return result;
                    }
                }

                // 否则直接invoke目标方法
                return method.invoke(target, args);
            }
        };
    }

    private static Object createEmptyObject(Class<?> returnType) {
        try {
            return returnType.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            System.out.println("createEmptyObject error");
            return null;
        }
    }
}

缓存机制

注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
    String key();
}

缓存

/**
 * 缓存接口
 */
public interface Cache {
    /**
     * 获取缓存
     *
     * @param cacheKey
     * @return
     */
    String get(String cacheKey);

    /**
     * 设置缓存
     *
     * @param cacheKey
     * @param value
     */
    void put(String cacheKey, String value);
}

为了简化代码,这里就不引入Redis了,直接用Map代替。如果后期有需要,可以自行编写RedisCache implements Cache。

/**
 * 本地缓存,充当Redis
 */
public class LocalCache implements Cache {

    private final Map<String, String> LOCAL_CACHE = new ConcurrentHashMap<>();

    @Override
    public String get(String cacheKey) {
        return LOCAL_CACHE.get(cacheKey);
    }

    @Override
    public void put(String cacheKey, String value) {
        System.out.println("使用了缓存, cacheKey=" + cacheKey);
        LOCAL_CACHE.put(cacheKey, value);
    }
}

缓存key生成器

/**
 * 默认的CacheKey生成器
 */
public final class DefaultKeyGenerator {

    private static final Pattern CACHE_KEY_PATTERN = Pattern.compile("(?<=\\{)(.+?)(?=\\})");

    /**
     * 生成缓存key
     *
     * @param method 目标方法
     * @param args   目标方法参数
     * @return
     */
    public static String generate(Method method, Object[] args) {
        String cacheKey = method.getAnnotation(Cacheable.class).key();

        // 解析 obm:member:{uid}:{platform} 得到 [uid, platform]
        List<String> placeholders = parsePlaceholder(cacheKey);

        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            if (placeholders.contains(parameters[i].getName())) {
                // 将占位符替换为变量值
                cacheKey = cacheKey.replace(parameters[i].getName(), String.valueOf(args[i]));
            }
        }

        return cacheKey;
    }


    private static List<String> parsePlaceholder(String cacheKeyTemplate) {
        Matcher matcher = CACHE_KEY_PATTERN.matcher(cacheKeyTemplate);
        List<String> fields = new ArrayList<>();
        while (matcher.find()) {
            fields.add(matcher.group());
        }
        return fields;
    }

    /**
     * 测试
     *
     * @param args
     */
    public static void main(String[] args) {
        Pattern pattern = Pattern.compile("(?<=\\{)(.+?)(?=\\})");

        Matcher matcher = pattern.matcher("{uid}_{name}");
        while (matcher.find()) {
            System.out.println(matcher.group());
        }


        String format = MessageFormat.format("obm:{0}:{1}", "2", "3");
        System.out.println(format);
    }

}

测试案例

public interface UserService {
    UserTO getUser(Long uid, Long platform);
}

public class UserServiceImpl implements UserService {

    @Cacheable(key = "obm:member:{platform}:{uid}")
    @Override
    public UserTO getUser(Long uid, Long platform) {
        System.out.println("调用UserService获取User, uid:" + uid);
        return new UserTO("peter");
    }

}

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class UserTO implements Serializable {
    private String username;
}

public class CacheTest {

    public static void main(String[] args) {

        ApplicationContext applicationContext = new ApplicationContext();
        UserService userService = (UserService) applicationContext.getBean("com.bravo.tech_share.september.service.UserServiceImpl");

        // 运行五次,如果开启缓存,那么只有第一次会发出实际请求,后续请求均从缓存读取
        for (int i = 0; i < 5; i++) {
            UserTO user = userService.getUser(10086L, 1L);
            System.out.println(JSON.toJSONString(user));
        }

    }
}

需要说明的是:

  • SpringBoot的缓存机制有好几层抽象,比如CacheProvider、CacheManager、Cache等,而上面为了简化,只抽象了Cache
  • 缓存key的生成规则是最麻烦的,特别是如何应对各种不同的业务需求。SpringBoot抽取了KeyGenerator接口,允许使用者通过@Bean自定义KeyGenerator

最后请大家思考一个问题:

public class UserServiceImpl implements UserService {

    @Cacheable(key = "obm:member:{uid}:{platform}")
    @Override
    public UserTO getUser(User user, Long platform) {
        // ...
    }

}

getUser()的参数从uid+platform变成了user+platform,但我还是希望以obm:member:uid:platform的方式构建cacheKey,该怎么做呢?

不妨思考一下,uid其实可以通过user.getUid()得到。如果@Cacheable的key表达式能支持"obm:member:{user.uid}:{platform}",通过user.uid的形式解析出uid,是不是就能解决问题了?此时就要求我们的KeyGenerator支持属性导航了。所谓属性导航,就是通过“点语法”的形式,通过一层层导航,最终获取目标字段,比如:province.city.area最终获取到某省某市的某个地区。

当然,我的观点是尽量不去要满足这种需求。实现起来比较繁琐,性能也不高。我在文章末尾尝试实现了一版,大家随便看看知道大概原理就行了。

拓展:支持属性导航

/**
 * 默认的CacheKey生成器,支持obm:member:{user.username}:{platform})
 */
public final class DefaultKeyGenerator {

    private static final Pattern CACHE_KEY_PATTERN = Pattern.compile("(?<=\\{)(.+?)(?=\\})");

    /**
     * 生成缓存key
     *
     * @param method 目标方法
     * @param args   目标方法参数
     * @return
     */
    public static String generate(Method method, Object[] args) {
        String cacheKey = method.getAnnotation(Cacheable.class).key();

        // 解析 obm:member:{user.uid}:{platform} 得到 [user.uid, platform]
        List<String> placeholders = parsePlaceholder(cacheKey);

        Parameter[] parameters = method.getParameters();
        for (String placeholder : placeholders) {
            if (placeholder.contains(".")) {
                // 可以尝试递归,但没必要,约定好最多两级即可
                cacheKey = parseCascadePlaceholder(cacheKey, placeholder, parameters, args);
            } else {
                cacheKey = parseSimplePlaceholder(cacheKey, placeholder, parameters, args);
            }
        }

        return cacheKey;
    }

    private static String parseSimplePlaceholder(String cacheKey, String placeholder, Parameter[] parameters, Object[] args) {
        for (int i = 0; i < parameters.length; i++) {
            if (parameters[i].getName().equals(placeholder)) {
                cacheKey = cacheKey.replace("{" + placeholder + "}", String.valueOf(args[i]));
                break;
            }
        }
        return cacheKey;
    }

    private static String parseCascadePlaceholder(String cacheKey, String placeholder, Parameter[] parameters, Object[] args) {
        for (int i = 0; i < parameters.length; i++) {
            Class<?> parameterType = parameters[i].getType();
            if (isSimpleType(parameterType)) {
                continue;
            }
            Object arg = args[i];
            String[] split = StringUtils.split(placeholder, ".");
            for (Field declaredField : parameterType.getDeclaredFields()) {
                if (declaredField.getName().equals(split[1])) {
                    declaredField.setAccessible(true);
                    try {
                        Object fieldValue = declaredField.get(arg);
                        return cacheKey.replace("{" + placeholder + "}", String.valueOf(fieldValue));
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return cacheKey;
    }

    private static boolean isSimpleType(Class<?> parameterType) {
        // 基础类型
        if (parameterType.isPrimitive()) {
            return true;
        }

        // 包装类型
        if (Long.class.equals(parameterType)) {
            return true;
        } else if (Integer.class.equals(parameterType)) {
            return true;
        } else if (String.class.equals(parameterType)) {
            return true;
        } else if (Boolean.class.equals(parameterType)) {
            return true;
        } else if (Double.class.equals(parameterType)) {
            return true;
        } else if (BigDecimal.class.equals(parameterType)) {
            return true;
        }

        return false;
    }

    private static List<String> parsePlaceholder(String cacheKeyTemplate) {
        Matcher matcher = CACHE_KEY_PATTERN.matcher(cacheKeyTemplate);
        List<String> fields = new ArrayList<>();
        while (matcher.find()) {
            fields.add(matcher.group());
        }
        return fields;
    }

}

如果想提高性能,或许可以考虑提前缓存元数据字段等,但从效率角度考虑,不是很推荐太复杂的cacheKey生成策略。

文章来源:https://blog.csdn.net/smart_an/article/details/135236127
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。