sql >> Database >  >> NoSQL >> Redis

Een applicatie ontwerpen met Redis als datastore. Wat? Waarom?

1) Inleiding

Dag iedereen! Veel mensen weten wat Redis is, en als je het niet weet, kan de officiële site je op de hoogte houden.
Voor de meeste Redis is een cache en soms een berichtenwachtrij.
Maar wat als we een beetje gek worden en een hele applicatie proberen te ontwerpen met alleen Redis als gegevensopslag? Welke taken kunnen we met Redis oplossen?
We zullen proberen deze vragen in dit artikel te beantwoorden.

Wat zien we hier niet?

  • Elke Redis-gegevensstructuur in detail zal hier niet zijn. Voor welke doeleinden u speciale artikelen of documentatie moet lezen.
  • Hier zal ook geen productieklare code zijn die u in uw werk zou kunnen gebruiken.

Wat gaan we hier zien?

  • We zullen verschillende Redis-gegevensstructuren gebruiken om verschillende taken van een datingtoepassing te implementeren.
  • Hier zijn voorbeelden van Kotlin + Spring Boot-codes.

2) Leer gebruikersprofielen te maken en op te vragen.

  • Laten we voor de eerste keer leren hoe we gebruikersprofielen kunnen maken met hun namen, vind-ik-leuks, enz.

    Om dit te doen, hebben we een eenvoudige key-value store nodig. Hoe je dat doet?

  • Gewoon. Een Redis heeft een datastructuur - een hash. In wezen is dit gewoon een bekende hash-kaart voor ons allemaal.

Redis-querytaalopdrachten zijn hier en hier te vinden.
De documentatie heeft zelfs een interactief venster om deze opdrachten direct op de pagina uit te voeren. En de hele lijst met opdrachten is hier te vinden.
Vergelijkbare links werken voor alle volgende commando's die we zullen overwegen.

In de code gebruiken we RedisTemplate bijna overal. Dit is een basisvoorwaarde voor het werken met Redis in het Spring-ecosysteem.

Het enige verschil met de kaart hier is dat we "field" als eerste argument doorgeven. Het "veld" is de naam van onze hash.

fun addUser(user: User) {
        val hashOps: HashOperations<String, String, User> = userRedisTemplate.opsForHash()
        hashOps.put(Constants.USERS, user.name, user)
    }

fun getUser(userId: String): User {
        val userOps: HashOperations<String, String, User> = userRedisTemplate.opsForHash()
        return userOps.get(Constants.USERS, userId)?: throw NotFoundException("Not found user by $userId")
    }

Hierboven ziet u een voorbeeld van hoe het eruit zou kunnen zien in Kotlin met behulp van Spring's bibliotheken.

Alle stukjes code uit dat artikel kun je vinden op Github.

3) Gebruikers-likes bijwerken met Redis-lijsten.

  • Super goed!. We hebben gebruikers en informatie over vind-ik-leuks.

    Nu moeten we een manier vinden om die likes te updaten.

    We gaan ervan uit dat gebeurtenissen heel vaak kunnen voorkomen. Laten we dus een asynchrone benadering gebruiken met een wachtrij. En we zullen de informatie uit de wachtrij volgens een schema lezen.

  • Redis heeft een lijstgegevensstructuur met een dergelijke reeks opdrachten. U kunt Redis-lijsten zowel als FIFO-wachtrij als als LIFO-stack gebruiken.

In het voorjaar gebruiken we dezelfde aanpak om ListOperations van RedisTemplate te krijgen.

We moeten naar rechts schrijven. Omdat we hier een FIFO-wachtrij van rechts naar links simuleren.

fun putUserLike(userFrom: String, userTo: String, like: Boolean) {
        val userLike = UserLike(userFrom, userTo, like)
        val listOps: ListOperations<String, UserLike> = userLikeRedisTemplate.opsForList()
        listOps.rightPush(Constants.USER_LIKES, userLike)
}

Nu gaan we ons werk volgens schema uitvoeren.

We brengen eenvoudig informatie over van de ene Redis-gegevensstructuur naar de andere. Dit is voor ons als voorbeeld voldoende.

fun processUserLikes() {
        val userLikes = getUserLikesLast(USERS_BATCH_LIMIT).filter{ it.isLike}
        userLikes.forEach{updateUserLike(it)}
}

Het updaten van gebruikers is hier heel eenvoudig. Geef een hallo aan HashOperation uit het vorige deel.

private fun updateUserLike(userLike: UserLike) {
        val userOps: HashOperations<String, String, User> = userLikeRedisTemplate.opsForHash()
        val fromUser = userOps.get(Constants.USERS, userLike.fromUserId)?: throw UserNotFoundException(userLike.fromUserId)
        fromUser.fromLikes.add(userLike)
        val toUser = userOps.get(Constants.USERS, userLike.toUserId)?: throw UserNotFoundException(userLike.toUserId)
        toUser.fromLikes.add(userLike)

        userOps.putAll(Constants.USERS, mapOf(userLike.fromUserId to fromUser, userLike.toUserId to toUser))
    }

En nu laten we zien hoe u gegevens uit de lijst kunt halen. Dat krijgen we van links. Om een ​​heleboel gegevens uit de lijst te halen, gebruiken we een range methode.
En er is een belangrijk punt. De bereikmethode haalt alleen gegevens uit de lijst, maar verwijdert deze niet.

We moeten dus een andere methode gebruiken om gegevens te verwijderen. trim doe het. (En je kunt daar wat vragen hebben).

private fun getUserLikesLast(number: Long): List<UserLike> {
        val listOps: ListOperations<String, UserLike> = userLikeRedisTemplate.opsForList()
        return (listOps.range(Constants.USER_LIKES, 0, number)?:mutableListOf()).filterIsInstance(UserLike::class.java)
            .also{
listOps.trim(Constants.USER_LIKES, number, -1)
}
}

En de vragen zijn:

  • Hoe krijg ik gegevens uit de lijst in verschillende threads?
  • En hoe zorg je ervoor dat de gegevens niet verloren gaan in geval van fouten? Uit de doos - niets. U moet gegevens uit de lijst in één thread halen. En alle nuances die ontstaan, moet je zelf afhandelen.

4) Pushmeldingen verzenden naar gebruikers met pub/sub

  • Blijf vooruit gaan!
    We hebben al gebruikersprofielen. We hebben uitgezocht hoe we de stroom van vind-ik-leuks van deze gebruikers kunnen verwerken.

    Maar stel je het geval voor dat je een pushmelding naar een gebruiker wilt sturen op het moment dat we een like krijgen.
    Wat ga je doen?

  • We hebben al een asynchroon proces voor het afhandelen van vind-ik-leuks, dus laten we daar het verzenden van pushmeldingen in bouwen. We zullen natuurlijk WebSocket voor dat doel gebruiken. En we kunnen het gewoon via WebSocket sturen waar we een like krijgen. Maar wat als we langlopende code willen uitvoeren voordat we ze verzenden? Of wat als we het werk met WebSocket willen delegeren aan een ander onderdeel?
  • We zullen onze gegevens opnieuw nemen en overdragen van de ene Redis-gegevensstructuur (lijst) naar de andere (pub/sub).
fun processUserLikes() {
        val userLikes = getUserLikesLast(USERS_BATCH_LIMIT).filter{ it.isLike}
                pushLikesToUsers(userLikes)
        userLikes.forEach{updateUserLike(it)}
}

private fun pushLikesToUsers(userLikes: List<UserLike>) {
  GlobalScope.launch(Dispatchers.IO){
        userLikes.forEach {
            pushProducer.publish(it)
        }
  }
}
@Component
class PushProducer(val redisTemplate: RedisTemplate<String, String>, val pushTopic: ChannelTopic, val objectMapper: ObjectMapper) {

    fun publish(userLike: UserLike) {
        redisTemplate.convertAndSend(pushTopic.topic, objectMapper.writeValueAsString(userLike))
    }
}

De listenerbinding aan het onderwerp bevindt zich in de configuratie.
Nu kunnen we onze luisteraar gewoon meenemen naar een aparte dienst.

@Component
class PushListener(val objectMapper: ObjectMapper): MessageListener {
    private val log = KotlinLogging.logger {}

    override fun onMessage(userLikeMessage: Message, pattern: ByteArray?) {
        // websocket functionality would be here
        log.info("Received: ${objectMapper.readValue(userLikeMessage.body, UserLike::class.java)}")
    }
}

5) De dichtstbijzijnde gebruikers vinden via geografische operaties.

  • We zijn klaar met likes. Maar hoe zit het met de mogelijkheid om de dichtstbijzijnde gebruikers op een bepaald punt te vinden.

  • GeoOperations helpt ons hierbij. We zullen de sleutel-waardeparen opslaan, maar nu is onze waarde gebruikerscoördinaat. Om te vinden gebruiken we de [radius](https://redis.io/commands/georadius) methode. We geven de te vinden gebruikers-ID en de zoekradius zelf door.

Redis retourneert resultaat inclusief onze gebruikers-ID.

fun getNearUserIds(userId: String, distance: Double = 1000.0): List<String> {
    val geoOps: GeoOperations<String, String> = stringRedisTemplate.opsForGeo()
    return geoOps.radius(USER_GEO_POINT, userId, Distance(distance, RedisGeoCommands.DistanceUnit.KILOMETERS))
        ?.content?.map{ it.content.name}?.filter{ it!= userId}?:listOf()
}

6) De locatie van gebruikers bijwerken via streams

  • We hebben bijna alles geïmplementeerd wat we nodig hadden. Maar nu hebben we weer een situatie waarin we gegevens moeten bijwerken die snel kunnen wijzigen.

    We moeten dus weer een wachtrij gebruiken, maar het zou leuk zijn om iets meer schaalbaars te hebben.

  • Redis-streams kunnen dit probleem helpen oplossen.
  • Waarschijnlijk ken je Kafka en waarschijnlijk ook Kafka-streams, maar het is niet hetzelfde als Redis-streams. Maar Kafka zelf is vrij gelijkaardig als Redis-streams. Het is ook een log-ahead datastructuur met consumentengroep en offset. Dit is een complexere gegevensstructuur, maar het stelt ons in staat om gegevens parallel te krijgen en een reactieve benadering te gebruiken.

Zie de Redis-streamdocumentatie voor details.

Spring heeft ReactiveRedisTemplate en RedisTemplate voor het werken met Redis-gegevensstructuren. Het zou voor ons handiger zijn om RedisTemplate te gebruiken om de waarde te schrijven en ReactiveRedisTemplate om te lezen. Als we het over streams hebben. Maar in dergelijke gevallen zal niets werken.
Als iemand weet waarom het zo werkt, vanwege Spring of Redis, schrijf dan in de comments.

fun publishUserPoint(userPoint: UserPoint) {
    val userPointRecord = ObjectRecord.create(USER_GEO_STREAM_NAME, userPoint)
    reactiveRedisTemplate
        .opsForStream<String, Any>()
        .add(userPointRecord)
        .subscribe{println("Send RecordId: $it")}
}

Onze luisteraarmethode ziet er als volgt uit:

@Service
class UserPointsConsumer(
    private val userGeoService: UserGeoService
): StreamListener<String, ObjectRecord<String, UserPoint>> {

    override fun onMessage(record: ObjectRecord<String, UserPoint>) {
        userGeoService.addUserPoint(record.value)
    }
}

We verplaatsen onze gegevens gewoon naar een geogegevensstructuur.

7) Tel unieke sessies met HyperLogLog.

  • En tenslotte, laten we ons voorstellen dat we moeten berekenen hoeveel gebruikers de applicatie per dag hebben ingevoerd.
  • Laten we er bovendien rekening mee houden dat we veel gebruikers kunnen hebben. Een eenvoudige optie met een hash-kaart is dus niet geschikt voor ons omdat het te veel geheugen in beslag neemt. Hoe kunnen we dit doen met minder middelen?
  • Daar komt een probabilistische datastructuur HyperLogLog om de hoek kijken. Op de Wikipedia-pagina kun je er meer over lezen. Een belangrijk kenmerk is dat deze gegevensstructuur ons in staat stelt het probleem op te lossen met aanzienlijk minder geheugen dan de optie met een hash-kaart.


fun uniqueActivitiesPerDay(): Long {
    val hyperLogLogOps: HyperLogLogOperations<String, String> = stringRedisTemplate.opsForHyperLogLog()
    return hyperLogLogOps.size(Constants.TODAY_ACTIVITIES)
}

fun userOpenApp(userId: String): Long {
    val hyperLogLogOps: HyperLogLogOperations<String, String> = stringRedisTemplate.opsForHyperLogLog()
    return hyperLogLogOps.add(Constants.TODAY_ACTIVITIES, userId)
}

8) Conclusie

In dit artikel hebben we gekeken naar de verschillende Redis-datastructuren. Inclusief niet zo populaire geografische operaties en HyperLogLog.
We gebruikten ze om echte problemen op te lossen.

We hebben Tinder bijna ontworpen, hierna is het mogelijk in FAANG)))
We hebben ook de belangrijkste nuances en problemen benadrukt die kunnen optreden bij het werken met Redis.

Redis is een zeer functionele gegevensopslag. En als je het al in je infrastructuur hebt, kan het de moeite waard zijn om Redis te zien als een tool om je andere taken daarmee op te lossen zonder onnodige complicaties.

PS:
Alle codevoorbeelden zijn te vinden op github.

Schrijf in de reacties als je een fout opmerkt.
Laat hieronder een opmerking achter over een dergelijke manier om te beschrijven met behulp van wat technologie. Vind je het leuk of niet?

En volg mij op Twitter:🐦@de____ro


  1. Celery gebruiken voor realtime, synchrone externe API-query's met Gevent

  2. MongoDB $toDecimal

  3. Hoe kan ik mijn /sidekiq-route met een wachtwoord beveiligen (d.w.z. authenticatie vereisen voor de Sidekiq::Web-tool)?

  4. Ondersteunt Spring Data Redis (1.3.2.RELEASE) JedisSentinelPool van jedis?