sql >> Databasteknik >  >> NoSQL >> Redis

Designa en applikation med Redis som datalager. Vad? Varför?

1) Introduktion

Hej alla! Många människor vet vad Redis är, och om du inte vet kan den officiella webbplatsen ge dig uppdaterad.
För de flesta är Redis en cache och ibland en meddelandekö.
Men vad händer om vi blir lite galna och försöker designa en hel applikation med endast Redis som datalagring? Vilka uppgifter kan vi lösa med Redis?
Vi kommer att försöka svara på dessa frågor i den här artikeln.

Vad kommer vi inte att se här?

  • Varje Redis-datastruktur i detalj kommer inte att finnas här. För vilka ändamål bör du läsa speciella artiklar eller dokumentation.
  • Här kommer inte heller att finnas någon produktionsklar kod som du kan använda i ditt arbete.

Vad kommer vi att se här?

  • Vi kommer att använda olika Redis-datastrukturer för att implementera olika uppgifter för en dejtingapplikation.
  • Här kommer Kotlin + Spring Boot-kodexempel.

2) Lär dig att skapa och fråga efter användarprofiler.

  • För det första, låt oss lära oss hur man skapar användarprofiler med deras namn, gillar osv.

    För att göra detta behöver vi en enkel nyckel-värde butik. Hur man gör det?

  • Helt enkelt. En Redis har en datastruktur - en hash. I grund och botten är detta bara en bekant hashkarta för oss alla.

Redis frågespråkkommandon finns här och här.
Dokumentationen har till och med ett interaktivt fönster för att utföra dessa kommandon direkt på sidan. Och hela kommandolistan finns här.
Liknande länkar fungerar för alla efterföljande kommandon som vi kommer att överväga.

I koden använder vi RedisTemplate nästan överallt. Detta är en grundläggande sak för att arbeta med Redis i vårens ekosystem.

Den enda skillnaden från kartan här är att vi passerar "fält" som första argument. "Fältet" är vår hashs namn.

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

Ovan är ett exempel på hur det kan se ut i Kotlin med Springs bibliotek.

Alla delar av kod från den artikeln kan du hitta på Github.

3) Uppdatera användarnas gillar med Redis-listor.

  • Bra!. Vi har användare och information om gilla-markeringar.

    Nu borde vi hitta ett sätt att uppdatera som gillar.

    Vi antar att händelser kan inträffa väldigt ofta. Så låt oss använda ett asynkront tillvägagångssätt med lite kö. Och vi kommer att läsa informationen från kön enligt ett schema.

  • Redis har en listdatastruktur med en sådan uppsättning kommandon. Du kan använda Redis-listor både som en FIFO-kö och som en LIFO-stack.

På våren använder vi samma metod för att hämta ListOperations från RedisTemplate.

Vi måste skriva till höger. För här simulerar vi en FIFO-kö från höger till vänster.

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 ska vi köra vårt jobb enligt schemat.

Vi överför helt enkelt information från en Redis-datastruktur till en annan. Detta räcker för oss som exempel.

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

Användaruppdatering är väldigt enkel här. Ge ett hej till HashOperation från föregående del.

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

Och nu visar vi hur man får data från listan. Det får vi från vänster. För att få en massa data från listan kommer vi att använda ett range metod.
Och det finns en viktig poäng. Områdesmetoden kommer bara att hämta data från listan, men inte ta bort den.

Så vi måste använda en annan metod för att radera data. trim gör det. (Och du kan ha några frågor där).

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

Och frågorna är:

  • Hur får man data från listan till flera trådar?
  • Och hur säkerställer man att data inte går förlorade vid fel? Från lådan - ingenting. Du måste hämta data från listan i en tråd. Och du måste hantera alla nyanser som uppstår på egen hand.

4) Skicka push-meddelanden till användare som använder pub/sub

  • Fortsätt att röra dig framåt!
    Vi har redan användarprofiler. Vi kom på hur vi skulle hantera strömmen av likes från dessa användare.

    Men föreställ dig fallet när du vill skicka ett push-meddelande till en användare när vi fick en like.
    Vad ska du göra?

  • Vi har redan en asynkron process för att hantera likes, så låt oss bara bygga in att skicka push-meddelanden där. Vi kommer naturligtvis att använda WebSocket för det ändamålet. Och vi kan bara skicka det via WebSocket där vi får en like. Men vad händer om vi vill köra långvarig kod innan vi skickar? Eller vad händer om vi vill delegera arbete med WebSocket till en annan komponent?
  • Vi kommer att ta och överföra vår data igen från en Redis-datastruktur (lista) till en annan (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))
    }
}

Lyssnaren som binder till ämnet finns i konfigurationen.
Nu kan vi bara ta vår lyssnare till en separat tjänst.

@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) Hitta närmaste användare genom geooperationer.

  • Vi är klara med gilla-markeringar. Men hur är det med möjligheten att hitta de användare som ligger närmast en viss punkt.

  • GeoOperations hjälper oss med detta. Vi kommer att lagra nyckel-värde-paren, men nu är vårt värde användarkoordinat. För att hitta använder vi [radius](https://redis.io/commands/georadius) metod. Vi skickar användar-id för att hitta och själva sökradien.

Redis returnerar resultat inklusive vårt användar-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) Uppdatera användarnas plats genom strömmar

  • Vi implementerade nästan allt vi behöver. Men nu har vi återigen en situation då vi måste uppdatera data som kan ändras snabbt.

    Så vi måste använda en kö igen, men det skulle vara trevligt med något mer skalbart.

  • Redis-strömmar kan hjälpa till att lösa det här problemet.
  • Du känner förmodligen till Kafka och förmodligen känner du till Kafka-strömmar, men det är inte samma sak som Redis-strömmar. Men Kafka i sig är en ganska liknande sak som Redis-strömmar. Det är också en log-ahead-datastruktur som har konsumentgrupp och offset. Detta är en mer komplex datastruktur, men den tillåter oss att få data parallellt och med ett reaktivt tillvägagångssätt.

Se Redis stream-dokumentation för mer information.

Spring har ReactiveRedisTemplate och RedisTemplate för att arbeta med Redis datastrukturer. Det skulle vara bekvämare för oss att använda RedisTemplate för att skriva värdet och ReactiveRedisTemplate för läsning. Om vi ​​pratar om strömmar. Men i sådana fall kommer ingenting att fungera.
Om någon vet varför det fungerar så här, på grund av Spring eller Redis, skriv i kommentarerna.

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

Vår lyssnarmetod kommer att se ut så här:

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

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

Vi flyttar bara vår data till en geodatastruktur.

7) Räkna unika sessioner med HyperLogLog.

  • Och slutligen, låt oss föreställa oss att vi behöver beräkna hur många användare som har angett applikationen per dag.
  • Låt oss dessutom komma ihåg att vi kan ha många användare. Så ett enkelt alternativ med en hashkarta är inte lämpligt för oss eftersom det kommer att förbruka för mycket minne. Hur kan vi göra detta med färre resurser?
  • En probabilistisk datastruktur HyperLogLog spelar in där. Du kan läsa mer om det på Wikipedia-sidan. En nyckelfunktion är att denna datastruktur tillåter oss att lösa problemet med betydligt mindre minne än alternativet med en hashkarta.


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

I den här artikeln tittade vi på de olika Redis-datastrukturerna. Inklusive inte så populära geooperationer och HyperLogLog.
Vi använde dem för att lösa verkliga problem.

Vi designade nästan Tinder, det är möjligt i FAANG efter detta)))
Vi lyfte också fram de viktigaste nyanserna och problemen som kan uppstå när man arbetar med Redis.

Redis är en mycket funktionell datalagring. Och om du redan har det i din infrastruktur kan det vara värt att titta på Redis som ett verktyg för att lösa dina andra uppgifter med det utan onödiga komplikationer.

PS:
Alla kodexempel finns på github.

Skriv i kommentarerna om du märker ett misstag.
Lämna en kommentar nedan om ett sådant sätt att beskriva användning av viss teknik. Gillar du det eller inte?

Och följ mig på Twitter:🐦@de____ro


  1. Fyll en mangustmodell med ett fält som inte är ett id

  2. hur man grupperar i mongoDB och returnerar alla fält i resultat

  3. Är det möjligt att ha en Linux VFS-cache med ett FUSE-filsystem?

  4. Meteor.js distribueras till example.com eller www.example.com?