How CoSky Handles Service Registration and Deregistration Lifecycle
CoSky implements a Redis-backed service registry where instances register with a TTL, emit heartbeat messages to renew leases, and publish explicit deregistration events, while discovery nodes subscribe to a heartbeat channel to maintain a consistent local cache of available services.
Managing the lifecycle of microservice instances requires a robust mechanism for registration, health checking, and cleanup. In the ahoo-wang/cosky repository, the service registration and deregistration lifecycle is built on a lightweight Redis architecture that combines time-to-live (TTL) keys with pub/sub messaging to ensure real-time consistency across the cluster.
Service Registration Architecture in CoSky
The Core Registration Flow
In cosky-discovery/src/main/java/me/ahoo/cosky/discovery/redis/RedisServiceRegistry.java, the register() method persists service instance metadata as JSON to Redis and immediately publishes the instance key to the HEARTBEAT_CHANNEL.
The implementation follows this sequence:
- Constructs a Redis key using the pattern
cosky:service:{serviceId}:{instanceId}. - Serializes the
ServiceInstanceto JSON format. - Stores the value with a configurable TTL defined by
ServiceRegistryProperties#getTtl(). - Publishes the key to the
cosky:service:heartbeatchannel viaredisTemplate.convertAndSend().
This pub/sub notification ensures that discovery clients receive immediate updates when new services join the cluster, eliminating polling delays.
Interface Contract
The ServiceRegistry interface in cosky-discovery/src/main/java/me/ahoo/cosky/discovery/ServiceRegistry.java defines the reactive contract with three primary operations:
Mono<ServiceInstance> register(ServiceInstance instance)Mono<Void> deregister(String serviceId, String instanceId)Mono<Void> renew(String serviceId, String instanceId)
Heartbeat Mechanism and Lease Renewal
Renewing Service Leases
To prevent TTL expiration while a service remains healthy, the renew() method in RedisServiceRegistry refreshes the key's expiration time using redisTemplate.expire(key, properties.getTtl()). This lightweight operation extends the instance's lease without rewriting the entire value.
Scheduled Heartbeat Emission
While the registry includes a @Scheduled method sendHeartbeat to periodically publish heartbeat messages, the primary mechanism for keeping instances visible involves the initial registration publish and subsequent renewals. The heartbeat channel serves as the event bus for both new registrations and periodic keep-alives.
Service Deregistration and Cache Consistency
Explicit Deregistration
When a service shuts down gracefully, the deregister() method in RedisServiceRegistry performs two critical operations:
- Deletes the instance key from Redis using
redisTemplate.delete(key). - Publishes a deregistration message with the prefix
deregister:followed by the key to the heartbeat channel.
This dual action ensures that both the persistent store and all subscribed discovery clients remove the instance simultaneously.
Discovery Side Reaction
The RedisServiceDiscovery class in cosky-discovery/src/main/java/me/ahoo/cosky/discovery/redis/RedisServiceDiscovery.java implements MessageListener to subscribe to the heartbeat channel. In the onMessage() method, it distinguishes between heartbeat and deregistration events:
- Heartbeat messages: Update the local cache with fresh instance data and emit an
InstanceChangedEventof typeHEARTBEAT. - Deregistration messages: Remove the instance from the local cache using
localCache.remove(key)and emit anInstanceChangedEventof typeDEREGISTERED.
This design maintains a strongly consistent local view of the service topology without requiring repeated full scans of Redis.
Practical Implementation Examples
The following examples demonstrate how to interact with the CoSky service registration and deregistration lifecycle in a Spring Boot application.
Registering a service at startup:
@Component
@RequiredArgsConstructor
public class ServiceRegistrationRunner implements ApplicationRunner {
private final ServiceRegistry serviceRegistry;
@Override
public void run(ApplicationArguments args) {
ServiceInstance instance = ServiceInstance.builder()
.serviceId("payment-service")
.instanceId(UUID.randomUUID().toString())
.host("192.168.1.100")
.port(8080)
.metadata(Map.of("region", "us-east-1", "version", "2.1.0"))
.build();
serviceRegistry.register(instance)
.doOnSuccess(i -> System.out.println("Registered: " + i))
.subscribe();
}
}
Deregistering gracefully on shutdown:
@Component
@RequiredArgsConstructor
public class ServiceShutdownHook {
private final ServiceRegistry serviceRegistry;
private final ServiceInstance currentInstance; // Injected or constructed
@PreDestroy
public void destroy() {
serviceRegistry.deregister(
currentInstance.getServiceId(),
currentInstance.getInstanceId()
).doOnSuccess(v -> System.out.println("Deregistered successfully"))
.subscribe();
}
}
Reacting to instance lifecycle events:
@Component
public class InstanceChangeHandler {
@EventListener
public void handleInstanceChanged(InstanceChangedEvent event) {
if (event.getType() == InstanceChangedEvent.Type.DEREGISTERED) {
System.out.println("Service instance removed: " + event.getInstance());
} else if (event.getType() == InstanceChangedEvent.Type.HEARTBEAT) {
System.out.println("Heartbeat received from: " + event.getInstance());
}
}
}
Summary
- CoSky stores service instances in Redis with configurable TTLs to ensure automatic cleanup when services fail or network partitions occur.
- The
RedisServiceRegistryclass manages the lifecycle through theregister(),deregister(), andrenew()methods, utilizing theHEARTBEAT_CHANNELfor event propagation. - Service discovery nodes maintain a local cache via
RedisServiceDiscovery, which subscribes to heartbeat messages and updates its view in real-time. - Explicit deregistration triggers immediate cache eviction across all consumers through the pub/sub mechanism, while TTL expiration handles implicit cleanup for crashed instances.
Frequently Asked Questions
How does CoSky ensure high availability during service registration?
CoSky leverages Redis as a centralized, highly available data store with TTL-based lease management. If a registering service loses connectivity after registration, the TTL expiration automatically removes the stale entry, preventing failed instances from remaining in the registry indefinitely.
What happens if a service crashes without calling deregister?
The Redis key associated with the service instance will expire after the TTL duration configured in ServiceRegistryProperties (defaulting to 30 seconds). Once expired, subsequent discovery queries will not return the instance, effectively handling the deregistration implicitly without requiring explicit cleanup.
How does the heartbeat interval affect service discovery performance?
The heartbeat interval, configured via cosky.service.registry.heartbeat-interval, determines the frequency of cache refresh events across the cluster. Shorter intervals reduce the time to detect new or failed instances but increase Redis pub/sub traffic, while longer intervals trade off discovery latency for reduced network overhead.
Can CoSky work with Redis Sentinel or Cluster for production deployments?
Yes, the implementation uses standard StringRedisTemplate operations, making it compatible with Redis Sentinel and Cluster configurations. The pub/sub mechanism for heartbeats and the TTL-based storage work transparently across these topologies, ensuring the service registration and deregistration lifecycle remains robust in distributed Redis environments.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →