Redis faqat Cache emas - 6 ta muammo, bitta yechim

Suhbatlarda "Redis nima?" deb so'rashsa, 90% holatda bitta javobni eshitamiz:

"Cache qilish uchun, aka."

Bu to'g'ri. Lekin Redis'ni faqat cache sifatida ishlatish oxirgi iPhone sotib olib, undan faqat kalkulyator sifatida foydalanishdek gap.

Bugun Redis'ning haqiqiy qudratini ko'ramiz. Backend arxitekturasida u bitta o'zi qanday 6 ta jiddiy muammoni yechadi.


Redis nima keshdan tashqari

In-Memory Data Structure Server xotirada (RAM) ishlaydigan ma'lumotlar strukturalari serveri.

Nega u bunday tez?

ByteByteGo — Nega Redis bu qadar tez?

3 ta asosiy sabab:

  1. RAM asosida disk o'qishdan 1000x tezroq
  2. Single-threaded lock va context switch muammolari yo'q, qator ketma-ket bajariladi
  3. I/O Multiplexing bitta thread minglab ulanishni boshqaradi

Eng muhimi Redis ichida faqat oddiy String key -> value emas, quyidagi tayyor ma'lumotlar strukturalari bor:

Struktura Ichki ishlashi Asosiy buyruqlar
String SDS (Simple Dynamic String) SET, GET, INCR
List Quicklist (linked list + ziplist) LPUSH, RPOP, BRPOP
Hash Listpack yoki Hash Table HSET, HGET, HGETALL
Set Hash Table SADD, SMEMBERS, SINTER
Sorted Set Skip List + Hash Table ZADD, ZRANGE, ZREVRANK

Aynan mana shu strukturalar unga cache'dan tashqari yana 5 ta super qudrat beradi.


Message Queue navbat tizimi

Foydalanuvchi "Hisobot tayyorla" tugmasini bosdi. Hisobot 20 sekund vaqt oladi. HTTP request'ni 20 sekund ushlab turish bu daxshat. Foydalanuvchi brauzerini yopib ketadi.

Bunga yechim vazifani navbatga tashlab, orqa fonda (background worker) bajarish.

Ko'pchilik buning uchun RabbitMQ o'rnatadi. Lekin tizim katta bo'lmasa, Redis buni muammosiz eplaydi.

Qanday ishlaydi? Redis'ning List strukturasi xuddi navbat (FIFO queue) kabi ishlaydi:

Producer  --[LPUSH]--> [task3, task2, task1] --[RPOP/BRPOP]--> Consumer
  • LPUSH chap (bosh) tomonga element qo'shadi
  • RPOP o'ng (dum) tomongidan oladi
  • BRPOP blocking POP: navbat bo'sh bo'lsa, yangi element kelguncha kutadi (polling yo'q!)

Java misoli (Spring Boot + Jedis):

// Producer vazifani navbatga qo'shish
@Service
public class ReportProducer {
    private final JedisPool jedisPool;

    public void scheduleReport(String reportData) {
        try (Jedis jedis = jedisPool.getResource()) {
            jedis.lpush("report:queue", reportData);
            log.info("Vazifa navbatga qo'shildi");
        }
    }
}

// Consumer - orqa fonda ishlaydi
@Component
public class ReportConsumer {

    @Scheduled(fixedDelay = 100) // yoki alohida thread
    public void processQueue() {
        try (Jedis jedis = jedisPool.getResource()) {
            // 30 soniya kutadi, yangi element kelguncha
            List<String> result = jedis.brpop(30, "report:queue");
            if (result != null) {
                String task = result.get(1);
                generateReport(task); // og'ir ish bu yerda
            }
        }
    }
}

Qachon Redis Queue yetarli emas?

Agar xabarlar saqlanib qolishi shart bo'lsa (worker o'chib qolsa ham yo'qolmasin) Redis Streams yoki to'laqonli RabbitMQ/Kafka kerak. Redis List'da BRPOP qilgan element qaytarib bo'lmaydi agar worker birinchi marta olib, ishlatmay o'chib qolsa, u yo'qoladi.


Distributed Lock

Omborda oxirgi 1 ta mahsulot bor. 3 ta foydalanuvchi bir vaqtda "Sotib ol" tugmasini bosdi. Uchala server ham bazadan quantity = 1 o'qidi. Hammasi ham sotishga ruxsat berdi.

Java'dagi synchronized bloki yordamimi? Yo'q! U faqat bitta JVM ichida ishlaydi. 3 ta alohida server bo'lsa, u ishlamaydi.

Yechim: Redis'da global qulf barcha serverlar ko'radigan yagona egalik belgisi.

Mexanizm — SET NX EX:

SET lock:product_99 <uuid> NX EX 10
  • NX faqat kalit yo'q bo'lsa qo'y (Set if Not eXists)
  • EX 10 10 soniyadan keyin avtomatik o'chir (dead lock oldini oladi)
  • <uuid> kimning qulflagan, boshqa server uchun emas!

Java misoli (Redisson tavsiya etilgan kutubxona):

@Service
public class ProductService {
    private final RedissonClient redisson;

    public boolean purchaseLastProduct(Long productId) {
        // Redisson lock UUID + TTL avtomatik
        RLock lock = redisson.getLock("lock:product:" + productId);

        try {
            // 3 soniya kutib ko'r, 10 soniyada avtomatik o'chir
            boolean acquired = lock.tryLock(3, 10, TimeUnit.SECONDS);

            if (!acquired) {
                throw new RuntimeException("Mahsulot boshqasi tomonidan band!");
            }

            // bazadan o'q, tekshir, yoz
            Product product = productRepository.findById(productId);
            if (product.getQuantity() < 1) {
                throw new RuntimeException("Mahsulot tugagan!");
            }

            product.setQuantity(product.getQuantity() - 1);
            productRepository.save(product);
            return true;

        } finally {
            // faqat o'z lockini o'chir!
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

Nega UUID muhim? Agar A serverning lock muddati tugasa va B server uni olsa, keyin A o'z ishini bitirib lockni o'chirmoqchi bo'lsa B'ning lockini o'chirib yuboradi! UUID bilan bu muammo yo'q u o'z nishonimgina o'chiraman.

O'tgan safargi "Username band" maqolasida ham aynan shu mavzuni chuqurroq ko'rib chiqqandik.


Rate Limiter

Bitta foydalanuvchi 1 minutda 10 000 ta so'rov yubordi. Tizim ishdan chiqdi.

Buni "1 minutda ko'pi bilan 60 ta so'rov" qoidasi Redis INCR + EXPIRE bilan hal qilsak bo'ladi.

Mexanizm:

1. Request keldi: user_id = 42
2. Redis'da kalit: "rate:user:42" ni INCR qil
3. Agar birinchi so'rov bo'lsa (INCR natijasi = 1): EXPIRE 60 qo'y
4. Agar qiymat > 60: HTTP 429 Too Many Requests qaytardir

Java misoli (Spring + Redis):

@Component
public class RateLimiterService {
    private final StringRedisTemplate redis;
    private static final int MAX_REQUESTS = 60;
    private static final int WINDOW_SECONDS = 60;

    public boolean isAllowed(String userId) {
        String key = "rate:" + userId;

        // Atomik operatsiya increment + get
        Long count = redis.opsForValue().increment(key);

        if (count == 1) {
            // Birinchi so'rov 60 soniyalik oyna boshlaydi
            redis.expire(key, Duration.ofSeconds(WINDOW_SECONDS));
        }

        return count <= MAX_REQUESTS;
    }
}

// Spring MVC interceptor'da ishlatish
@Component
public class RateLimitInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, 
                              HttpServletResponse response, 
                              Object handler) throws Exception {
        String userId = getUserId(request); // JWT'dan olinadi

        if (!rateLimiter.isAllowed(userId)) {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.getWriter().write("{\"error\": \"Rate limit exceeded\"}");
            return false;
        }

        return true;
    }
}

Ilg'or usul Sliding Window:

Yuqoridagi Fixed Window'da muammo bor: 0:59 da 60 ta, 1:00 da yana 60 ta = 2 daqiqada 120 ta so'rov. Buning uchun Redis Sorted Set ishlatiladi:

public boolean isAllowedSlidingWindow(String userId) {
    long now = System.currentTimeMillis();
    long windowStart = now - 60_000; // 60 soniya oldin
    String key = "sliding:" + userId;

    ZSetOperations<String, String> zset = redis.opsForZSet();

    // Eski so'rovlarni o'chir
    zset.removeRangeByScore(key, 0, windowStart);

    // Joriy so'rov sonini ol
    Long count = zset.zCard(key);

    if (count != null && count >= MAX_REQUESTS) {
        return false;
    }

    // Yangi so'rovni qo'sh
    zset.add(key, UUID.randomUUID().toString(), now);
    redis.expire(key, Duration.ofSeconds(70)); // oynadan birozroq uzun

    return true;
}

Real-time Xabarlar

Chat ilovasi qurmoqchisiz. Bitta foydalanuvchi xabar yozdi boshqalarga real-time yetib borishi kerak.

Redis'da Pub/Sub xuddi radio-stansiya kabi.

[Abdukarim xabar yozadi] --PUBLISH--> Redis Channel: "chat:room:101"
                                                |
              SUBSCRIBE qilganlar: [Ali], [Vali], [Hamid]
                                    hammasi bir vaqtda oladi

Java misoli (Spring Data Redis):

// Publisher xabar yuboradi
@Service
public class ChatPublisher {
    private final StringRedisTemplate redis;

    public void sendMessage(String roomId, String message) {
        redis.convertAndSend("chat:room:" + roomId, message);
    }
}

// Subscriber xabarlarni qabul qiladi
@Component
public class ChatSubscriber implements MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {
        String chatMessage = new String(message.getBody());
        // WebSocket orqali frontendga yuborish
        webSocketSession.sendMessage(new TextMessage(chatMessage));
    }
}

// Configuration Spring Boot'da
@Configuration
public class RedisConfig {

    @Bean
    public RedisMessageListenerContainer container(
            RedisConnectionFactory factory, ChatSubscriber subscriber) {

        RedisMessageListenerContainer container = 
            new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        container.addMessageListener(subscriber, 
            new PatternTopic("chat:room:*")); // barcha xonalar
        return container;
    }
}

Muhim ogohlantirish:

Redis Pub/Sub "Fire and Forget" tamoyilida ishlaydi. Agar subscriber o'chiq bo'lsa xabar yo'qoladi, saqlanib qolmaydi.

  • Slack, Discord Redis Pub/Sub ishlatadilar (lekin yuqori yuklamada Kafka bilan birga)
  • Agar xabarlar saqlanib qolishi shart bo'lsa Redis Streams yoki Kafka

Session Management - Stateless Arxitektura

Foydalanuvchi Server A ga login qildi. Keyingi request Server B ga tushdi B foydalanuvchini tanimaydi.

ByteByteGo 6 ta eng muhim Redis qo'llanilishi

Buning yechimi barcha serverlar foydalanadigan yagona markazlashgan session ombori.

Oqim:

1. Foydalanuvchi login qiladi -> User Service
2. Session yaratiladi Redis'da: "session:abc123" = {user_id, roles, email}
3. "abc123" Cookie orqali brauzerga qaytariladi
4. Keyingi request: Cookie -> Har qaysi server Redis'dan session'ni tekshiradi

Spring Session + Redis (faqat 2 ta annotation!):

// pom.xml
// spring-session-data-redis dependency qo'shish

// Application.java
@EnableRedisHttpSession
@SpringBootApplication
public class Application { ... }

// application.yml
spring:
  session:
    store-type: redis
    timeout: 1800s  # 30 daqiqa
  redis:
    host: localhost
    port: 6379

Spring Session avtomatik ravishda HttpSession'ni Redis'ga yo'naltiradi. Hech qanday qo'shimcha kod shart emas

Nima saqlanadi Redis'da?

"spring:session:sessions:abc123" = {
  "sessionAttr:user_id": 27,
  "sessionAttr:email": "user@example.com",
  "sessionAttr:roles": ["ADMIN"],
  "lastAccessedTime": 1735555200000,
  "maxInactiveInterval": 1800
}

Leaderboard (Reyting tizimi)

Bu ko'pchilik bilmaydigan lekin juda kuchli qo'llanilish. Instagram'da "eng ko'p like olgan postlar" yoki o'yin ilovasida "top 10 o'yinchilar" ko'rsatish uchun Redis Sorted Set (ZSet) har element uchun score bor va u bo'yicha tartiblangan.

@Service
public class LeaderboardService {
    private final StringRedisTemplate redis;
    private static final String LEADERBOARD_KEY = "game:leaderboard";

    // Ball qo'shish yoki yangilash O(log n)
    public void updateScore(String playerId, double points) {
        redis.opsForZSet().incrementScore(LEADERBOARD_KEY, playerId, points);
    }

    // Top 10'ni olish O(log n + k)
    public List<PlayerScore> getTopPlayers(int count) {
        Set<ZSetOperations.TypedTuple<String>> topPlayers = 
            redis.opsForZSet()
                 .reverseRangeWithScores(LEADERBOARD_KEY, 0, count - 1);

        return topPlayers.stream()
            .map(entry -> new PlayerScore(
                entry.getValue(),    // playerId
                entry.getScore()     // ball
            ))
            .collect(Collectors.toList());
    }

    // Foydalanuvchining o'rnini bilish
    public Long getPlayerRank(String playerId) {
        // null bo'lmasa, o'rni 0-based qaytaradi
        Long rank = redis.opsForZSet()
            .reverseRank(LEADERBOARD_KEY, playerId);
        return rank != null ? rank + 1 : null; // 1-based qilish
    }
}

Bir millionlab o'yinchi bo'lsa ha ZREVRANGE O(log n + k) da ishlaydi PostgrSQL'da ORDER BY score DESC LIMIT 10 uchun full scan kerak bo'lishi mumkin.


Redis qanday mos keladi?

ByteByteGo Redis arxitekturasi qanday rivojlangan

Redis 2010 yildan beri oddiy keshdan boshlab to'laqonli klaster arxitekturasigacha o'sdi. Hozir Airbnb, Uber, Slack, Twitter kabi gigantlar ishlatadi va ularning har biri Redis'ni turli maqsadlarda ishlatadi.


Cheat Sheet Qaysi muammo uchun qaysi yechim?

Muammo Redis Strukturasi Asosiy buyruqlar Muqobil
Cache String, Hash SET nx ex, HSET Memcached
Message Queue List / Streams LPUSH, BRPOP, XADD RabbitMQ, Kafka
Distributed Lock String (atomic) SET key uuid NX EX Zookeeper
Pub/Sub Built-in channel PUBLISH, SUBSCRIBE Kafka, NATS
Rate Limiting String / Sorted Set INCR, EXPIRE, ZADD Nginx + Lua
Session Hash HSET, EXPIRE DB sessions
Leaderboard Sorted Set ZADD, ZREVRANGE SQL ORDER BY
Counter String INCR, INCRBY DB atomic update

Redis nima emas?

Redis faqat kesh emas. U ma'lumotlar strukturalari serveri:

  • Navbat kerakmi? -> Redis List
  • Global qulf kerakmi? -> Redis SETNX
  • Real-time xabarmi? -> Redis Pub/Sub
  • So'rovlarni cheklashmi? -> Redis INCR
  • Session boshqaruvimi? -> Redis + Spring Session
  • Reyting kerakmi? -> Redis Sorted Set

Keyingi safar intervyuda "Redis nima?" deb so'rashsa, endi javob tayyor "Redis In-Memory Data Structure Server bo'lib, u faqat kesh emas, balki distributed lock, message queue, rate limiter, pub/sub va session management muammolarini ham bitta instrument bilan yechadi."

Suhbat oluvchi ko'zlarida chaqmoq chaqnaydi ishoning :)

Sizga shu savol berib maqolamni yakunlayman
Redis single-threaded bo'lsa, bir vaqtda minglab so'rovni qanday boshqaradi va nima uchun bu arxitektura disk asosidagi bazalardan ko'ra ko'p hollarda tezroq ishlaydi?


Manbalar