Este artigo é para aqueles que usam um cache efetivo em seus aplicativos e desejam adicionar estabilidade não apenas ao aplicativo, mas a todo o ambiente, simplesmente adicionando 1 classe ao projeto.
Se você se reconhece, continue lendo.
O que é um disjuntor?

O tema é hackeado como o mundo e não vou aborrecê-lo, aumentando a entropia e repetindo a mesma coisa. Do meu ponto de vista, Martin Fowler falou o melhor de tudo
aqui , mas vou tentar encaixar a definição em uma frase:
funcionalidade que evita solicitações conscientemente condenadas a um serviço indisponível, permitindo que “fique de joelhos” e continue a operação normal .
Idealmente, evitando solicitações condenadas, o disjuntor (a seguir designado CB) não deve interromper sua aplicação. Em vez disso, é uma boa prática retornar, se não os dados mais atuais, mas ainda relevantes (“não sujos”) ou, se isso não for possível, algum valor padrão.
Objetivos
Destacamos o principal:
- É necessário permitir que a fonte de dados se recupere, interrompendo as consultas por um tempo
- No caso de interromper solicitações para o serviço de destino, você precisa fornecer, se não os dados mais recentes, mas ainda relevantes
- Se o serviço de destino estiver indisponível e não houver dados relevantes, forneça uma estratégia de comportamento (retornando o valor padrão ou outra estratégia adequada para um caso específico)
Mecanismo de implementação
Caso: o serviço está disponível (primeira solicitação)
- Vamos para o cache. Por chave (CRT veja abaixo). Vemos que não há nada no cache
- Nós vamos para o serviço de destino. Nós obtemos o valor
- Armazenamos o valor no cache, definimos-o para um TTL que cubra o tempo máximo possível que o serviço de destino não está disponível, mas ao mesmo tempo não deve exceder o período de relevância dos dados que você está pronto para fornecer ao cliente em caso de perda de conexão com o serviço de destino.
- O tempo de atualização de cache (CRT) é armazenado no cache para o valor da cláusula 3 - o tempo após o qual você precisa tentar acessar o serviço de destino e atualizar o valor
- Retorne o valor do item 2 para o usuário
Caso: a CRT não expirou
- Vamos para o cache. Pela chave, encontramos CRT. Vemos que é relevante
- Obtenha o valor do cache.
- Retorne o valor ao usuário.
Caso: CRT expirou, o serviço de destino está disponível
- Vamos para o cache. Pela chave, encontramos CRT. Vemos que é irrelevante
- Nós vamos para o serviço de destino. Nós obtemos o valor
- Atualizando o valor no cache e seu TTL
- Atualize o CRT adicionando período de atualização de cache (CRP) - este é o valor que precisa ser adicionado ao CRT para obter o próximo CRT
- Retorne o valor ao usuário.
Caso: CRT expirou, serviço de destino indisponível
- Vamos para o cache. Pela chave, encontramos CRT. Vemos que é irrelevante
- Nós vamos para o serviço de destino. Ele não está disponível
- Obtenha o valor do cache. Não é o mais recente (com um CRT podre), mas ainda é relevante, pois seu TTL ainda não expirou
- Devolvemos para o usuário
Caso: CRT expirada, serviço de destino indisponível, nada no cache
- Vamos para o cache. Pela chave, encontramos CRT. Vemos que é irrelevante
- Nós vamos para o serviço de destino. Ele não está disponível
- Obtenha o valor do cache. Ele não é
- Estamos tentando aplicar uma estratégia especial para esses casos. Por exemplo, retornando um valor padrão para um campo especificado ou um valor especial do tipo "Esta informação não está disponível no momento". Em geral, se isso for possível, é melhor retornar algo e não interromper o aplicativo. Se isso não for possível, será necessário aplicar a estratégia de lançamento de exceção e resposta rápida ao usuário da exceção.
O que vamos usar
Eu uso o Spring Boot 1.5 no meu projeto, ainda não encontrei tempo para atualizar para a segunda versão.
Como o artigo não saiu duas vezes mais, usarei Lombok.
Como armazenamento de valor-chave (doravante referido simplesmente como KV), uso o Redis 5.0.3, mas tenho certeza de que o Hazelcast ou um análogo funcionará. O principal é que existe uma implementação da interface CacheManager. No meu caso, este é o RedisCacheManager de spring-boot-starter-data-redis.
Implementação
Acima, na seção “Mecanismo de Implementação”, foram feitas duas definições importantes: CRT e CRP. Vou escrevê-los novamente com mais detalhes, porque eles são muito importantes para entender o código a seguir:
O tempo de atualização de cache (
CRT ) é uma entrada separada em KV (chave + postfix "_crt"), que mostra a hora em que é hora de ir ao serviço de destino para obter novos dados. Ao contrário do TTL, o início do CRT não significa que seus dados estão "podres", mas apenas que provavelmente ficará mais recente no serviço de destino. Ficou fresco - bem, se não, e a corrente diminuirá.
O período de atualização de cache (
CRP ) é um valor adicionado ao CRT após a pesquisa do serviço de destino (não importa se é bem-sucedido ou não). Graças a ela, um serviço remoto tem a capacidade de "recuperar o fôlego" e restaurar o trabalho em caso de queda.
Então, tradicionalmente, começamos projetando a interface principal. É através dele que você precisará trabalhar com o cache se precisar da lógica do CB. Deve ser o mais simples possível:
public interface CircuitBreakerService { <T> T getStableValue(StableValueParameter parameter); void evictValue(EvictValueParameter parameter); }
Parâmetros de interface:
@Getter @AllArgsConstructor public class StableValueParameter<T> { private String cachePrefix;
@Getter @AllArgsConstructor public class EvictValueParameter { private String cachePrefix; private String objectCacheKey; }
É assim que vamos usá-lo:
public AccountDataResponse findAccount(String accountId) { final StableValueParameter<?> parameter = new StableValueParameter<>( ACCOUNT_CACHE_PREFIX, accountId, properties.getCrpInSeconds(), () -> bankClient.findById(accountId) ); return circuitBreakerService.getStableValue(parameter); }
Se você precisar limpar o cache, então:
public void evictAccount(String accountId) { final EvictValueParameter parameter = new EvictValueParameter( ACCOUNT_CACHE_PREFIX, accountId ); circuitBreakerService.evictValue(parameter); }
Agora, o mais interessante é a implementação (explicada nos comentários no código):
@Override public <T> T getStableValue(StableValueParameter parameter) { final Cache cache = cacheManager.getCache(parameter.getCachePrefix()); if (cache == null) { return logAndThrowUnexpectedCacheMissing(parameter.getCachePrefix(), parameter.getObjectCacheKey()); }
Se o serviço de destino estiver indisponível, tente obter os dados ainda relevantes do cache:
private <T> T getFromTargetServiceAndUpdateCache( StableValueParameter parameter, Cache cache, String crtKey, LocalDateTime crt ) { T result; try { result = getFromTargetService(parameter); } catch (WebServiceIOException ex) { log.warn( "[CircuitBreaker] Service responded with error: {}. Try get from cache {}: {}", ex.getMessage(), parameter.getCachePrefix(), parameter.getObjectCacheKey()); result = getFromCacheOrDisasterStrategy(parameter, cache); } cache.put(parameter.getObjectCacheKey(), result); cache.put(crtKey, crt.plusSeconds(parameter.getCrpInSeconds())); return result; } private static <T> T getFromTargetService(StableValueParameter parameter) { return (T) parameter.getTargetServiceAction().get(); }
Se não houver dados reais no cache (eles foram excluídos pelo TTL e o serviço de destino ainda está indisponível), usamos o DisasterStrategy:
private <T> T getFromCacheOrDisasterStrategy(StableValueParameter parameter, Cache cache) { return (T) getFromCache(parameter, cache).orElseGet(() -> parameter.getDisasterStrategy().getValue()); }
Remover do cache não é nada interessante, darei aqui apenas para completar:
private <T> T getFromCacheOrDisasterStrategy(StableValueParameter parameter, Cache cache) { return (T) getFromCache(parameter, cache).orElseGet(() -> parameter.getDisasterStrategy().getValue()); }
Remover do cache não é nada interessante, darei aqui apenas para completar:
@Override public void evictValue(EvictValueParameter parameter) { final Cache cache = cacheManager.getCache(parameter.getCachePrefix()); if (cache == null) { logAndThrowUnexpectedCacheMissing(parameter.getCachePrefix(), parameter.getObjectCacheKey()); return; } final String crtKey = parameter.getObjectCacheKey() + CRT_CACHE_POSTFIX; cache.evict(crtKey); }
Estratégia de desastre

Essa é, de fato, a lógica que ocorre se o CRT expirar, o serviço de destino não estiver disponível e não houver nada no cache.
Eu queria descrever essa lógica separadamente, porque muitos não conseguem pensar em como implementá-lo. Mas é isso que torna nosso sistema verdadeiramente estável.
Você não quer sentir esse sentimento de orgulho em sua criação quando tudo o que só pode falhar é recusado e seu sistema ainda funciona. Mesmo que, por exemplo, no campo "preço" não seja exibido o custo real das mercadorias, mas a inscrição: "atualmente sendo especificada", mas quão melhor é isso do que a resposta "500 serviços não estão disponíveis". Afinal, por exemplo, os 10 campos restantes: descrição do produto etc. você voltou. Quanto a qualidade de um serviço desse tipo muda? .. Minha ligação é prestar mais atenção aos detalhes, melhorando-os.
Terminando a digressão lírica. Portanto, a interface da estratégia será a seguinte:
public interface DisasterStrategy<T> { T getValue(); }
Você deve escolher a implementação, dependendo do caso específico. Por exemplo, se você pode retornar algum valor padrão, pode fazer algo assim:
public class DefaultValueDisasterStrategy implements DisasterStrategy<String> { @Override public String getValue() { return " "; } }
Ou, se em um caso específico, você não precisar retornar nada, poderá lançar uma exceção:
public class ThrowExceptionDisasterStrategy implements DisasterStrategy<Object> { @Override public Object getValue() { throw new CircuitBreakerNullValueException("Ops! Service is down and there's null value in cache"); } }
Nesse caso, o CRT não será incrementado e a próxima solicitação seguirá novamente para o serviço de destino.
Conclusão
Eu aderir ao seguinte ponto de vista - se você tiver a oportunidade de usar uma solução pronta e não se preocupar, de fato, embora seja simples, mas ainda ande de bicicleta neste artigo, faça-o. Use este artigo para entender como funciona, e não como um guia de ação.
Existem muitas soluções prontas, especialmente se você estiver usando o Spring Boot 2, como o Hystrix.
O mais importante a entender é que esta solução é baseada no cache e sua eficácia é igual à eficácia do cache. Se o cache for ineficaz (poucos acertos, muitas falhas), esse disjuntor será igualmente ineficaz: cada falha de cache será acompanhada de uma viagem ao serviço de destino, que, talvez neste momento, esteja em agonia e agonia, tentando aumentar.
Certifique-se de medir a eficácia do seu cache antes de aplicar essa abordagem. Isso pode ser feito por "Taxa de acertos do cache" = acertos / (acertos + erros), deve tender para 1, e não para 0.
E sim, ninguém incomoda você manter várias variedades de CB em seu projeto de uma só vez, usando a que melhor resolve o problema específico.