Cache缓存的用法

我爱海鲸 2026-05-15 10:19:26 暂无标签

简介Caffeine、redis缓存配置、咖啡因

2026-05-15 start:

================================================================================
知识点备忘:进程内 Token 缓存与并发(通用说明,已与具体业务脱敏)
================================================================================
编写说明:本文仅描述通用 JVM / Spring 并发与缓存思路,不涉及真实域名、密钥、工号或示例手机号。
--------------------------------------------------------------------------------
一、场景
--------------------------------------------------------------------------------
• 调用第三方 REST:先获取短时有效的 token,再在后续请求里携带 token。
• token 在服务端有约数分钟的有效期(具体以接口文档为准)。
• 应用希望在有效期内复用同一 token,减少对「获取 token」接口的调用频率。
• 典型实现位置:Spring @Service 单例 Bean 内的私有方法与字段。
--------------------------------------------------------------------------------
二、缓存形态:进程内、单机内存
--------------------------------------------------------------------------------
• 使用 Bean 的成员变量保存「当前 token」与「本地认为的过期时刻」(例如毫秒时间戳)。
• 特点是实现简单、无 Redis 等外部依赖。
• 局限:每台 JVM / 每个部署实例各自一份缓存;集群下不会自动共享同一份 token。
• 若需全集群共用缓存或失效一致性,再考虑分布式缓存或统一网关。
--------------------------------------------------------------------------------
三、volatile 的作用(可见性与有序性约束)
--------------------------------------------------------------------------------
• 多线程会同时进入「读缓存 → 可能刷新」的逻辑。
• 普通字段的写可能被其他线程延迟看到(CPU 缓存、指令重排等),易出现重复刷新或读到不一致的中间态。
• 对「token 字符串」和「过期时间戳」使用 volatile:
  - 强调多线程之间的可见性:一个线程更新后,其他线程后续读更易看到新值。
  - 对 volatile 的访问在 JMM 下具有特定的 happens-before 关系,便于推理并发行为。
• 说明:volatile 不替代互斥;写缓存的临界区仍需要 synchronized 或其它手段保证「只会有一个线程在写」。
--------------------------------------------------------------------------------
四、双重检查锁定(Double-Checked Locking)+ synchronized
--------------------------------------------------------------------------------
目的:在「缓存仍有效」时走无锁快速路径;在「可能需刷新」时少打远端,并避免惊群。
典型结构(伪代码):
  1)无锁第一次检查:若 token 非空且当前时间早于本地过期时间 → 直接返回缓存 token。
  2)否则进入 synchronized(锁对象):
     3)锁内第二次检查:再次看缓存是否已被其他线程刷新 → 有效则直接返回。
     4)仍无效则调用远端获取 token,解析成功后写入缓存,并设置新的本地过期时刻。
锁对象常用:当前单例 Bean 的 this(整个 Bean 一把锁)。若将来该 Bean 内还有其它重量级同步,可改为私有 final Object 专用于 token 刷新,减小锁粒度话题另议。
--------------------------------------------------------------------------------
五、TTL(本地存活时间)与文档中 token 有效期的关系
--------------------------------------------------------------------------------
• 服务端返回的 expire_in(秒)或类似字段表示 token 在对方系统中的寿命。
• 本地 TTL 可以略小于对方声明的寿命,留出网络延迟、时钟误差、边界请求等余量,降低「已过期仍被复用」的概率。
• 若本地 TTL 大于对方实际寿命,可能出现用旧 token 调后续接口失败,需依赖重试或强制刷新策略(视业务而定)。
--------------------------------------------------------------------------------
六、两个字段更新的原子性(进阶注意点)
--------------------------------------------------------------------------------
• 「token 字符串」与「过期时间」分两次赋值时,理论上存在极短中间态(其他线程可能看到只更新了一半的组合)。
• 常见影响:偶尔多刷新一次 token,一般可接受。
• 若要求更严格的一致性,可将「token + 过期时间」封装为不可变对象,用 volatile 引用或 AtomicReference 一次性替换整条记录。
--------------------------------------------------------------------------------
七、与项目代码的对应关系(仅索引,不含敏感信息)
--------------------------------------------------------------------------------
• 实现类:cn.lomark.zoc.service.impl.XjOrderWhiteIpCheckServiceImpl
• 缓存字段示例命名:cachedWhitelistToken、cachedWhitelistTokenExpiresAtMillis
• 核心方法示例:getWhitelistTokenCached()(内含双重检查与 synchronized)
--------------------------------------------------------------------------------
八、代码示例(教学用,已与业务脱敏;接口 URL、JSON 字段请按对接文档替换)
--------------------------------------------------------------------------------
【示例 A】volatile + 双重检查锁定 + synchronized(单例 Bean 内常见写法)
// 示意:Spring @Service 单例中的一个片段
public final class SampleTokenCacheService {
    /** 本地缓存 TTL(毫秒),宜略小于对方声明的 token 有效期 */
    private static final long TOKEN_CACHE_TTL_MS = 240_000L;
    /** ThirdPartyClient 仅表示「负责 HTTP 调用的组件」,非真实项目类名 */
    private final ThirdPartyClient thirdPartyClient;
    private volatile String cachedToken;
    private volatile long cachedExpiresAtMillis;
    public SampleTokenCacheService(ThirdPartyClient thirdPartyClient) {
        this.thirdPartyClient = thirdPartyClient;
    }
    /** 对外:拿到可用 token(缓存命中则不访问网络) */
    public String obtainTokenOrThrow() {
        long now = System.currentTimeMillis();
        String t = cachedToken;
        if (t != null && !t.isEmpty() && now < cachedExpiresAtMillis) {
            return t;
        }
        synchronized (this) {
            now = System.currentTimeMillis();
            t = cachedToken;
            if (t != null && !t.isEmpty() && now < cachedExpiresAtMillis) {
                return t;
            }
            String raw = thirdPartyClient.fetchTokenRawResponse();
            String newToken = parseTokenStrict(raw);
            cachedToken = newToken;
            cachedExpiresAtMillis = System.currentTimeMillis() + TOKEN_CACHE_TTL_MS;
            return newToken;
        }
    }
    private static String parseTokenStrict(String raw) {
        // 按对方 JSON 校验业务码、取出 token;失败则抛 IllegalStateException
        throw new UnsupportedOperationException("示意占位");
    }
}
interface ThirdPartyClient {
    /** 调用远端「获取 token」接口,返回原始响应字符串 */
    String fetchTokenRawResponse();
}
【示例 B】与第四节一致的完整双重检查(对照阅读,去掉外层类壳)
public String getTokenCached() {
    long now = System.currentTimeMillis();
    String token = cachedToken;
    if (token != null && !token.isEmpty() && now < cachedExpiresAtMillis) {
        return token;
    }
    synchronized (this) {
        now = System.currentTimeMillis();
        token = cachedToken;
        if (token != null && !token.isEmpty() && now < cachedExpiresAtMillis) {
            return token;
        }
        String raw = thirdPartyClient.fetchTokenRawResponse();
        String newToken = parseTokenStrict(raw);
        cachedToken = newToken;
        cachedExpiresAtMillis = System.currentTimeMillis() + TOKEN_CACHE_TTL_MS;
        return newToken;
    }
}
【示例 C】进阶:不可变快照 + volatile 一次性发布(减轻「token 与过期时刻两次赋值」的中间态讨论)
public final class TokenSnapshot {
    private final String token;
    private final long expiresAtMillis;
    public TokenSnapshot(String token, long expiresAtMillis) {
        this.token = token;
        this.expiresAtMillis = expiresAtMillis;
    }
    public boolean validAt(long nowMillis) {
        return token != null && !token.isEmpty() && nowMillis < expiresAtMillis;
    }
    public String token() {
        return token;
    }
}
public final class SampleTokenCacheWithSnapshot {
    private final ThirdPartyClient thirdPartyClient;
    private static final long TOKEN_CACHE_TTL_MS = 240_000L;
    private volatile TokenSnapshot snapshot;
    public String obtainTokenOrThrow() {
        long now = System.currentTimeMillis();
        TokenSnapshot s = snapshot;
        if (s != null && s.validAt(now)) {
            return s.token();
        }
        synchronized (this) {
            now = System.currentTimeMillis();
            s = snapshot;
            if (s != null && s.validAt(now)) {
                return s.token();
            }
            String raw = thirdPartyClient.fetchTokenRawResponse();
            String newToken = parseTokenStrict(raw);
            long exp = System.currentTimeMillis() + TOKEN_CACHE_TTL_MS;
            snapshot = new TokenSnapshot(newToken, exp);
            return newToken;
        }
    }
    private static String parseTokenStrict(String raw) {
        throw new UnsupportedOperationException("示意占位");
    }
}
【示例 D】专用锁对象(同一 Bean 内若还有其它 synchronized(this),可降低无意串扰)
private final Object tokenRefreshLock = new Object();
public String getTokenCachedWithDedicatedLock() {
    long now = System.currentTimeMillis();
    String token = cachedToken;
    if (token != null && !token.isEmpty() && now < cachedExpiresAtMillis) {
        return token;
    }
    synchronized (tokenRefreshLock) {
        now = System.currentTimeMillis();
        token = cachedToken;
        if (token != null && !token.isEmpty() && now < cachedExpiresAtMillis) {
            return token;
        }
        String raw = thirdPartyClient.fetchTokenRawResponse();
        String newToken = parseTokenStrict(raw);
        cachedToken = newToken;
        cachedExpiresAtMillis = System.currentTimeMillis() + TOKEN_CACHE_TTL_MS;
        return newToken;
    }
}
--------------------------------------------------------------------------------
九、延伸阅读关键字(自学检索用)
--------------------------------------------------------------------------------
Java Memory Model(JMM)、happens-before、volatile、synchronized、double-checked locking、
singleton Bean 并发安全、缓存击穿 / 惊群(类比)、缓存 TTL 与 stale read。
================================================================================
文档结束
================================================================================

end

Caffeine配置:

@Slf4j
@Configuration
@EnableCaching
public class CaffeineCacheConfig {

    private static final int MAX_REFRESH_COUNT = 2;

    private static Cache caffeine = Caffeine.newBuilder()
            .expireAfterAccess(Duration.ofMinutes(5))
            .build();

    @Bean
    public CacheLoader cacheLoader() {
        return new CacheLoader() {
            @Nullable
            @Override
            public Object load(@NonNull Object o) throws Exception {
                return null;
            }

            @Nullable
            @Override
            public Object reload(@NonNull Object key, @NonNull Object oldValue) throws Exception {
                Integer refreshCount = caffeine.get(key, k -> 0);
                if (refreshCount > MAX_REFRESH_COUNT) {
                    // 超过最大刷新次数时,强制重新加载
                    caffeine.invalidate(key);
                    return load(key);
                }
                caffeine.put(key, ++ refreshCount);
                return oldValue;
            }
        };
    }
}

pom依赖:


    com.github.ben-manes.caffeine
    caffeine
    org.springframework.cache
    spring-cache
    org.springframework.boot
    spring-boot-starter-cache

使用的注解:

@Cacheable(value = "CACHE_DATA", key = "#id1+'-'+#id2", unless = "#result == null")
public Item getItemById(Long id) {
    // ...
}

只有在方法返回结果不为空的情况下才会缓存结果(unless 属性的作用)。并且,缓存的 key 是根据方法参数 id 来生成的。

在 Spring 配置类或主应用程序类上添加 @EnableCaching 注解,以开启缓存支持。

确保配置了合适的缓存管理器(如 EhCache, Caffeine, Redis 等)。

如果使用的是 XML 配置,确保 元素已添加到你的配置文件中。

application.xml上配置:

---
spring:
  cache:
    type: caffeine
    caffeine:
      spec: initialCapacity=500,maximumSize=5000,expireAfterWrite=600s,refreshAfterWrite=500s
    cache-names: CACHE_DATA
  • type: CAFFEINE:指定了应用程序使用 Caffeine 作为默认的缓存提供者。

  • initialCapacity=500

    • 这是缓存初始化时分配的容量大小。当缓存创建时,它将预先分配一定的空间来存储缓存条目。这个值表示缓存开始时可以容纳的条目数量。

  • maximumSize=5000

    • 设置了缓存的最大容量。一旦缓存中的条目数超过了这个数值,最旧的条目将会被移除(根据 LRU 或其他策略)以腾出空间给新的条目。

  • expireAfterWrite=600s

    • 定义了写入后过期时间。这意味着从一个条目被添加或更新后的那一刻起,经过 600 秒(10 分钟)之后,该条目将被视为过期,并在下次访问时被移除。

  • refreshAfterWrite=500s

    • 指定刷新间隔时间。这表示如果一个条目在写入后超过 500 秒未被访问,则会在下一次访问时尝试刷新其内容。注意,refreshAfterWrite 并不会自动触发数据加载;它仅影响下一次访问时的行为。具体来说,只有当条目确实过期并且需要重新加载时才会发生刷新操作。

2025-01-20 start:

redis缓存配置

pom:

		
			org.springframework.boot
			spring-boot-starter-data-redis
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-cache
        

配置文件:RedisCacheConfig

import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import java.time.Duration;

/**
 * spring-cache使用redis缓存配置
 */
@Slf4j
@Configuration
@EnableCaching
public class RedisCacheConfig extends CachingConfigurerSupport {

    @Bean
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
        Jackson2JsonRedisSerializer<【返回的结果类】> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(【返回的结果类】);
        RedisCacheConfiguration configuration = RedisCacheConfiguration
                .defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
                 // 过期:一天
                .entryTtl(Duration.ofDays(1));
        return RedisCacheManager
                .builder(redisCacheWriter)
                .cacheDefaults(configuration)
                .build();
    }

    @Bean
    @Override
    public CacheErrorHandler errorHandler() {
        log.warn("Redis occur exception, use custom CacheErrorHandler to handle");
        return new CacheErrorHandler() {
            @Override
            public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
                doHandleRedisErrorException(exception, key);
            }

            @Override
            public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
                doHandleRedisErrorException(exception, key);
            }

            @Override
            public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
                doHandleRedisErrorException(exception, key);
            }

            @Override
            public void handleCacheClearError(RuntimeException exception, Cache cache) {
                doHandleRedisErrorException(exception, null);
            }
        };
    }

    protected void doHandleRedisErrorException(RuntimeException exception, Object key) {
        log.warn("Redis occur exception:key=[{}]", key, exception);
        String redisKey = (String) key;
        if (redisKey.contains("LOCK")) {
            throw exception;
        }
    }
}

redis配置:RedisConfiguration

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置
 * @author huangjuguan
 * @date 2020/7/8.
 */

@Configuration
public class RedisConfiguration {

    @Bean("objectRedisTemplate")
    public RedisTemplate objectRedisTemplate(RedisConnectionFactory factory) {

        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);

        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
        redisTemplate.setEnableTransactionSupport(true);
        return redisTemplate;
    }

}

使用同上

end

2026-01-22 start:

咖啡因简单的导入使用,用来缓存,五分钟过期

导入pom依赖

        
        
            com.github.ben-manes.caffeine
            caffeine
        
Caffeine 缓存配置:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

/**
 * Caffeine 缓存配置
 * 
 * @author system
 */
@Configuration
public class CacheConfig {

    /**
     * 测试列表缓存
     * 缓存时间:5分钟
     * 
     * @return Cache实例
     */
    @Bean
    public Cache testListCache() {
        return Caffeine.newBuilder()
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .maximumSize(1000)
                .build();
    }
}

在service中导入:

    @Resource
    private Cache testListCache;
        // 构建缓存key
        String cacheKey = "testList:" + type.name();       
       
        // 从缓存中获取
        Object cached = testListCache.getIfPresent(cacheKey);
        if (cached != null) {
            log.info("视频列表缓存命中key【{}】",cacheKey);
            return (VideoListResponse) cached;
        }
        
        //TODO  缓存未命中,执行查询
        
        // 将结果放入缓存
        testListCache.put(cacheKey, response);

end

你好:我的2025