Registry Module Design
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
HeartbeatStateManagerdecides (timestamp only, or timestamp + metadata) - Expired-instance cleanup: a background daemon pool removes heartbeats-expired temporary instances
- Graceful shutdown:
deregisterdeletes atomically and broadcastsREMOVED
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
RTopicchannel{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 reportsHEALTH_RECOVERY/HEALTH_FAILUREon 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):
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 channelInstance 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:
| Decision | Lua update_mode | Written content |
|---|---|---|
HEARTBEAT_ONLY | heartbeat_only | heartbeat timestamp (ZSet score + Hash lastHeartbeatTime) |
METADATA_UPDATE | metadata_update | timestamp + 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_SNAPSHOTSLua script: among instances withheartbeat_time < now - timeout, those withephemeral=true(or a missing field) are removed from the ZSet and their Hash deleted; persistent instances (ephemeral=false) only gethealthyset tofalseand are never deleted - When a service's heartbeat ZSet becomes empty after cleanup, the service is atomically removed from the service-index Set (a
ZCARDcheck +SREMinside the Lua) - Removing a temporary instance broadcasts
REMOVEDfrom the snapshot; once a persistent instance is marked unhealthy, a client's active heartbeat restoreshealthytotrue
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==); themetadataandmetricsJSON documents are matched independently, all conditions ANDed - Comparison tries
tonumbernumeric 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 subscriptionHEALTH_RECOVERY/HEALTH_FAILURE: consumer-side health-probe verdict flips (requiresenableHealthCheck=true)
Protocols & Health Checks
| Protocol | Checker | Method |
|---|---|---|
| HTTP / HTTPS | HttpHealthChecker | GET {uri}/health; 2xx-3xx counts as healthy, request errors fall back to TCP connectivity |
| TCP / UDP | TcpHealthChecker | Socket connection test |
| WS / WSS | StandardHealthChecker | TCP connectivity test (the removed WebSocketHealthChecker was exactly this) |
| Everything else (GRPC/GRPCS, DUBBO, etc.) | StandardHealthChecker default branch | TCP connectivity test — there is no dedicated gRPC health-check implementation |
| Custom | extend CustomHealthChecker | TCP 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 class | Key fields (defaults) |
|---|---|
BaseRedisConfig (registry) | keyPrefix=redis_streaming_registry, enableKeyPrefix=true |
NamingServiceConfig | enableHealthCheck=false, healthCheckInterval=30 (SECONDS), healthCheckTimeout=5000ms, enableAdminService=true |
ServiceProviderConfig | heartbeatTimeoutSeconds=90 |
ServiceConsumerConfig | enableHealthCheck=false, healthCheckInterval=30, healthCheckTimeout=5000, heartbeatTimeoutSeconds=90, enableAdminService=true |
HeartbeatConfig | heartbeatInterval=3s, enableMetadataChangeDetection=false, metadataUpdateIntervalSeconds=600 |
LoadBalancerConfig | cpuWeight=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: