④优雅的缓存框架:SpringCache之多级缓存

198 阅读3分钟

多级缓存可降低系统响应时间,并且降低二级缓存的压力。

本文采用的二级缓存是 Redis、一级缓存是 Caffeine

多级缓存设计

  1. 声明一个 LocalCache 注解,标识此方法需要多级缓存
  2. 自定义一个 CaffeineRedisCache 多级缓存对象
  3. 自定义CacheResolver缓存解析器:将 @Cacheable@CachePut@CacheEvict等注解中的信息解析为CaffeineRedisCache

多级缓存业务流程图

多级缓存业务流程图

声明LocalCache注解

源码地址

/**
 * Cacheable和CacheEvict必须配置一致,如TTL一下,本地缓存配置一致
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LocalCache {

    /**
     * 仅使用本地缓存
     */
    boolean onlyLocal() default false;
    
}

自定义CaffeineRedisCache

源码地址

@Getter
@Setter
public class CaffeineRedisCache extends AbstractValueAdaptingCache {

    /**
     * 一级缓存,若启用一级缓存则设置一个非空实例,反之则设置为null
     */
    protected Cache firstCache;

    /**
     * 二级缓存
     */
    protected Cache secondCache;

    /**
     * Create an {@code AbstractValueAdaptingCache} with the given setting.
     *
     * @param allowNullValues whether to allow for {@code null} values
     */
    public CaffeineRedisCache(boolean allowNullValues) {
        super(allowNullValues);
    }

    @Override
    protected Object lookup(Object key) {
        ValueWrapper valueWrapper = null;
        if (firstCache != null) {
            // 若启用了本地缓存则先从本地缓存查询
            valueWrapper = firstCache.get(key);
        }
        if (valueWrapper == null) {
            if (secondCache == null) {
                return null;
            }
            valueWrapper = secondCache.get(key);
            // 若二级缓存存在,但一级缓存被启用并且不存在key,则put到一级缓存
            if (valueWrapper != null) {
                if (firstCache != null) {
                    firstCache.put(key, valueWrapper.get());
                }
            } else {
                return null;
            }
        }
        return valueWrapper.get();

    }

    @Override
    public String getName() {
        if (firstCache != null) {
            return firstCache.getName();
        }
        if (secondCache != null) {
            return secondCache.getName();
        }
        throw new RuntimeException("Please provide a valid cache.");
    }

    @Override
    public Object getNativeCache() {
        if (firstCache != null) {
            return firstCache.getNativeCache();
        }
        if (secondCache != null) {
            return secondCache.getNativeCache();
        }
        return null;
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        if (firstCache != null) {
            T t = firstCache.get(key, valueLoader);
            if (t != null) {
                return t;
            }
        }
        if (secondCache != null) {
            return secondCache.get(key, valueLoader);
        }
        return null;
    }

    @Override
    public void put(Object key, Object value) {
        if (firstCache != null) {
            firstCache.put(key, value);
        }
        if (secondCache != null) {
            secondCache.put(key, value);
        }
    }

    @Override
    public void evict(Object key) {
        if (firstCache != null) {
            firstCache.evict(key);
        }
        if (secondCache != null) {
            secondCache.evict(key);
        }
    }

    @Override
    public void clear() {
        if (firstCache != null) {
            firstCache.clear();
        }
        if (secondCache != null) {
            secondCache.clear();
        }
    }
}

自定义CacheResolver

源码地址

public class MultipleCacheResolver implements CacheResolver {

    private final CustomRedisCacheManager redisCacheManager;
    private final CaffeineCacheManager caffeineCacheManager;

    /**
     * 相同过期策略可使用同一个Caffeine
     */
    private final Map<Long, Caffeine<Object, Object>> ttlCaffeineMap = new ConcurrentHashMap<>(64);

    /**
     * Cache无需重复创建,缓存相同CacheName的Cache对象
     */
    private final Map<String, CaffeineCache> caffeineTTLCacheMap = new ConcurrentHashMap<>(64);

    public MultipleCacheResolver(CustomRedisCacheManager redisCacheManager, CaffeineCacheManager caffeineCacheManager) {
        this.redisCacheManager = redisCacheManager;
        this.caffeineCacheManager = caffeineCacheManager;
    }

    @Override
    @NotNull
    public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
         Collection<String> cacheNames = context.getOperation().getCacheNames();
        Class<?> targetClazz = context.getTarget().getClass();
        if (CollectionUtils.isEmpty(cacheNames)) {
            return Collections.emptyList();
        }

        long clazzTTL = -1;
        if (targetClazz.isAnnotationPresent(CacheTTL.class)) {
            CacheTTL cacheTTL = targetClazz.getAnnotation(CacheTTL.class);
            if (cacheTTL != null) {
                clazzTTL = cacheTTL.value();
            }
        }
        // FIXME: 缓存注解和驱逐缓存的TTL配置和LocalCache配置不一致时可能会取到不同的Cache导致数据异常
        Method method = context.getMethod();
        Collection<Cache> result = new ArrayList<>(cacheNames.size());
        for (String cacheName : cacheNames) {

            CaffeineRedisCache caffeineRedisCache = new CaffeineRedisCache(true);
            boolean isOnlyLocalCache = false;
            // 启用本地缓存
            if (method.isAnnotationPresent(LocalCache.class)) {
                LocalCache localCache = method.getAnnotation(LocalCache.class);
                isOnlyLocalCache = localCache.onlyLocal(); // 是否只使用本地缓存
                if (method.isAnnotationPresent(CacheTTL.class)) { // 有TTL
                    CacheTTL cacheTTL = context.getMethod().getAnnotation(CacheTTL.class);
                    clazzTTL = cacheTTL.value();
                }

                if (clazzTTL > 0) {
                    // 相同的TTL不必重复创建Caffeine, 取同一个即可
                    Caffeine<Object, Object> caffeine = ttlCaffeineMap.computeIfAbsent(clazzTTL, key -> Caffeine.newBuilder().expireAfterAccess(key, TimeUnit.SECONDS));

                    // (TTL, Caffeine)
                    // (CacheName, CaffeineCache)
                    // 缓存cacheName -> CaffeineCache
                    // 若同一个CacheName,不同的TTL,这里是有问题的,key应该是cacheName + TTL
                    CaffeineCache caffeineCache = caffeineTTLCacheMap.computeIfAbsent(cacheName + clazzTTL, k -> new CaffeineCache(cacheName, caffeine.build(), true));

                    caffeineRedisCache.setFirstCache(caffeineCache);
                }

                // 若已设置有TTL的Cache则不使用CaffeineManager的Cache对象
                if (caffeineRedisCache.getFirstCache() == null) {
                    Cache caffeineCache = caffeineCacheManager.getCache(cacheName);
                    if (caffeineCache == null) {
                        throw new IllegalArgumentException("Cannot find cache named '" +
                                cacheName + "' for " + context.getOperation());
                    }
                    caffeineRedisCache.setFirstCache(caffeineCache);
                }

            }


            // 非仅本地缓存,则需要创建redis二级缓存
            if (!isOnlyLocalCache) {
                Cache redisCache = redisCacheManager.getCache(cacheName);
                if (redisCache == null) {
                    throw new IllegalArgumentException("Cannot find cache named '" +
                            cacheName + "' for " + context.getOperation());
                }
                caffeineRedisCache.setSecondCache(redisCache);
            }

            result.add(caffeineRedisCache);
        }
        return result;
    }
}

将自定义CacheResolver注册为默认cacheResolver

源码地址

/**
 * 自定义默认cache resolver和 cacheManager
 */
@Configuration
public class CustomSpringCacheConfig implements CachingConfigurer {

    @Resource
    CaffeineCacheManager caffeineCacheManager;

    @Resource
    CustomRedisCacheManager redisCacheManager;

    /**
     * 多级缓存解析器
     */
    @Override
    public CacheResolver cacheResolver() {
        return new MultipleCacheResolver(redisCacheManager, caffeineCacheManager);
    }

    /**
     * 默认使用redis缓存
     */
    @Override
    public CacheManager cacheManager() {
        return redisCacheManager;
    }
}

实战应用

源码地址

@Service
// SpringCache缓存框架的缓存机制类似于Map<String, Cache>, key是cacheName, Cache(类似于一个Map<String, Object>)是一个缓存对象
@CacheConfig(cacheNames = "user_role") 
@CacheTTL(7 * 24 * 60 * 60L) // 设置TTL过期时间
public class UserRoleServiceImpl extends AbstractService<UserRoleDO, UserRoleOutputDTO, PageDTO> implements IUserRoleService {

    @LocalCache
    @Cacheable(key = "'user:' + #p0", unless = "#result == null")
    public List<String> listRoleIdByUserId(String userId) {
        return baseMapper.wrapper().eq(UserRoleDO::getUserId, userId).list().stream().map(UserRoleDO::getRoleId).distinct().toList();
    }

    @LocalCache
    @Cacheable(key = "'role:' + #p0", unless = "#result == null")
    public List<String> listUserIdByRoleId(String roleId) {
        return baseMapper.wrapper().eq(UserRoleDO::getRoleId, roleId).list().stream().map(UserRoleDO::getUserId).distinct().toList();
    }

    @LocalCache
    @CacheEvict(key = "'user:' + #p0")
    public void deleteRoleIdListByUserId(String userId) {
        baseMapper.wrapper().eq(UserRoleDO::getUserId, userId).delete();
    }

    @LocalCache
    @CacheEvict(key = "'role:' + #p0")
    public void evictUserIdListByRoleId(String roleId) { }
    
}

以上代码来源: 后端代码:github.com/L1yp/van-te…

前端代码:github.com/L1yp/van-te…

点击链接加入群聊:【Van交流群】