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