Połączenie aplikacji do 2 baz danych jednocześnie (redis)

1

Rozwijam aplikację backendową napisaną w java & micronaut.
Aktualnie aplikacja łączy się do bazy danych redis, wystawia api po websocketach.
Aplikacja używa podejścia reaktywnego (mono, flux etc).
Jest wymaganie aby aplikacja łączyła się do 2 baz jednocześnie ta druga jest docelową, na która migrujemy,
pierwsza deprecated docelowo do odłączenia.

Każde żądanie do aplikacji pobiera ileś tam wiadomości z bazy można określić ile np. 50.
Teraz trzeba tak obsłużyć taki request aby 25 było obsłużone z bazy pierwszej a 25 z nowej bazy.

Pytanie jak podejść do zaimplementowania takiego mechanizmu?

Codebase jest spory nie chciałem w każdej klasie "wstrzykiwać" 2 klientów do bazy i powtarzać
warunku czy użyć klienta do pierwszej bazy czy do drugiej.

Jednym z pomysłów na jaki wpadłem to przeszukanie miejsc w kodzie gdzieś nisko w hierarchii
gdzie używany jest RedisClient i zamianę tego na coś w stylu RedisClientSupplier,
który miałby metodę Mono<RedisClient> get() i wyciągał by z kontekstu czy działamy w trybie nowej czy starej bazy.

@Singleton
public class RedisClientSupplier {

    private final RedisClient redisClientOld;
    private final RedisClient redisClientNew;

    public RedisClientSupplier(@Named("old") RedisClient redisClientOld, @Named("new") RedisClient redisClientNew) {
        this.redisClientOld = redisClientOld;
        this.redisClientNew = redisClientNew;
    }
    public Mono<RedisClient> get() {
        return Mono.deferContextual(ctx -> ctx.get("mode") == RedisConnectionMode.OLD
                ? Mono.just(redisClientOld) : Mono.just(redisClientNew));
    }
}

Natomiast nie jestem pewny tego rozwiązania zwłaszcza, że na razie nie działa, kontekst niepoprawnie się propaguje.
Poza tym są miejsca w kodzie gdzie podczas tworzenia beana już następuję nawiązywanie połączenia do bazy
co zmusiłoby do rozmnożenia kolejnego beana na 2 jeden dla jednej bazy a drugi dla drugiej.

4

Taki może mały offtop, ale i tak klauzula sumienia każe mi to napisać:

Tworząc (wiem, wiem - to było dawno temu i nie zależało od Ciebie) system/apkę trzeba było od razu nałożyć jakąś abstrakcję na komunikację z bazą, a nie czytać/pisać do niej bezpośrednio. Jakby to było ogarnięte to teraz nie musiałbyś szukać w kodzie wystąpień funkcji komunikujących się z bazą, a jedynie w jednym miejscu (czyli tam, gdzie masz zaimplementowany swój middleware do bazy danych) poprawić sposób zapisu/odczytu. Reszta aplikacji nawet by nie miała zielonego pojęcia, że cokolwiek się zmieniło pod spodem, że jakakolwiek migracja miała miejsce.

1
cerrato napisał(a):

Taki może mały offtop, ale i tak klauzula sumienia każe mi to napisać:

Tworząc (wiem, wiem - to było dawno temu i nie zależało od Ciebie) system/apkę trzeba było od razu nałożyć jakąś abstrakcję na komunikację z bazą, a nie czytać/pisać do niej bezpośrednio. Jakby to było ogarnięte to teraz nie musiałbyś szukać w kodzie wystąpień funkcji komunikujących się z bazą, a jedynie w jednym miejscu (czyli tam, gdzie masz zaimplementowany swój middleware do bazy danych) poprawić sposób zapisu/odczytu. Reszta aplikacji nawet by nie miała zielonego pojęcia, że cokolwiek się zmieniło pod spodem, że jakakolwiek migracja miała miejsce.

A może on właśnie przerabia dokładnie to miejsce :-)
Jeśli codebase jest spory to "abstrakcja na dane" też może być sama w sobie spora.

2

Jednym z rozwiązań byłoby wydzierganie własnego proxy do redisa, wówczas byłoby to przezroczyste dla aplikacji. Własne proxy z racji tego, że proxy typowo pracują na poziomie połączenia, a Ty chcesz mieć na poziomie requestu/paczki requestów.

Appka -> RedisClient -> CustomRedisProxy (RedisClient) -> Redis#1, Redis#2, ...

Minusem rozwiązania jest potencjalnie konieczność zapoznania się z protokołem redisowym https://redis.io/docs/latest/develop/reference/protocol-spec/

Brak abstrakcji, o której wspominał @cerrato może sugerować, że napisanie takiego proxy może być zbyt dużym wyzwaniem.

1

Jednym z pomysłów na jaki wpadłem to przeszukanie miejsc w kodzie gdzieś nisko w hierarchii
gdzie używany jest RedisClient

Czy RedisClient jest interfejsem? Bo jeśli tak, to pewnie możesz w aplikację wstrzyknąć swoją implementację, która w środku będzie miała logikę rozrzucania zapytań na obie bazy. Może uda się uniknąć bolesnego refactoru.

Coś w stylu pewnie:

class Config {
  @Bean RedisClient myFancyRedisClient(@Named(...) ..., ...) {
     // tu sobie go stwórz, niech twoja klasa opakuje oryginalne RedisClienty

  }
}
0

trzeba było od razu nałożyć jakąś abstrakcję na komunikację z bazą

jestem nowy w firmie

Jednym z rozwiązań byłoby wydzierganie własnego proxy do redisa

zbyt kosztowne

Czy RedisClient jest interfejsem?

klasa z biblioteki https://lettuce.io/core/release/api/io/lettuce/core/RedisClient.html

1
jarekr000000 napisał(a):

A może on właśnie przerabia dokładnie to miejsce :-)
Jeśli codebase jest spory to "abstrakcja na dane" też może być sama w sobie spora.

Niby tak, ale dla mnie fragment Codebase jest spory nie chciałem w każdej klasie "wstrzykiwać" 2 klientów do bazy i powtarzać oznacza raczej, że każda klasa sobie sama wali z/do bazy, zamiast korzystać z jakiegoś pośrednika - czy to byłby ORM, czy jakieś autorskie rozwiązanie.

Pytanie do @lukascode - jak wygląda sytuacja? Jest jakaś abstrakcja/middleware, czy w 200 miejscach 40 klas gada bezpośrednio z bazą?

1

@cerrato Klasy albo wstrzykują RedisClient albo coś w stylu RedisReactiveCommands<String, ?> redis.
Ta aplikacja nie ma jako takiej domeny, po prostu czyta wiadomości z bazy i przepycha dalej websocketami. Jest wiele klas,
które w nazwie mają Redis, Stream, Reader, Bucket, etc i one pod spodem tego używają.

Czasem beany są tworzone w taki sposób:

@Singleton
public SomeBean someBean(RedisClient redisClient) {
  StatefulRedisConnection<String, String> connection = redisClient.connect();
  Mono.fromCallable(() -> connection.sync().clientSetname(name))
                .subscribeOn(Schedulers.boundedElastic())
                .subscribe();
  var commands = connection.reactive();
  return new SomeBean(commands);
}

Zastanawiałem się dlaczego tak, dlaczego nie ma jakiejś puli połączeń gdzie to by było całkiem ukryte abstrakcją. Wtedy by było łatwiej.

1

Tak źle opisane że ciężko pomóc.

Przedstaw:

  • Definicję beana dla RedisClient (wszystkie, ewentualnie miejsca gdzie jest tworzone ręcznie)
  • Dokładne błędy ze Springa zamiast context się nie propaguje

Podejście w dobrym kierunku: to jest abstrakcja która opakowuje daną bazę. Repozytorium było by tutaj lepszym wzrocem ale bardziej kosztownym i trzeba by sie zastanowić czy to repozytorium nie byłoby po prostu przepisaniem RedisClient'a.

Zwykle jak jest repo to migrację robi się "pod spodem" to jest na repo wołasz metody do aktualnej bazy danych typu get, save a na repo jest wrapper MigrationRepositoryDecorator który "przepompowuje dane do nowej bazy".

Tak czy siak, rozważcie też inne rozwiązanie: napisać toola który przepompuje dane do nowej bazy. Puścić toola niech chodzi w tle, po timestamp'ach. W pewnym momencie zatrzymać apkę na np. 10 min. Puścić toola niech dokończy migrację. Uruchomić apkę z nowym konfigiem i zweryfikować czy działa, jak nie powrót do starej DB, jak działa to działa. Ew. można też próbować migrować backup'em.

PS. micronaut + mono/flux + websocket's - architekt miał wyobraźnię... mono/flux jest już defacto przestarzałe (virtual threads), micronaut się nie przyjął choć był hype 2 lata temu, websockets - jak trzeba to trzeba, ale ja bym cisnął na REST. Niemniej czego się nauczysz to twoje 👍

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.