Redis
 sql >> Teknologi Basis Data >  >> NoSQL >> Redis

Merancang aplikasi dengan Redis sebagai penyimpan data. Apa? Mengapa?

1) Pendahuluan

Halo semuanya! Banyak orang tahu apa itu Redis, dan jika Anda tidak tahu, situs resminya dapat memberi Anda informasi terbaru.
Untuk sebagian besar Redis adalah cache dan terkadang antrian pesan.
Tetapi bagaimana jika kita menjadi sedikit gila dan mencoba mendesain seluruh aplikasi hanya dengan menggunakan Redis sebagai penyimpanan data? Tugas apa yang bisa kita selesaikan dengan Redis?
Kami akan mencoba menjawab pertanyaan tersebut, dalam artikel ini.

Apa yang tidak akan kita lihat di sini?

  • Setiap detail struktur data Redis tidak akan ada di sini. Untuk tujuan apa Anda harus membaca artikel atau dokumentasi khusus.
  • Di sini juga tidak akan ada kode siap produksi yang dapat Anda gunakan dalam pekerjaan Anda.

Apa yang akan kita lihat di sini?

  • Kami akan menggunakan berbagai struktur data Redis untuk mengimplementasikan berbagai tugas aplikasi kencan.
  • Berikut adalah contoh kode Kotlin + Spring Boot.

2) Belajar membuat dan menanyakan profil pengguna.

  • Untuk yang pertama, mari kita pelajari cara membuat profil pengguna dengan nama, suka, dll.

    Untuk melakukan ini, kita memerlukan penyimpanan nilai kunci sederhana. Bagaimana cara melakukannya?

  • Sederhana. Redis memiliki struktur data - hash. Intinya, ini hanyalah peta hash yang familiar bagi kita semua.

Perintah bahasa kueri redis dapat ditemukan di sini dan di sini.
Dokumentasi bahkan memiliki jendela interaktif untuk menjalankan perintah ini langsung di halaman. Dan seluruh daftar perintah dapat ditemukan di sini.
Tautan serupa berfungsi untuk semua perintah berikutnya yang akan kami pertimbangkan.

Dalam kode, kami menggunakan RedisTemplate hampir di semua tempat. Ini adalah hal dasar untuk bekerja dengan Redis di ekosistem Spring.

Satu-satunya perbedaan dari peta di sini adalah kita melewati "bidang" sebagai argumen pertama. The "field" adalah nama hash kami.

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

Di atas adalah contoh tampilannya di Kotlin menggunakan library Spring.

Semua potongan kode dari artikel tersebut dapat Anda temukan di Github.

3) Memperbarui suka pengguna menggunakan daftar Redis.

  • Besar!. Kami memiliki pengguna dan informasi tentang suka.

    Sekarang kita harus menemukan cara bagaimana memperbarui suka itu.

    Kami berasumsi bahwa peristiwa dapat terjadi sangat sering. Jadi mari kita gunakan pendekatan asinkron dengan beberapa antrian. Dan kami akan membaca informasi dari antrian sesuai jadwal.

  • Redis memiliki struktur data daftar dengan serangkaian perintah seperti itu. Anda dapat menggunakan daftar Redis baik sebagai antrian FIFO dan sebagai tumpukan LIFO.

Di Musim Semi, kami menggunakan pendekatan yang sama untuk mendapatkan ListOperations dari RedisTemplate.

Kita harus menulis ke kanan. Karena disini kita mensimulasikan antrian FIFO dari kanan ke kiri.

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

Sekarang kita akan menjalankan pekerjaan kita sesuai jadwal.

Kami hanya mentransfer informasi dari satu struktur data Redis ke struktur data Redis lainnya. Ini cukup bagi kita sebagai contoh.

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

Pembaruan pengguna sangat mudah di sini. Berikan salam untuk HashOperation dari bagian sebelumnya.

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

Dan sekarang kami menunjukkan cara mendapatkan data dari daftar. Kami mendapatkan itu dari kiri. Untuk mendapatkan banyak data dari daftar, kita akan menggunakan range metode.
Dan ada poin penting. Metode rentang hanya akan mendapatkan data dari daftar, tetapi tidak menghapusnya.

Jadi kita harus menggunakan metode lain untuk menghapus data. trim lakukan. (Dan Anda dapat memiliki beberapa pertanyaan di sana).

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

Dan pertanyaannya adalah:

  • Bagaimana cara mendapatkan data dari daftar menjadi beberapa thread?
  • Dan bagaimana memastikan data tidak hilang jika terjadi kesalahan? Dari kotak - tidak ada. Anda harus mendapatkan data dari daftar dalam satu utas. Dan Anda harus menangani semua nuansa yang muncul sendiri.

4) Mengirim pemberitahuan push ke pengguna menggunakan pub/sub

  • Terus melangkah!
    Kami sudah memiliki profil pengguna. Kami menemukan cara menangani aliran suka dari pengguna ini.

    Tapi bayangkan kasusnya ketika Anda ingin mengirim pemberitahuan push ke pengguna saat kami mendapat suka.
    Apa yang akan kamu lakukan?

  • Kami sudah memiliki proses asinkron untuk menangani suka, jadi mari kita buat pengiriman pemberitahuan push ke sana. Kami akan menggunakan WebSocket untuk tujuan itu, tentu saja. Dan kami hanya dapat mengirimkannya melalui WebSocket di mana kami mendapatkan suka. Tetapi bagaimana jika kita ingin mengeksekusi kode yang sudah berjalan lama sebelum mengirim? Atau bagaimana jika kita ingin mendelegasikan pekerjaan dengan WebSocket ke komponen lain?
  • Kami akan mengambil dan mentransfer data kami lagi dari satu struktur data (daftar) Redis ke yang lain (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))
    }
}

Pendengar yang mengikat topik terletak di konfigurasi.
Sekarang, kita bisa membawa pendengar kita ke layanan terpisah.

@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) Menemukan pengguna terdekat melalui operasi geografis.

  • Kami sudah selesai dengan suka. Tapi bagaimana dengan kemampuan untuk menemukan pengguna terdekat ke titik tertentu.

  • GeoOperations akan membantu kami dalam hal ini. Kami akan menyimpan pasangan nilai kunci, tetapi sekarang nilai kami adalah koordinat pengguna. Untuk menemukan kita akan menggunakan [radius](https://redis.io/commands/georadius) metode. Kami meneruskan id pengguna untuk menemukan dan radius pencarian itu sendiri.

Mengembalikan hasil ulang termasuk id pengguna kami.

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) Memperbarui lokasi pengguna melalui aliran

  • Kami menerapkan hampir semua yang kami butuhkan. Tapi sekarang kita menghadapi situasi di mana kita harus memperbarui data yang bisa diubah dengan cepat.

    Jadi kita harus menggunakan antrian lagi, tapi alangkah baiknya jika ada sesuatu yang lebih terukur.

  • Streaming Redis dapat membantu memecahkan masalah ini.
  • Mungkin Anda tahu tentang Kafka dan mungkin Anda bahkan tahu tentang aliran Kafka, tetapi itu tidak sama dengan aliran Redis. Tapi Kafka sendiri adalah hal yang sangat mirip dengan aliran Redis. Ini juga merupakan struktur data log ke depan yang memiliki grup konsumen dan offset. Ini adalah struktur data yang lebih kompleks, tetapi memungkinkan kita untuk mendapatkan data secara paralel dan menggunakan pendekatan reaktif.

Lihat dokumentasi aliran Redis untuk detailnya.

Spring memiliki ReactiveRedisTemplate dan RedisTemplate untuk bekerja dengan struktur data Redis. Akan lebih mudah bagi kita untuk menggunakan RedisTemplate untuk menulis nilai dan ReactiveRedisTemplate untuk membaca. Jika kita berbicara tentang aliran. Tetapi dalam kasus seperti itu, tidak ada yang akan berhasil.
Jika seseorang tahu mengapa ini bekerja dengan cara ini, karena Spring atau Redis, tulis di komentar.

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

Metode pendengar kami akan terlihat seperti ini:

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

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

Kami hanya memindahkan data kami ke dalam struktur data geografis.

7) Hitung sesi unik menggunakan HyperLogLog.

  • Dan terakhir, bayangkan kita perlu menghitung berapa banyak pengguna yang masuk ke aplikasi per hari.
  • Selain itu, perlu diingat bahwa kita dapat memiliki banyak pengguna. Jadi, opsi sederhana menggunakan peta hash tidak cocok untuk kami karena akan menghabiskan terlalu banyak memori. Bagaimana kita bisa melakukannya dengan menggunakan lebih sedikit sumber daya?
  • Struktur data probabilistik HyperLogLog berperan di sana. Anda dapat membaca lebih lanjut tentang itu di halaman Wikipedia. Fitur utamanya adalah struktur data ini memungkinkan kita untuk memecahkan masalah menggunakan memori yang jauh lebih sedikit daripada opsi dengan peta hash.


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

Dalam artikel ini, kita melihat berbagai struktur data Redis. Termasuk operasi geo yang tidak begitu populer dan HyperLogLog.
Kami menggunakannya untuk memecahkan masalah nyata.

Kami hampir mendesain Tinder, mungkin di FAANG setelah ini)))
Selain itu, kami menyoroti nuansa dan masalah utama yang dapat dihadapi saat bekerja dengan Redis.

Redis adalah penyimpanan data yang sangat fungsional. Dan jika Anda sudah memilikinya di infrastruktur Anda, ada baiknya melihat Redis sebagai alat untuk menyelesaikan tugas Anda yang lain tanpa komplikasi yang tidak perlu.

PS:
Semua contoh kode dapat ditemukan di github.

Tulis di komentar jika Anda melihat kesalahan.
Tinggalkan komentar di bawah tentang cara untuk menggambarkan menggunakan beberapa teknologi. Suka atau tidak?

Dan ikuti saya di Twitter:🐦@de____ro


  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Pencari kecocokan multi-parameter dengan Redis

  2. dapatkan tanggal dan waktu saat ini di lua di redis

  3. Tambahkan Kedaluwarsa ke Redis Cache

  4. Koneksi redis hilang dari acara dekat

  5. Ruby - Mutex berbasis redis dengan implementasi kedaluwarsa