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());
    }
}

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

相关推荐
运维栈记2 小时前
如何排错运行在Kubernetes集群中的服务?
云原生·容器·kubernetes
阿里云云原生3 小时前
直播回顾丨详解阿里云函数计算 AgentRun,手把手带你走进 Agentic AI 生产时代
云原生
檀越剑指大厂5 小时前
查看 Docker 镜像详情的几种常用方法
docker·容器·eureka
此生只爱蛋9 小时前
【Redis】列表List类型
数据库·redis·缓存
菜鸟小九9 小时前
redis实战(缓存)
数据库·redis·缓存
叫致寒吧10 小时前
zookeeper与kafka
分布式·zookeeper·云原生
快乐就去敲代码@!10 小时前
Boot Cache Star ⭐(高性能两级缓存系统)
spring boot·redis·后端·缓存·docker·压力测试
廋到被风吹走11 小时前
【数据库】【Redis】基本概念和特点
数据库·redis·缓存
HitpointNetSuite11 小时前
云原生与 AI:NetSuite 如何塑造亚太中端市场
人工智能·云原生
轩轩Aminent11 小时前
WSL 中的 Ubuntu 系统中使用 Docker
ubuntu·docker·eureka