Redis + Caffeine打造超速两级缓存架构

发布于:2025-04-16 ⋅ 阅读:(44) ⋅ 点赞:(0)

在这里插入图片描述

背景

接口的逻辑非常简单:根据传入的城市、仓库和发货时间,查询快递的预计送达时间。

然而,由于会频繁调用这个接口,尤其是在大促期间,接口的性能要求极高。

数据量虽然不大,但为了确保接口的高性能和高可用性,决定采用 Redis + Caffeine 两级缓存策略,以应对可能出现的缓存雪崩、缓存穿透等问题。

本地缓存的优缺点

优点

  1. 极速查询:本地缓存基于内存,查询速度极快,适合数据更新频率低、实时性要求不高的场景(例如我们每天凌晨更新一次数据,总量约7k)。
  2. 减少网络I/O:相比查询远程缓存,本地缓存可以显著降低网络消耗,避免因网络问题导致的查询延迟。

缺点

  1. 一致性问题:在分布式环境下,本地缓存的更新难以同步到其他节点,容易导致数据不一致。
  2. 不支持持久化:Caffeine 缓存仅存储在内存中,一旦应用重启,缓存数据将丢失。
  3. 内存溢出风险:本地缓存需要合理设置容量,避免因数据过多导致内存溢出。

代码实现

一、配置类实现

1.MySQL表结构

CREATE TABLE `t_estimated_arrival_date` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `warehouse_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULL COMMENT '货仓id',
  `warehouse` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULL COMMENT '发货仓',
  `city` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULLDEFAULTNULL COMMENT '签收城市',
  `delivery_date` dateNULLDEFAULTNULL COMMENT '发货时间',
  `estimated_arrival_date` dateNULLDEFAULTNULL COMMENT '预计到货日期',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_warehouse_id_city_delivery_date`(`warehouse_id`, `city`, `delivery_date`) USING BTREE
) ENGINE = InnoDB COMMENT ='预计到货时间表' ROW_FORMAT =Dynamic;

2.依赖配置(pom.xml)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.28</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.1</version>
</dependency>

3.配置类

RedisConfig

public classRedisConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = newRedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        Jackson2JsonRedisSerializer<Object> serializer = newJackson2JsonRedisSerializer<>(Object.class);
        ObjectMappermapper=newObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
        serializer.setObjectMapper(mapper);

        redisTemplate.setKeySerializer(newStringRedisSerializer());
        redisTemplate.setValueSerializer(serializer);
        redisTemplate.setHashKeySerializer(newStringRedisSerializer());
        redisTemplate.setHashValueSerializer(serializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

CaffeineConfig

public classCaffeineConfig {
    @Bean
    public Cache<String, Object> caffeineCache() {
        return Caffeine.newBuilder()
                .initialCapacity(128)
                .maximumSize(1024)
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .build();
    }

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManagercacheManager=newCaffeineCacheManager();
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .initialCapacity(128)
                .maximumSize(1024)
                .expireAfterWrite(60, TimeUnit.SECONDS));
        return cacheManager;
    }
}

4.Service 实现

@Slf4j
@Service
public class DoubleCacheServiceImpl doubleCacheServiceImpl {
    @Resource
    private Cache caffeineCache;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private EstimatedArrivalDateMapper estimatedArrivalDateMapper;

    @Override
    public EstimatedArrivalDateEntity getEstimatedArrivalDateCommon(EstimatedArrivalDateEntity request) {
        String key = request.getDeliveryDate() + ":" + request.getWarehouseId() + ":" + request.getCity();
        log.info("Cache key: {}", key);
        Objectvalue = caffeineCache.getIfPresent(key);
        if (Objects.nonNull(value)) {
            log.info("get from caffeine");
            return EstimatedArrivalDateEntity.builder().estimatedArrivalDate(value.toString()).build();
        }
        value = redisTemplate.opsForValue().get(key);
        if (Objects.nonNull(value)) {
            log.info("get from redis");
            caffeineCache.put(key, value);
            return EstimatedArrivalDateEntity.builder().estimatedArrivalDate(value.toString()).build();
        }
        log.info("get from mysql");
        DateTimedeliveryDate = DateUtil.parse(request.getDeliveryDate(), "yyyy-MM-dd");
        EstimatedArrivalDateEntity entity = estimatedArrivalDateMapper.selectOne(newQueryWrapper<>()
                .eq("delivery_date", deliveryDate)
                .eq("warehouse_id", request.getWarehouseId())
                .eq("city", request.getCity()));
        redisTemplate.opsForValue().set(key, entity.getEstimatedArrivalDate(), 120, TimeUnit.SECONDS);
        caffeineCache.put(key, entity.getEstimatedArrivalDate());
        return EstimatedArrivalDateEntity.builder().estimatedArrivalDate(entity.getEstimatedArrivalDate()).build();
    }
}

代码分析:

  1. 首先从 Caffeine 缓存中获取数据,如果命中则直接返回。
  2. 如果 Caffeine 缓存未命中,则从 Redis 中查询数据,并将结果写入 Caffeine 缓存。
  3. 如果 Redis 中也未命中,则从数据库中查询数据,并同时写入 RedisCaffeine 缓存。

二、注解实现

1.DoubleCache 注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DoubleCache {
    String cacheName();
    String[] key();
    longexpireTime() default 120;
    CacheType type() default CacheType.FULL;

    enumCacheType {
        FULL, PUT, DELETE
    }
}

2.DoubleCacheAspect

@Slf4j
@Component
@Aspect
public class DoubleCacheAspect {
    @Resource
    private Cache caffeineCache;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Pointcut("@annotation(com.itender.redis.annotation.DoubleCache)")
    public void doubleCachePointcut() {}

    @Around("doubleCachePointcut()")
    public Object doAround(ProceedingJoinPoint point)throws Throwable {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Methodmethod = signature.getMethod();
        String[] paramNames = signature.getParameterNames();
        Object[] args = point.getArgs();
        TreeMap<String, Object> treeMap = newTreeMap<>();
        for (int i = 0; i < paramNames.length; i++) {
            treeMap.put(paramNames[i], args[i]);
        }
        Double Cacheannotation = method.getAnnotation(DoubleCache.class);
        String elResult = DoubleCacheUtil.arrayParse(Lists.newArrayList(annotation.key()), treeMap);
        String realKey = annotation.cacheName() + ":" + elResult;

        if (annotation.type() == DoubleCache.CacheType.PUT) {
            Object object = point.proceed();
            redisTemplate.opsForValue().set(realKey, object, annotation.expireTime(), TimeUnit.SECONDS);
            caffeineCache.put(realKey, object);
            return object;
        } elseif (annotation.type() == DoubleCache.CacheType.DELETE) {
            redisTemplate.delete(realKey);
            caffeineCache.invalidate(realKey);
            return point.proceed();
        }
        Object caffeineCacheObj = caffeineCache.getIfPresent(realKey);
        if (Objects.nonNull(caffeineCacheObj)) {
            log.info("get data from caffeine");
            return caffeineCacheObj;
        }
        Object redisCache = redisTemplate.opsForValue().get(realKey);
        if (Objects.nonNull(redisCache)) {
            log.info("get data from redis");
            caffeineCache.put(realKey, redisCache);
            return redisCache;
        }
        log.info("get data from database");
        Object object = point.proceed();
        if (Objects.nonNull(object)) {
            log.info("get data from database write to cache: {}", object);
            redisTemplate.opsForValue().set(realKey, object, annotation.expireTime(), TimeUnit.SECONDS);
            caffeineCache.put(realKey, object);
        }
        return object;
    }
}

代码分析:

  1. 注解驱动:通过自定义注解 @DoubleCache,可以在方法上灵活配置缓存逻辑。
  2. 动态拼接 Key:支持使用 Spring EL 表达式动态拼接缓存 Key。
  3. 缓存一致性:在注解中支持全缓存、仅写入缓存、仅删除缓存等操作,便于灵活管理缓存数据。

总结

需要注意的是,本地缓存的容量和过期时间需要根据实际业务场景合理设置,以防止内存溢出等问题。
虽然 Redis 单独使用已经足够强大,但在某些场景下,结合 Caffeine 的本地缓存可以进一步提升性能。


网站公告

今日签到

点亮在社区的每一天
去签到