Skip to content

Registry Module Design ​

中文 | English


Overview ​

A Redis-based service registration and discovery module. Key structures and operations follow the current implementation under registry/src/main/java. Configuration-center capability lives in the separate config module (see the Chinese config.md).

Core Roles ​

1. Service Provider ​

Implementation class RedisServiceProvider (implements ServiceProvider):

  • Service registration: writes the instance Hash, adds the heartbeat ZSet entry and the service-index Set entry — all atomically via Lua scripts
  • Heartbeat handling: each heartbeat writes what HeartbeatStateManager decides (timestamp only, or timestamp + metadata)
  • Expired-instance cleanup: a background daemon pool removes heartbeats-expired temporary instances
  • Graceful shutdown: deregister deletes atomically and broadcasts REMOVED

2. Service Consumer ​

Implementation class RedisServiceConsumer (implements ServiceConsumer):

  • Service discovery: takes active instance IDs from the heartbeat window, then reads each instance Hash to assemble ServiceInstance
  • Change subscription: subscribes to the RTopic channel {prefix}:services:{serviceName}:changes
  • Metadata/metrics filtering: pushed down to Lua for server-side filtering
  • Optional health probing: with enableHealthCheck=true, probes by protocol and reports HEALTH_RECOVERY / HEALTH_FAILURE on flips

RedisNamingService aggregates Provider and Consumer (composition with a shared lifecycle) and implements NamingService.

3. Admin ​

admin.RegistryAdminService: lists services, inspects instance details (including metrics), aggregates metrics, reports overall registry health, and can trigger expired-instance cleanup manually.

Interface Design ​

The two business-role interfaces are both extended by NamingService (the former ServiceRegistry/ServiceDiscovery technical-view interfaces were removed; their method names survive as NamingService default aliases):

java
public interface ServiceProvider {
    void register(ServiceInstance instance);
    void deregister(ServiceInstance instance);
    void sendHeartbeat(ServiceInstance instance);
    void batchSendHeartbeats(List<ServiceInstance> instances);
    void start();
    void stop();
    boolean isRunning();
}

public interface ServiceConsumer {
    List<ServiceInstance> getAllInstances(String serviceName);
    List<ServiceInstance> getHealthyInstances(String serviceName);
    List<ServiceInstance> getInstances(String serviceName, boolean healthy);
    List<ServiceInstance> getInstancesByMetadata(String serviceName, Map<String, String> metadataFilters);
    List<ServiceInstance> getHealthyInstancesByMetadata(String serviceName, Map<String, String> metadataFilters);
    void subscribe(String serviceName, ServiceChangeListener listener);
    void unsubscribe(String serviceName, ServiceChangeListener listener);
    void start();
    void stop();
    boolean isRunning();
}

public interface NamingService extends ServiceProvider, ServiceConsumer {
    default List<ServiceInstance> getHealthyInstances(String serviceName); // alias of getInstances(name, true)
    List<ServiceInstance> getInstancesByMetadata(String serviceName, Map<String, String> metadataFilters);
    List<ServiceInstance> getHealthyInstancesByMetadata(String serviceName, Map<String, String> metadataFilters);

    // Legacy-vocabulary default aliases (existing call sites keep compiling)
    default void heartbeat(ServiceInstance instance);                        // -> sendHeartbeat
    default void batchHeartbeat(List<ServiceInstance> instances);            // -> batchSendHeartbeats
    default List<ServiceInstance> discover(String serviceName);              // -> getAllInstances
    default List<ServiceInstance> discoverHealthy(String serviceName);       // -> getHealthyInstances
    default List<ServiceInstance> discoverByMetadata(String, Map<String, String>);       // -> getInstancesByMetadata
    default List<ServiceInstance> discoverHealthyByMetadata(String, Map<String, String>); // -> getHealthyInstancesByMetadata
}

Beyond the interfaces, RedisNamingService adds: getInstancesByFilters(name, metadataFilters, metricsFilters), getHealthyInstancesByFilters(...), chooseHealthyInstance(name, lb, context), chooseHealthyInstanceByFilters(name, md, mt, lb, context), and getConfig().

Redis Key Structure (three-level storage) ​

Key templates are generated by keys.RegistryKeys; the default prefix is redis_streaming_registry (registry.BaseRedisConfig.DEFAULT_KEY_PREFIX):

{prefix}:services                                        # Set: service index (all registered service names)
{prefix}:services:{serviceName}:heartbeats               # ZSet: heartbeat index (score = last heartbeat ms, member = instanceId)
{prefix}:services:{serviceName}:instance:{instanceId}    # Hash: instance details
{prefix}:services:{serviceName}:changes                  # Pub/Sub: service change notification channel

Instance Hash fields (InstanceEntryCodec.buildInstanceData): host, port, protocol, enabled, healthy, weight, ephemeral, registrationTime, lastHeartbeatTime, lastMetadataUpdate, metadata (JSON string), metrics (JSON string — a data-driven field: the framework no longer writes it; external processes may write it for the metrics filters and ScoredLoadBalancer).

RegistryKeys also sanitizes and validates names: :, spaces, tabs and newlines in service names / instance IDs are replaced with _ or - (sanitizeServiceName / sanitizeInstanceId); registration and deregistration go through validateAndSanitizeXxx, and an instance ID containing : throws IllegalArgumentException.

Heartbeat Mechanism ​

Heartbeats are not scheduled by the framework in the background — the caller triggers sendHeartbeat / batchSendHeartbeats (the spring-boot-starter AutoServiceRegistration does so every 30 seconds by default).

Each heartbeat first goes through HeartbeatStateManager, which combines HeartbeatConfig into an UpdateDecision:

DecisionLua update_modeWritten content
HEARTBEAT_ONLYheartbeat_onlyheartbeat timestamp (ZSet score + Hash lastHeartbeatTime)
METADATA_UPDATEmetadata_updatetimestamp + metadata JSON
NO_UPDATE—skipped entirely

Decision basis (HeartbeatConfig defaults): metadata change detection is off by default (enableMetadataChangeDetection=false); when enabled, a content-hash change past metadataUpdateIntervalSeconds (600s) yields METADATA_UPDATE; a metadata update also advances the heartbeat timestamp, otherwise a heartbeat due after heartbeatInterval (3s) yields HEARTBEAT_ONLY. (The former tiered metrics update — metricsInterval/changeThresholds/forceMetricsUpdateThreshold/metrics_update/full_update — was removed on 2026-10-12 together with the provider-side metrics collection package; UpdateDecision has only the three values above.)

Every heartbeat of a temporary instance slides the instance Hash TTL forward (TTL = heartbeatTimeoutSeconds, default 90 seconds); persistent instances carry no TTL.

Any update other than heartbeat_only broadcasts one UPDATED event.

Expired-Instance Cleanup ​

RedisServiceProvider.start() starts a daemon thread pool that runs cleanupExpiredInstances — first after 60 seconds, then every 30 seconds:

  • For every service in the index it runs the CLEANUP_EXPIRED_INSTANCES_WITH_SNAPSHOTS Lua script: among instances with heartbeat_time < now - timeout, those with ephemeral=true (or a missing field) are removed from the ZSet and their Hash deleted; persistent instances (ephemeral=false) only get healthy set to false and are never deleted
  • When a service's heartbeat ZSet becomes empty after cleanup, the service is atomically removed from the service-index Set (a ZCARD check + SREM inside the Lua)
  • Removing a temporary instance broadcasts REMOVED from the snapshot; once a persistent instance is marked unhealthy, a client's active heartbeat restores healthy to true

Metadata / Metrics Filtering ​

Filtering happens inside the Lua executed by RegistryLuaScriptExecutor.executeGetInstancesByFilters:

  • Candidate set = active instances within the heartbeat window (ZRANGEBYSCORE heartbeats (now-timeout, +inf)
  • Filter keys parse into field + operator (== != > >= < <=; no suffix means ==); the metadata and metrics JSON documents are matched independently, all conditions ANDed
  • Comparison tries tonumber numeric comparison first and falls back to lexicographic string comparison when either side is non-numeric; a missing field never matches
  • An empty filter set is equivalent to "return all active instances"

Change Notification ​

Changes are broadcast over the Redis Pub/Sub channel {prefix}:services:{serviceName}:changes with an event.ServiceChangeEvent payload (serviceName, action, instanceId, timestamp, instance snapshot). Actions are defined by ServiceChangeAction:

  • ADDED / REMOVED / UPDATED: registration, deregistration, attribute updates (including non-timestamp-only heartbeats)
  • CURRENT: current-state notification delivered on subscription
  • HEALTH_RECOVERY / HEALTH_FAILURE: consumer-side health-probe verdict flips (requires enableHealthCheck=true)

Protocols & Health Checks ​

ProtocolCheckerMethod
HTTP / HTTPSHttpHealthCheckerGET {uri}/health; 2xx-3xx counts as healthy, request errors fall back to TCP connectivity
TCP / UDPTcpHealthCheckerSocket connection test
WS / WSSStandardHealthCheckerTCP connectivity test (the removed WebSocketHealthChecker was exactly this)
Everything else (GRPC/GRPCS, DUBBO, etc.)StandardHealthChecker default branchTCP connectivity test — there is no dedicated gRPC health-check implementation
Customextend CustomHealthCheckerTCP connectivity first (3s timeout), then the subclass doCheck

The default timeout is 5000ms (no-arg constructors of the checkers); values ≤ 0 are normalized to 5000ms. HealthCheckManager schedules all instance probes on a shared daemon pool and reports a result only when the status flips.

StandardProtocol enum values: HTTP, HTTPS, TCP, UDP, WS, WSS, KCP, GRPC, GRPCS, DUBBO, DUBBO2. MessagingProtocol holds the four Redis-related values only: REDIS_STREAM, REDIS_STREAM_TLS, REDIS_PUBSUB, REDIS_PUBSUB_TLS.

Configuration ​

Config classKey fields (defaults)
BaseRedisConfig (registry)keyPrefix=redis_streaming_registry, enableKeyPrefix=true
NamingServiceConfigenableHealthCheck=false, healthCheckInterval=30 (SECONDS), healthCheckTimeout=5000ms, enableAdminService=true
ServiceProviderConfigheartbeatTimeoutSeconds=90
ServiceConsumerConfigenableHealthCheck=false, healthCheckInterval=30, healthCheckTimeout=5000, heartbeatTimeoutSeconds=90, enableAdminService=true
HeartbeatConfigheartbeatInterval=3s, enableMetadataChangeDetection=false, metadataUpdateIntervalSeconds=600
LoadBalancerConfigcpuWeight=1.0, latencyWeight=1.0, targetLatencyMs=50.0, regionBoost=1.1, zoneBoost=1.05, remaining weights default 0, hard thresholds default -1 (disabled)

Lua script cache: RegistryLuaScriptExecutor runs the scripts (register, deregister, heartbeat update, expired cleanup, active-instance query, filtered query) through the Redisson RScript API.


Version: 0.2.0 Last Updated: 2026-10-05

Related documentation: