Redis
 sql >> Base de données >  >> NoSQL >> Redis

Conception d'une application avec Redis comme magasin de données. Quelle? Pourquoi?

1) Présentation

Bonjour à tous! Beaucoup de gens savent ce qu'est Redis, et si vous ne le savez pas, le site officiel peut vous mettre à jour.
Pour la plupart, Redis est un cache et parfois une file d'attente de messages.
Mais que se passe-t-il si nous devenons un peu fous et essayons de concevoir une application entière en utilisant uniquement Redis comme stockage de données ? Quelles tâches pouvons-nous résoudre avec Redis ?
Nous allons essayer de répondre à ces questions, dans cet article.

Qu'est-ce que nous ne verrons pas ici ?

  • Chaque structure de données Redis en détail ne sera pas ici. À quelles fins devriez-vous lire des articles ou de la documentation spéciaux.
  • Il n'y aura pas non plus de code prêt pour la production que vous pourriez utiliser dans votre travail.

Que verrons-nous ici ?

  • Nous utiliserons diverses structures de données Redis pour implémenter différentes tâches d'une application de rencontres.
  • Voici des exemples de code Kotlin + Spring Boot.

2) Apprenez à créer et à interroger des profils d'utilisateurs.

  • Pour le premier, apprenons à créer des profils d'utilisateurs avec leurs noms, likes, etc.

    Pour ce faire, nous avons besoin d'un simple magasin clé-valeur. Comment faire?

  • Simplement. Un Redis a une structure de données - un hachage. Il s'agit essentiellement d'une carte de hachage familière pour nous tous.

Les commandes du langage de requête Redis peuvent être trouvées ici et ici.
La documentation a même une fenêtre interactive pour exécuter ces commandes directement sur la page. Et la liste complète des commandes peut être trouvée ici.
Des liens similaires fonctionnent pour toutes les commandes ultérieures que nous considérerons.

Dans le code, nous utilisons RedisTemplate presque partout. C'est une chose de base pour travailler avec Redis dans l'écosystème Spring.

La seule différence par rapport à la carte ici est que nous passons "champ" comme premier argument. Le « champ » est le nom de notre hachage.

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")
    }

Ci-dessus, un exemple de ce à quoi cela pourrait ressembler dans Kotlin en utilisant les bibliothèques de Spring.

Vous pouvez trouver tous les morceaux de code de cet article sur Github.

3) Mettre à jour les préférences des utilisateurs à l'aide des listes Redis.

  • Génial!. Nous avons des utilisateurs et des informations sur les goûts.

    Maintenant, nous devrions trouver un moyen de mettre à jour ce qui aime.

    Nous supposons que les événements peuvent se produire très souvent. Utilisons donc une approche asynchrone avec une file d'attente. Et nous lirons les informations de la file d'attente selon un calendrier.

  • Redis a une structure de données de liste avec un tel ensemble de commandes. Vous pouvez utiliser des listes Redis à la fois comme file d'attente FIFO et comme pile LIFO.

Au printemps, nous utilisons la même approche pour obtenir ListOperations à partir de RedisTemplate.

Nous devons écrire à droite. Car ici nous simulons une file d'attente FIFO de droite à gauche.

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)
}

Maintenant, nous allons exécuter notre travail dans les délais.

Nous transférons simplement des informations d'une structure de données Redis à une autre. Cela nous suffit comme exemple.

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

La mise à jour des utilisateurs est vraiment facile ici. Saluez HashOperation de la partie précédente.

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))
    }

Et maintenant, nous montrons comment obtenir des données à partir de la liste. Nous obtenons cela de la gauche. Pour obtenir un tas de données de la liste, nous utiliserons une range méthode.
Et il y a un point important. La méthode de plage obtiendra uniquement les données de la liste, mais ne les supprimera pas.

Nous devons donc utiliser une autre méthode pour supprimer les données. trim fais-le. (Et vous pouvez y poser des questions).

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)
}
}

Et les questions sont :

  • Comment obtenir des données de la liste dans plusieurs threads ?
  • Et comment s'assurer que les données ne seront pas perdues en cas d'erreur ? De la boîte - rien. Vous devez obtenir les données de la liste dans un thread. Et vous devez gérer vous-même toutes les nuances qui surviennent.

4) Envoi de notifications push aux utilisateurs utilisant pub/sub

  • Continuez d'avancer !
    Nous avons déjà des profils d'utilisateurs. Nous avons compris comment gérer le flux de likes de ces utilisateurs.

    Mais imaginez le cas où vous voudriez envoyer une notification push à un utilisateur dès que nous aurons un like.
    Qu'allez-vous faire ?

  • Nous avons déjà un processus asynchrone pour gérer les likes, alors construisons simplement l'envoi de notifications push. Nous utiliserons WebSocket à cette fin, bien sûr. Et nous pouvons simplement l'envoyer via WebSocket où nous obtenons un like. Mais que se passe-t-il si nous voulons exécuter du code de longue durée avant de l'envoyer ? Et si nous voulions déléguer le travail avec WebSocket à un autre composant ?
  • Nous reprendrons et transférerons à nouveau nos données d'une structure de données Redis (liste) à une autre (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))
    }
}

L'écouteur lié au sujet se trouve dans la configuration.
Maintenant, nous pouvons simplement amener notre auditeur dans un service séparé.

@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) Trouver les utilisateurs les plus proches grâce aux opérations géographiques.

  • Nous en avons fini avec les likes. Mais qu'en est-il de la possibilité de trouver les utilisateurs les plus proches d'un point donné ?

  • GeoOperations nous y aidera. Nous allons stocker les paires clé-valeur, mais maintenant notre valeur est la coordonnée de l'utilisateur. Pour trouver, nous utiliserons le [radius](https://redis.io/commands/georadius) méthode. Nous transmettons l'identifiant de l'utilisateur à rechercher et le rayon de recherche lui-même.

Redis renvoie le résultat, y compris notre identifiant utilisateur.

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) Mise à jour de la position des utilisateurs via des flux

  • Nous avons mis en œuvre presque tout ce dont nous avons besoin. Mais maintenant, nous avons à nouveau une situation où nous devons mettre à jour des données qui pourraient changer rapidement.

    Nous devons donc utiliser à nouveau une file d'attente, mais ce serait bien d'avoir quelque chose de plus évolutif.

  • Les flux Redis peuvent aider à résoudre ce problème.
  • Vous connaissez probablement Kafka et vous connaissez probablement même les flux Kafka, mais ce n'est pas la même chose que les flux Redis. Mais Kafka lui-même est une chose assez similaire aux flux Redis. Il s'agit également d'une structure de données log-ahead qui a un groupe de consommateurs et un décalage. Il s'agit d'une structure de données plus complexe, mais elle nous permet d'obtenir des données en parallèle et en utilisant une approche réactive.

Consultez la documentation du flux Redis pour plus de détails.

Spring a ReactiveRedisTemplate et RedisTemplate pour travailler avec les structures de données Redis. Il serait plus pratique pour nous d'utiliser RedisTemplate pour écrire la valeur et ReactiveRedisTemplate pour la lecture. Si nous parlons de flux. Mais dans de tels cas, rien ne fonctionnera.
Si quelqu'un sait pourquoi cela fonctionne de cette façon, à cause de Spring ou de Redis, écrivez dans les commentaires.

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

Notre méthode d'écoute ressemblera à ceci :

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

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

Nous déplaçons simplement nos données dans une structure de données géographiques.

7) Compter les sessions uniques à l'aide d'HyperLogLog.

  • Et enfin, imaginons que nous devions calculer le nombre d'utilisateurs qui ont accédé à l'application par jour.
  • De plus, gardons à l'esprit que nous pouvons avoir un grand nombre d'utilisateurs. Ainsi, une option simple utilisant une carte de hachage ne nous convient pas car elle consommera trop de mémoire. Comment pouvons-nous faire cela en utilisant moins de ressources ?
  • Une structure de données probabiliste HyperLogLog entre en jeu ici. Vous pouvez en savoir plus à ce sujet sur la page Wikipedia. Une caractéristique clé est que cette structure de données nous permet de résoudre le problème en utilisant beaucoup moins de mémoire que l'option avec une carte de hachage.


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) Conclusion

Dans cet article, nous avons examiné les différentes structures de données Redis. Y compris les opérations géographiques pas si populaires et HyperLogLog.
Nous les avons utilisés pour résoudre de vrais problèmes.

On a presque conçu Tinder, c'est possible dans FAANG après ça)))
Nous avons également mis en évidence les principales nuances et problèmes pouvant être rencontrés lors de l'utilisation de Redis.

Redis est un stockage de données très fonctionnel. Et si vous l'avez déjà dans votre infrastructure, cela peut valoir la peine de considérer Redis comme un outil pour résoudre vos autres tâches sans complications inutiles.

PS :
Tous les exemples de code peuvent être trouvés sur github.

Écrivez dans les commentaires si vous remarquez une erreur.
Laissez un commentaire ci-dessous sur une telle façon de décrire l'utilisation de certaines technologies. Aimez-vous ou pas ?

Et suivez-moi sur Twitter :🐦@de____ro