Eureka 多层缓存机制详解

一、Eureka 多层缓存机制详解

java 复制代码
客户端请求
     ↓
[第一层] 只读缓存 (ReadOnlyCache) ← 30秒同步一次
     ↓
[第二层] 读写缓存 (ReadWriteCache) ← 实时更新  
     ↓
[第三层] 注册表内存 (Registry) ← 真实数据存储

具体工作流程示例,场景设置

服务实例:order-service-1, payment-service-1, user-service-1.

客户端:需要频繁拉取服务注册表进行服务发现

步骤1:客户端首次拉取注册表(全量)

java 复制代码
// 客户端第一次请求服务列表
@RestController
public class OrderController {
    
    public void callPaymentService() {
        // 1. 客户端向Eureka Server发起请求
        List<ServiceInstance> instances = discoveryClient.getInstances("payment-service");
        
        // Eureka Server内部处理流程:
        //   a. 检查只读缓存 → 未命中(首次请求)
        //   b. 检查读写缓存 → 未命中  
        //   c. 查询真实注册表 → 命中,返回50个payment-service实例
        //   d. 写入读写缓存 + 只读缓存
        
        String result = restTemplate.getForObject(
            "http://payment-service/pay", String.class);
    }
}

数据流向:

java 复制代码
客户端请求 → 只读缓存(未命中) → 读写缓存(未命中) → 真实注册表(命中)
                                                     ↓
客户端响应 ← 只读缓存(已填充) ← 读写缓存(已填充) ← 返回结果

步骤2:30秒内的重复请求(缓存命中)

java 复制代码
// 在接下来的30秒内,其他客户端的相同请求
public class LoadBalancer {
    public Server chooseServer() {
        // 这些请求直接命中只读缓存,无需访问底层注册表
        // 性能提升:减少数据库压力90%+
        
        // 假设每秒133次请求(如图中计算):
        // - 缓存命中率:99% → 131次/秒从缓存返回
        // - 真实查询:仅2次/秒访问注册表
    }
}

步骤3:服务实例注册(缓存失效)

java 复制代码
// 当新的payment-service-51实例注册时
@Component
public class EurekaRegistry {
    
    public void register(InstanceInfo instance) {
        // 1. 更新真实注册表(内存操作)
        registry.put(instance.getId(), instance);
        
        // 2. 立即失效读写缓存中的相关条目
        readWriteCache.invalidate("payment-service");
        
        // 3. 只读缓存暂时不变(保持旧数据30秒)
        //    这保证了高并发读取性能,牺牲了短暂的数据一致性
        
        log.info("新实例注册:{}, 读写缓存已失效", instance.getId());
    }
}

缓存状态变化:

java 复制代码
真实注册表: [payment-service-1, ..., payment-service-50, payment-service-51] ✅ 最新
读写缓存:   [已失效] ❌
只读缓存:   [payment-service-1, ..., payment-service-50] ⏳ (30秒内保持旧数据)

步骤4:缓存同步(定时任务)

java 复制代码
// Eureka Server的缓存同步任务
@Scheduled(fixedRate = 30000) // 每30秒执行一次
public class CacheSyncTask {
    
    public void syncReadOnlyCache() {
        // 1. 获取读写缓存中的最新数据
        Map<String, Object> latestData = readWriteCache.getAll();
        
        // 2. 批量更新只读缓存
        readOnlyCache.clear();
        readOnlyCache.putAll(latestData);
        
        // 3. 记录同步日志
        log.info("只读缓存同步完成,实例数量: {}", latestData.size());
    }
}

同步后的状态:

java 复制代码
真实注册表: [payment-service-1, ..., payment-service-51] ✅
读写缓存:   [payment-service-1, ..., payment-service-51] ✅  
只读缓存:   [payment-service-1, ..., payment-service-51] ✅ (已同步)

二、增量心跳机制,客户端不是每次全量拉取注册表

java 复制代码
public class DiscoveryClient {
    public void getDelta() {
        // 首次全量拉取,后续只获取变更部分
        if (lastUpdateTimestamp == 0) {
            return fullRegistry; // 全量注册表
        } else {
            return getApplicationsSince(lastUpdateTimestamp); // 增量变更
        }
    }
}

三、Eureka高吞吐的原理

1、客户端缓存:Eureka客户端会缓存服务注册表信息。客户端(服务实例)并不每次需要服务发现时都去注册中心拉取,而是定期(默认30秒)从Eureka服务器拉取注册表信息并缓存到本地。这样,大部分服务发现请求可以直接从本地缓存获取,减轻了Eureka服务器的压力。

2、增量更新和压缩:Eureka服务器会存储增量的服务注册信息,并且支持压缩传输。客户端在拉取注册表信息时,可以使用增量更新,只获取发生变化的部分,减少网络传输的数据量。

3、多级缓存机制:Eureka服务器内部使用了多级缓存来提升读性能。包括:

只读缓存:Eureka服务器维护一个只读的缓存,定时从注册表中更新数据(默认30秒一次)。客户端拉取注册表时,直接返回只读缓存中的数据,这样避免了每次请求都去访问注册表,提高了读取速度。

读写缓存:Eureka服务器还维护一个读写缓存,当有服务注册、续约、注销等写操作时,会更新读写缓存,并定时同步到只读缓存。

4、集群部署和负载均衡:Eureka服务器通常以集群方式部署,多个Eureka节点之间通过异步复制数据来保证高可用。客户端可以配置多个Eureka服务器地址,实现负载均衡和故障转移。

5、自我保护机制:当网络分区或大规模故障时,Eureka进入自我保护模式,不再剔除失效的服务实例。这样可以避免在网络波动时频繁更新注册表,减少了写操作,保证了整个系统的稳定性。

6、异步操作:Eureka的很多操作都是异步的,比如客户端的注册、续约,以及服务器节点之间的数据复制等。通过异步化,可以避免阻塞主线程,提高吞吐量。

7、高效的序列化:Eureka使用JSON和XML等格式进行通信,但内部使用高效的数据结构,并且通过优化序列化/反序列化过程来提升性能。

8、心跳机制:服务实例通过心跳来维持注册,而不是每次重新注册,这减少了写操作。心跳是轻量级的操作,通常只包含实例ID等少量信息。

9、注册表的结构优化:Eureka的注册表使用ConcurrentHashMap等并发数据结构,保证读写的高性能。

10、限流和降级:Eureka服务器具备一定的限流能力,防止过多请求压垮服务器。在压力大时,可以通过降级策略来保证核心功能的可用性。

四、只读和读写缓存的总结

java 复制代码
// 多级缓存机制(核心优化), Eureka Server 缓存架构伪代码
public class ResponseCache {
    private final ConcurrentMap<Key, Value> readOnlyCacheMap;  // 只读缓存
    private final ConcurrentMap<Key, Value> readWriteCacheMap; // 读写缓存
    
    // 客户端拉取注册表时,直接从只读缓存获取
    public String getPayload(Key key) {
        Value payload = readOnlyCacheMap.get(key);
        if (payload == null) {
            payload = readWriteCacheMap.get(key);
            if (payload != null) {
                readOnlyCacheMap.put(key, payload); // 填充只读缓存
            }
        }
        return payload;
    }
    
    // 注册表变更时,先失效读写缓存,定时同步到只读缓存
    public void invalidate(Key key) {
        readWriteCacheMap.remove(key); // 立即失效读写缓存
        // 只读缓存通过定时任务(30秒)更新,避免频繁同步开销
    }
}

缓存更新策略:

读写缓存:注册表变更时立即失效

只读缓存:定时30秒同步一次(可配置)

效果:99%的读请求直接命中缓存,极大降低数据库压力

问题:"读写缓存"的数据是什么时候更新的?

第一种更新"读写缓存"的场景

核心原理:懒加载(Lazy Loading),读写缓存采用按需加载策略,不是在失效时立即更新,而是在下次查询时才从真实注册表加载最新数据。

java 复制代码
// Eureka Server 读写缓存的实现逻辑
public class ReadWriteCacheImpl {
    
    private final ConcurrentMap<Key, Value> cache = new ConcurrentHashMap<>();
    private final LoadingCache<Key, Value> loadingCache; // 加载缓存
    
    public Value get(Key key) {
        // 1. 先检查缓存中是否存在
        Value cachedValue = cache.get(key);
        if (cachedValue != null) {
            return cachedValue; // 缓存命中,直接返回
        }
        
        // 2. 缓存未命中(可能是首次查询或刚被失效)
        //    从真实注册表加载最新数据
        Value freshValue = loadFromRegistry(key);
        
        // 3. ⭐⭐⭐ 关键步骤:将最新数据写入读写缓存
        cache.put(key, freshValue);
        
        return freshValue;
    }
    
    public void invalidate(Key key) {
        // 只是移除缓存条目,不立即加载新数据
        cache.remove(key);
        // 注意:这里没有调用 loadFromRegistry()!
    }
}

步骤3的详细补充(您提到的缺失环节)

java 复制代码
// 完整的注册和缓存更新流程
@Component
public class EurekaRegistryService {
    
    public void registerInstance(InstanceInfo newInstance) {
        // 步骤3.1: 更新真实注册表(内存操作)
        registry.put(newInstance.getId(), newInstance);
        log.info("真实注册表已更新,实例数: {}", registry.size());
        
        // 步骤3.2: ⭐ 立即失效读写缓存中的相关条目
        String appName = newInstance.getAppName();
        readWriteCache.invalidate(appName); // 移除缓存,但不加载新数据
        log.info("读写缓存已失效,等待下次查询时重新加载");
        
        // 步骤3.3: ⭐⭐⭐ 关键问题:什么时候写入最新数据?
        // 答案:在下一次客户端查询时!
    }
    
    // 当客户端查询服务列表时触发数据加载
    public Applications getApplications(String appName) {
        // 触发读写缓存的懒加载机制
        return readWriteCache.get(appName, () -> {
            // 这个lambda表达式在缓存未命中时执行
            // ⭐ 在这里从真实注册表加载最新数据并写入缓存
            Applications freshData = loadLatestFromRegistry(appName);
            log.info("读写缓存重新加载,实例数: {}", freshData.size());
            return freshData;
        });
    }
}

第二种更新"读写缓存"的场景

Eureka Server 缓存同步任务的实际实现,完整流程

java 复制代码
@Scheduled(fixedRate = 30000) // 每30秒执行一次
public class CacheSyncTask {
    
    public void syncReadOnlyCache() {
        Map<Key, Value> latestData = new HashMap<>();
        
        // 1. 遍历所有应用程序,尝试从读写缓存获取数据
        for (String appName : getAllApplicationNames()) {
            Key key = new Key(Key.EntityType.Application, appName);
            
            // 2. ⭐⭐⭐ 关键:如果读写缓存为空,会触发懒加载
            Value data = readWriteCache.get(key);
            
            if (data != null) {
                // 缓存中有数据,直接使用
                latestData.put(key, data);
            } else {
                // 3. ⭐⭐⭐ 容错机制:缓存为空时,直接从真实注册表加载
                log.warn("读写缓存为空,从真实注册表直接加载: {}", appName);
                Value freshData = loadDirectlyFromRegistry(appName);
                latestData.put(key, freshData);
                
                // 4. 同时重新填充读写缓存(避免下次还是空)
                readWriteCache.put(key, freshData);
            }
        }
        
        // 5. 批量更新只读缓存
        readOnlyCache.clear();
        readOnlyCache.putAll(latestData);
        
        log.info("缓存同步完成,处理应用数: {}", latestData.size());
    }
}

也就是说,读写缓存被清理后,有定时任务缺失查询操作,对读写缓存的数据进行完善。

相关推荐
AL流云。3 小时前
学习Docker前提:多环境安装Docker
学习·docker·eureka·1024程序员节
李小白665 小时前
Redis常见指令
数据库·redis·缓存
秋千码途5 小时前
Spring的@Cacheable取缓存默认实现
java·spring·缓存
托比-马奎尔17 小时前
Redis主从集群
数据库·redis·缓存
丈剑走天涯20 小时前
k8s etcd服务安装维护
云原生·etcd·devops·1024程序员节
青0721松21 小时前
千云低代码平台ETMS-k8s实施部署
低代码·云原生·容器
睡不醒的猪儿1 天前
k8s部署自动化工具jenkins
云原生·kubernetes·自动化·jenkins
秋千码途1 天前
在K8S中部署MySQL主从
mysql·云原生·容器·kubernetes