Spring Cloud Netflix-Eureka(五)、多级缓存机制

Spring Cloud Netflix之Eureka源码系列文章一共分为六个片段

Spring Cloud Netflix-Eureka(一)、服务注册与发现
Spring Cloud Netflix-Eureka(二)、信息存储原理
Spring Cloud Netflix-Eureka(三)、自我保护机制
Spring Cloud Netflix-Eureka(四)、心跳续约机制
Spring Cloud Netflix-Eureka(五)、多级缓存机制
Spring Cloud Netflix-Eureka(六)、集群数据同步


Eureka Server 端,会把客户端的地址信息保存到一个 ConcurrentHashMap 中存储。客户端对定时获取服务端的注册信息,客户端上线、下线也会刷新当前 ConcurrentHashMap 的值。

一、多级缓存的意义

当存在大规模的服务注册和更新时,如果只是修改一个 ConcurrentHashMap 数据,那么势必因为锁的存在导致竞争,影响性能。而Eureka 又是 AP(可用、容错) 模型,只需要满足最终可用就行。所以它在这里用到多级缓存来实现读写分离

Eureka 多级缓存实现:

  1. 注册方法写的时候直接写内存注册表,写完表之后主动失效读写缓存。
  2. 获取注册信息接口先从只读缓存取,只读缓存没有再去读写缓存取,读写缓存没有再去内存注册表里取。
  3. 读写缓存会更新回写只读缓存。

二、Eureka多级缓存设计

Eureka Server 为了提供响应效率,提供了两层的缓存结构,将 Eureka Client 所需要的注册信息,直接存储在缓存结构中,实现原理如下图所示。
在这里插入图片描述

  • 一级缓存:readOnlyCacheMap,本质上是 ConcurrentHashMap,依赖定时任务从 readWriteCacheMap 同步数据,默认时间为 30 秒(可配置,responseCacheUpdateIntervalMs)。readOnlyCacheMap 主要是为了供客户端获取注册信息时使用,其缓存更新,依赖于定时器的更新,通过和 readWriteCacheMap 的值做对比,如果数据不一致,则以 readWriteCacheMap 的数据为准。

  • 二级缓存:readWriteCacheMap,本质上是 Guava 缓存,依赖于服务注册信息变更后从 registers 同步数据。readWriteCacheMap 的数据主要同步于存储层。当获取缓存时判断缓存中是否没有数据,如果不存在此数据,则通过 CacheLoader 的 load 方法去加载,加载成功之后将数据放入缓存,同时返回数据。
    readWriteCacheMap 缓存过期时间,默认为 180 秒(可配置,responseCacheAutoExpirationInSeconds),当服务下线、过期、注册、状态变更,都会来清除此缓存中的数据。

通过 Eureka Server 的二层缓存机制,可以非常有效地提升 Eureka Server 的响应时间,通过数据存储层和缓存层的数据切割,根据使用场景来提供不同的数据支持。

三、Eureka 缓存数据一致性问题

Eureka Client 获取全量或者增量的数据时,会先从一级缓存中获取;如果一级缓存中不存在,再从二级缓存中获取;如果二级缓存也不存在,这时候先将存储层的数据同步到缓存中,再从缓存中获取。具体实现如下。

服务端接收获取服务地址请求,具体处理逻辑在 ApplicationResource.getApplication() 中进行。

@Produces({"application/xml", "application/json"})
public class ApplicationResource {
	@GET
    public Response getApplication(@PathParam("version") String version,
                                   @HeaderParam("Accept") final String acceptHeader,
                                   @HeaderParam(EurekaAccept.HTTP_X_EUREKA_ACCEPT) String eurekaAccept) {
        // 是否允许获取
        if (!registry.shouldAllowAccess(false)) {
            return Response.status(Status.FORBIDDEN).build();
        }

        EurekaMonitors.GET_APPLICATION.increment();// 监控+1

        CurrentRequestVersion.set(Version.toEnum(version));
        KeyType keyType = Key.KeyType.JSON;
        if (acceptHeader == null || !acceptHeader.contains("json")) {
            keyType = Key.KeyType.XML;
        }

        Key cacheKey = new Key(
                Key.EntityType.Application,
                appName,
                keyType,
                CurrentRequestVersion.get(),
                EurekaAccept.fromString(eurekaAccept)
        );
		
		// 获取缓存
        String payLoad = responseCache.get(cacheKey);
        CurrentRequestVersion.remove();

        if (payLoad != null) {
            logger.debug("Found: {}", appName);
            return Response.ok(payLoad).build();
        } else {
            logger.debug("Not Found: {}", appName);
            return Response.status(Status.NOT_FOUND).build();
        }
    }
}

responseCache.get(cacheKey); 方法

public class ResponseCacheImpl implements ResponseCache {
	public String get(final Key key) {
        return get(key, shouldUseReadOnlyResponseCache);
    }
    
	@VisibleForTesting
    String get(final Key key, boolean useReadOnlyCache) {
    	// 获取
        Value payload = getValue(key, useReadOnlyCache);
        if (payload == null || payload.getPayload().equals(EMPTY_PAYLOAD)) {
            return null;
        } else {
            return payload.getPayload();
        }
    }
    
	@VisibleForTesting
    Value getValue(final Key key, boolean useReadOnlyCache) {
        Value payload = null;
        try {
            if (useReadOnlyCache) {// 是否使用读缓存
            	// 从一级缓存 readOnlyCacheMap 获取
                final Value currentPayload = readOnlyCacheMap.get(key);
                if (currentPayload != null) {
                    payload = currentPayload;
                } else {// 一级缓存为空
                	// 从二级缓存 readWriteCacheMap 获取
                    payload = readWriteCacheMap.get(key);
                    readOnlyCacheMap.put(key, payload);
                }
            } else {
                // 从二级缓存 readWriteCacheMap 获取
                payload = readWriteCacheMap.get(key);
            }
        } catch (Throwable t) {
            logger.error("Cannot get value for key : {}", key, t);
        }
        return payload;
    }
}

四、缓存初始化

4.1 DefaultEurekaServerContext

Eureka 在服务启动过程中,会注入一个 EurekaServerContext 对象,该对象会初始化 Eureka Server的上下文环境。具体如下。

@Configuration(proxyBeanMethods = false)
@Import(EurekaServerInitializerConfiguration.class)
public class EurekaServerAutoConfiguration implements WebMvcConfigurer {
	
	@Bean
	@ConditionalOnMissingBean
	public EurekaServerContext eurekaServerContext(ServerCodecs serverCodecs,
			PeerAwareInstanceRegistry registry, PeerEurekaNodes peerEurekaNodes) {
		return new DefaultEurekaServerContext(this.eurekaServerConfig, serverCodecs,
				registry, peerEurekaNodes, this.applicationInfoManager);
	}
}

4.2 DefaultEurekaServerContext.initialize()

DefaultEurekaServerContext 类在方法中用到了 Spring 中的 @PostConstruct 注解,在注入改实体类后,会调用被 @PostConstruct 标记的无参方法。具体如下。

@Singleton
public class DefaultEurekaServerContext implements EurekaServerContext {
    private final PeerAwareInstanceRegistry registry;

	@PostConstruct
    @Override
    public void initialize() {
        logger.info("Initializing ...");
        peerEurekaNodes.start();
        try {
        	// 调用 PeerAwareInstanceRegistry 中的init方法进行初始化
            registry.init(peerEurekaNodes);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        logger.info("Initialized");
    }
}

4.3 PeerAwareInstanceRegistryImpl.init(PeerEurekaNodes peerEurekaNodes)

DefaultEurekaServerContext 中的 initialize() 方法会调用 PeerAwareInstanceRegistryImpl.init(PeerEurekaNodes peerEurekaNodes) 完成一些初始化动作。

@Singleton
public class PeerAwareInstanceRegistryImpl extends AbstractInstanceRegistry implements PeerAwareInstanceRegistry {
	@Override
    public void init(PeerEurekaNodes peerEurekaNodes) throws Exception {
        this.numberOfReplicationsLastMin.start();
        this.peerEurekaNodes = peerEurekaNodes;
        // 初始化缓存
        initializedResponseCache();
        // 初始化更新自我保护机制阈值定时任务
        scheduleRenewalThresholdUpdateTask();
        // 初始化远程区域注册表
        initRemoteRegionRegistry();

        try {
            Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register the JMX monitor for the InstanceRegistry :", e);
        }
    }
    
    /**
     * initializedResponseCache() 是Eureka Server端完成缓存初始化的主要业务逻辑方法
     */
    @Override
    public synchronized void initializedResponseCache() {
        if (responseCache == null) {
            responseCache = new ResponseCacheImpl(serverConfig, serverCodecs, this);
        }
    }
}

4.4 ResponseCacheImpl

PeerAwareInstanceRegistryImpl.initializedResponseCache(); 是 Eureka Server 端完成缓存初始化的主要业务逻辑方法。在该方法中,主要创建了一个缓存对象-ResponseCacheImpl,该类就是 Eureka 中多级缓存的主要实现。

该类在属性里面定义并创建了一级缓存 readOnlyCacheMap ,在构造方法中使用guava中提供 LoadingCache 创建了二级缓存。具体实现如下。

public class ResponseCacheImpl implements ResponseCache {
	// 一级缓存:读缓存
    private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key, Value>();
	// 二级缓存:读写缓存
    private final LoadingCache<Key, Value> readWriteCacheMap;
    
	ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
        this.serverConfig = serverConfig;
        this.serverCodecs = serverCodecs;
        // 是否使用读缓存,默认为true
        this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
        this.registry = registry;
		
		// 获取定时更新一级缓存的时间配置:默认30s
        long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
		// 初始化 读写缓存
        this.readWriteCacheMap =
                CacheBuilder.newBuilder()
                		// 设置缓存大小,默认1000,可通过 initialCapacityOfResponseCache 配置
                		.initialCapacity(serverConfig.getInitialCapacityOfResponseCache())
                		// 设置时间对象没有被读/写访问则对象从内存中删除(底层线程里进行维护)
                        .expireAfterWrite(serverConfig.getResponseCacheAutoExpirationInSeconds(), TimeUnit.SECONDS)
                        // remova监听器,缓存项被remova时会触发
                        .removalListener(new RemovalListener<Key, Value>() {
                            @Override
                            public void onRemoval(RemovalNotification<Key, Value> notification) {
                                Key removedKey = notification.getKey();
                                if (removedKey.hasRegions()) {
                                    Key cloneWithNoRegions = removedKey.cloneWithoutRegions();
                                    regionSpecificKeys.remove(cloneWithNoRegions, removedKey);
                                }
                            }
                        })
                        // 构建
                        .build(new CacheLoader<Key, Value>() {
                            @Override
                            public Value load(Key key) throws Exception {
                                if (key.hasRegions()) {
                                    Key cloneWithNoRegions = key.cloneWithoutRegions();
                                    regionSpecificKeys.put(cloneWithNoRegions, key);
                                }
                                // 获取缓存
                                Value value = generatePayload(key);
                                return value;
                            }
                        });

        if (shouldUseReadOnlyResponseCache) {// 如果使用读缓存
        	// 开启一个定时任务,每隔 responseCacheUpdateIntervalMs(默认30s)从二级缓存中更新一次
            timer.schedule(getCacheUpdateTask(),
                    new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
                            + responseCacheUpdateIntervalMs),
                    responseCacheUpdateIntervalMs);
        }

        try {
            Monitors.registerObject(this);
        } catch (Throwable e) {
            logger.warn("Cannot register the JMX monitor for the InstanceRegistry", e);
        }
    }

4.4.1 LoadingCache 对象

LoadingCache对象,它是guava中提供的用来实现内存缓存的一个api。创建方式如下。

LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
									// 缓存池大小,在缓存项接近该大小时, Guava开始回收旧的缓存项
				 					.maximumSize(10000)
                					// 1.expireAfterWrite:,在put或者和Load的时候更新缓存的时间戳,因为这两种操作是写操作,
                					//   在get过程中如果发现当前时间与时间戳的差值大于过期时间,就会进行load操作。
                					// 2.expireAfterAccess,和上边最大的区别就是,不管是写还是读都会记录新的时间戳,所以不
                					//   会很快导致缓存过期,所以当读的时候,会和最新的时间戳进行对比,最新的时间戳可能是因
                					//   为写或者读而更改。
                					// 3.refreshAfterWrite, 再调用get进行值的获取的时候才会执行reload操作,如果缓存项没有
                					//   被检索,那么刷新就不会真正的存在。这里的刷新操作需要我们去设置一个cacheLoader,去
                					//   自行实现load法,但事实上如果想要进行异步刷新需要重写cacheLoader的reload方法(继承
                					//   AsyncReloadCacheLoader),因为在经刷新的时候调用的是reload的方法。
 									.expireAfterAccess(10, TimeUnit.MINUTES)
                        			// remova监听器,缓存项被remova时会触发
 									.removalListener(new RemovalListener <Long, String>() {
										@Override
										public void onRemoval(RemovalNotification<Long, String> rn) {
											// 业务逻辑
										}
 									}).recordStats()//开启Guava Cache的统计功能
 									.build(new CacheLoader<String, Object>() {
										@Override
										public Object load(String key) {
											//从 SQL或者NoSql 获取对象
 										}
 									});//CacheLoader类 实现自动加载

CacheLoader 是用来实现缓存自动加载的功能,当触发 readWriteCacheMap.get(key) 方法时,就会回调 CacheLoader.load(String key) 方法,根据 key 去查找实例数据进行缓存。

4.5 ResponseCacheImpl.generatePayload(Key key)

Eureka Server 通过 CacheLoader 来实现二级缓存,当客户端通过key获取缓存时,会调用 CacheLoader.load(String key) 中的 generatePayload(keyt) 方法获取缓存时间,然后存放到 CacheLoader 中,完成二级缓存的初始化。

public class ResponseCacheImpl implements ResponseCache {
	private Value generatePayload(Key key) {
        Stopwatch tracer = null;
        try {
            String payload;
            switch (key.getEntityType()) {
                case Application:
                    boolean isRemoteRegionRequested = key.hasRegions();

                    if (ALL_APPS.equals(key.getName())) {
                        if (isRemoteRegionRequested) {
                            tracer = serializeAllAppsWithRemoteRegionTimer.start();
                            // 获取服务注册地址信息
                            payload = getPayLoad(key, registry.getApplicationsFromMultipleRegions(key.getRegions()));
                        } else {
                            tracer = serializeAllAppsTimer.start();
                            // 获取服务注册地址信息
                            payload = getPayLoad(key, registry.getApplications());
                        }
                    } else if (ALL_APPS_DELTA.equals(key.getName())) {
                        if (isRemoteRegionRequested) {
                            tracer = serializeDeltaAppsWithRemoteRegionTimer.start();
                            versionDeltaWithRegions.incrementAndGet();
                            versionDeltaWithRegionsLegacy.incrementAndGet();
                            // 获取服务注册地址信息
                            payload = getPayLoad(key,
                                    registry.getApplicationDeltasFromMultipleRegions(key.getRegions()));
                        } else {
                            tracer = serializeDeltaAppsTimer.start();
                            versionDelta.incrementAndGet();
                            versionDeltaLegacy.incrementAndGet();
                            // 获取服务注册地址信息
                            payload = getPayLoad(key, registry.getApplicationDeltas());
                        }
                    } else {
                        tracer = serializeOneApptimer.start();
                        // 获取服务注册地址信息
                        payload = getPayLoad(key, registry.getApplication(key.getName()));
                    }
                    break;
                case VIP:
                case SVIP:
                    tracer = serializeViptimer.start();
                    // 获取服务注册地址信息
                    payload = getPayLoad(key, getApplicationsForVip(key, registry));
                    break;
                default:
                    logger.error("Unidentified entity type: {} found in the cache key.", key.getEntityType());
                    payload = "";
                    break;
            }
            return new Value(payload);
        } finally {
            if (tracer != null) {
                tracer.stop();
            }
        }
    }
}

此方法接受一个 Key 类型的参数,返回一个 Value 类型。 其中 Key 中重要的字段有:

  • KeyType ,表示payload文本格式,有 JSON 和 XML 两种值。
  • EntityType ,表示缓存的类型,有 Application , VIP , SVIP 三种值。
  • entityName ,表示缓存的名称,可能是单个应用名,也可能是 ALL_APPS 或 ALL_APPS_DELTA 。

Value中重要的字段有:

  • payload,String 类型,服务地址信息。
  • byte 数组,payload.getBytes(),表示gzip压缩后的字节。

至此,Eureka 缓存初始化完成。

五、缓存同步

在 ResponseCacheImpl 这个类的构造实现中,初始化了一个定时任务,每隔 responseCacheUpdateIntervalMs(默认30s)从二级缓存中更新有差异的数据同步到 readOnlyCacheMap 中。具体实现如下。

public class ResponseCacheImpl implements ResponseCache {
	// 一级缓存:读缓存
    private final ConcurrentMap<Key, Value> readOnlyCacheMap = new ConcurrentHashMap<Key, Value>();
	// 二级缓存:读写缓存
    private final LoadingCache<Key, Value> readWriteCacheMap;
    
	ResponseCacheImpl(EurekaServerConfig serverConfig, ServerCodecs serverCodecs, AbstractInstanceRegistry registry) {
        this.serverConfig = serverConfig;
        this.serverCodecs = serverCodecs;
        // 是否使用读缓存,默认为true
        this.shouldUseReadOnlyResponseCache = serverConfig.shouldUseReadOnlyResponseCache();
        this.registry = registry;
		
		// 获取定时更新一级缓存的时间配置:默认30s
        long responseCacheUpdateIntervalMs = serverConfig.getResponseCacheUpdateIntervalMs();
		
		// 省略其他代码...

        if (shouldUseReadOnlyResponseCache) {// 如果使用读缓存
        	// 开启一个定时任务,每隔 responseCacheUpdateIntervalMs(默认30s)从二级缓存中更新一次
            timer.schedule(getCacheUpdateTask(),
                    new Date(((System.currentTimeMillis() / responseCacheUpdateIntervalMs) * responseCacheUpdateIntervalMs)
                            + responseCacheUpdateIntervalMs),
                    responseCacheUpdateIntervalMs);
        }

		// 省略其他代码...
    }

5.1 ResponseCacheImpl.getCacheUpdateTask()

ResponseCacheImpl 中的 getCacheUpdateTask() 用处获取一个缓存更新的定时任务。

public class ResponseCacheImpl implements ResponseCache {
	private TimerTask getCacheUpdateTask() {
        return new TimerTask() {
            @Override
            public void run() {
                logger.debug("Updating the client cache from response cache");
                for (Key key : readOnlyCacheMap.keySet()) {// 遍历 readOnlyCacheMap
                    if (logger.isDebugEnabled()) {
                        logger.debug("Updating the client cache from response cache for key : {} {} {} {}",
                                key.getEntityType(), key.getName(), key.getVersion(), key.getType());
                    }
                    try {
                        CurrentRequestVersion.set(key.getVersion());
                        Value cacheValue = readWriteCacheMap.get(key);
                        Value currentCacheValue = readOnlyCacheMap.get(key);
                        if (cacheValue != currentCacheValue) { //判断差异信息,如果有差异,则更新
                            readOnlyCacheMap.put(key, cacheValue);
                        }
                    } catch (Throwable th) {
                        logger.error("Error while updating the client cache from response cache for key {}", key.toStringCompact(), th);
                    } finally {
                        CurrentRequestVersion.remove();
                    }
                }
            }
        };
    }
}

六、缓存失效

Spring Cloud Netflix-Eureka(一)、服务注册与发现 中,我们分析了服务的注册过程,其中在 AbstractInstanceRegistry.register() 这个方法中,当完成服务信息保存后,会调用 invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress()); 失效缓存。实际上,服务的注册和下线都会调用该方法进行缓存的失效,保证客户端能获取到最新的数据,保证数据的一致性。

public abstract class AbstractInstanceRegistry implements InstanceRegistry {
	public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        read.lock();
        try {
        	// 省略其他代码...

			invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
			logger.info("Registered instance {}/{} with status {} (replication={})",
                    registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
        } finally {
            read.unlock();
        }
	}

    public boolean cancel(String appName, String id, boolean isReplication) {
        return internalCancel(appName, id, isReplication);
    }
    
    /**
     * 失效缓存
     */
	private void invalidateCache(String appName, @Nullable String vipAddress, @Nullable String secureVipAddress) {
        // invalidate cache
        responseCache.invalidate(appName, vipAddress, secureVipAddress);
    }
}

6.1 ResponseCacheImpl.invalidate(Key… keys)

通过 AbstractInstanceRegistry 的 invalidateCache() 方法,最终会调用 ResponseCacheImpl 中的 invalidate(Key... keys) 方法,完成缓存的失效机制。

public class ResponseCacheImpl implements ResponseCache {
	@Override
    public void invalidate(String appName, @Nullable String vipAddress, @Nullable String secureVipAddress) {
        for (Key.KeyType type : Key.KeyType.values()) {
            for (Version v : Version.values()) {
            	// 缓存失效
                invalidate(
                        new Key(Key.EntityType.Application, appName, type, v, EurekaAccept.full),
                        new Key(Key.EntityType.Application, appName, type, v, EurekaAccept.compact),
                        new Key(Key.EntityType.Application, ALL_APPS, type, v, EurekaAccept.full),
                        new Key(Key.EntityType.Application, ALL_APPS, type, v, EurekaAccept.compact),
                        new Key(Key.EntityType.Application, ALL_APPS_DELTA, type, v, EurekaAccept.full),
                        new Key(Key.EntityType.Application, ALL_APPS_DELTA, type, v, EurekaAccept.compact)
                );
                if (null != vipAddress) {
            		// 缓存失效
                    invalidate(new Key(Key.EntityType.VIP, vipAddress, type, v, EurekaAccept.full));
                }
                if (null != secureVipAddress) {
            		// 缓存失效
                    invalidate(new Key(Key.EntityType.SVIP, secureVipAddress, type, v, EurekaAccept.full));
                }
            }
        }
    }
    
    // 缓存失效
	public void invalidate(Key... keys) {
        for (Key key : keys) {
            logger.debug("Invalidating the response cache key : {} {} {} {}, {}",
                    key.getEntityType(), key.getName(), key.getVersion(), key.getType(), key.getEurekaAccept());

            readWriteCacheMap.invalidate(key);
            Collection<Key> keysWithRegions = regionSpecificKeys.get(key);
            if (null != keysWithRegions && !keysWithRegions.isEmpty()) {
                for (Key keysWithRegion : keysWithRegions) {
                    logger.debug("Invalidating the response cache key : {} {} {} {} {}",
                            key.getEntityType(), key.getName(), key.getVersion(), key.getType(), key.getEurekaAccept());
                    readWriteCacheMap.invalidate(keysWithRegion);
                }
            }
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值