为什么说 Redis 是分布式系统的瑞士军刀

今天是一家名为 Hooli 的科技公司的黑客马拉松日。公司里的两名软件工程师想要创建一个小巧而精致的应用程序。
在这里插入图片描述

然而他们只熟悉少数工具——这其中包括 Redis。

他们决定尽可能地利用已掌握的技能,构建出一个原型出来。

注:本文涉及的代码只是为了展示Redis 使用场景的主要逻辑以帮助大家理解,因此没有考虑到必要的参数校验、异常处理、性能优化等方面。甚至,你可以将它当做伪代码~

Redis 使用场景

他们是这样开发这个应用程序的。

1.缓存

他们安装了一个Web服务器来处理API请求。但大多数情况下,同样的数据会频繁地从数据库中查询。所以他们在数据库前面设置了一个Redis缓存。

Redis是一种基于内存的k-v数据库,其QPS高达10W+,远远高于MySQL等关系型数据库。

在这里插入图片描述

Redis作为缓存,其工作流程如下:

  • 第一次访问数据时,会将从数据库查出来的数据缓存在 Redis中。
  • 后续访问同样数据的请求,都会查询Redis缓存,以检查数据是否存在
  • 如果数据位于Redis缓存中:直接从Redis缓存中拿到数据返回,这是缓存命中的情况。
  • 如果数据不在缓存中,继续查询数据库,拿到数据然后填充缓存并返回, 这是缓存未命中的情况。
    在这里插入图片描述

Redis缓存的引入为系统提供了:

  • 响应速度更快 - 100 倍
  • 减少数据库压力

Redis用作缓存的代码逻辑:

public class RedisCacheExample {
    private Jedis jedis;

    public RedisCacheExample() {
        this.jedis = new Jedis("localhost", 6379); // 连接到 Redis 服务器
    }

    public String getData(String key) {
        String cachedData = jedis.get(key);
        if (cachedData != null) {
            return cachedData; // 缓存命中
        } else {
            // 模拟从数据库中获取数据
            String dataFromDB = "data from database";
            jedis.set(key, dataFromDB);
            return dataFromDB; // 缓存未命中
        }
    }

    public static void main(String[] args) {
        RedisCacheExample cacheExample = new RedisCacheExample();
        String data = cacheExample.getData("some_key");
        System.out.println(data);
    }
}

2.排队

后来他们添加了额外的服务器来查询和处理数据,即查询服务器(下图中的query worker)

但有些查询需要很长的时间才能完成。为了应对这种情况,他们引入了 Redis流Redis流对请求进行排队。这使得他们可以异步处理请求。Redis流是一种数据结构,像附加日志一样工作。这意味着Redis流中每个元素都是不可变的,一旦数据被写入流,就不能被更改。
在这里插入图片描述

看看它是怎么运行的:

  1. 当请求的数据没有在缓存中找到时,请求会被转发到查询服务。
  2. 请求被包装在消息中并被分配一个唯一的标识符被添加到 Redis 流队列中。
  3. 如果查询服务有空闲资源,它就会从队列中取出消息进行处理。
  4. 查询服务器与数据库交互,获取所需的数据。

举个例子,假设你有一个在线商店,用户在网站上提交订单请求。每个订单请求需要查询库存数据库来确认库存情况。有时数据库查询可能很慢,导致用户等待时间过长。

通过使用 Redis 流:

  • 用户提交订单请求时,如果缓存中没有相关数据,请求会被包装成消息并放入 Redis 流队列。
  • 查询工作者从队列中取出消息,查询数据库,并更新订单状态。
  • 这样即使查询需要时间,用户的请求也不会被阻塞,可以尽快返回响应(返回一个暂时的订单状态,例如“处理中”,并在后台完成库存查询后更新订单状态。用户可以在个人账户页面查看订单的实时状态。)

Redis流的代码逻辑:

public class RedisQueueExample {
    private Jedis jedis;

    public RedisQueueExample() {
        this.jedis = new Jedis("localhost", 6379); // 连接到 Redis 服务器
    }

    public void addRequestToQueue(String request) {
        jedis.xadd("requestQueue", StreamEntryID.NEW_ENTRY, Map.of("request", request));
    }

    public void processQueue() {
        while (true) {
            var entries = jedis.xreadGroup("queryWorkers", "worker1", 1, 0, false,
                new redis.clients.jedis.StreamEntryID(">", "requestQueue"));
            for (var entry : entries) {
                var request = entry.getFields().get("request");
                System.out.println("Processing request: " + request);
                // 模拟处理请求
            }
        }
    }

    public static void main(String[] args) {
        RedisQueueExample queueExample = new RedisQueueExample();
        queueExample.addRequestToQueue("request_data");
        //正常情况,应该是另一个服务来消费处理流队列的请求
        queueExample.processQueue();
    }
}

3.分布式锁

假设有多个查询服务同时向数据库发送查询请求。如果这些查询非常耗时,数据库可能会因为同时处理太多请求而过载,导致性能下降甚至崩溃。

为了防止这种情况发生,他们使用 Redis 实现了一个分布式锁。

分布式锁协调多个客户端对共享资源(在这里是数据库)的访问。
在这里插入图片描述

看看分布式锁是怎么运行的:

  • 查询服务器准备向数据库发送请求之前,它会先尝试获取一个锁。如果成功获取到锁,才能继续访问数据库。
  • 锁有一个过期时间,以防止某些工作者在获取锁后由于故障或长时间未释放锁而导致锁一直占用。
  • 通过使用锁,可以限制同时访问数据库的查询服务数量。其他没有获取到锁的服务必须等待,直到有服务释放锁。

引入分布式锁后,能给系统带来以下好处:

  • 避免大量查询同时进入数据库,防止数据库过载,提高系统的稳定性和弹性。
  • 避免邻居吵闹问题,“吵闹邻居”问题是指某些高负载的请求影响了其他请求的性能。通过使用锁,可以避免这种情况的发生。

Redis用作分布式锁的代码逻辑:

public class RedisDistributedLock {
    private Jedis jedis;

    public RedisDistributedLock() {
        this.jedis = new Jedis("localhost", 6379); // 连接到 Redis 服务器
    }

    public boolean acquireLock(String lockKey, String lockValue, int expireTime) {
        String result = jedis.set(lockKey, lockValue, "NX", "EX", expireTime);
        return "OK".equals(result);
    }

    public void releaseLock(String lockKey, String lockValue) {
        if (lockValue.equals(jedis.get(lockKey))) {
            jedis.del(lockKey);
        }
    }

    public static void main(String[] args) {
        RedisDistributedLock lockExample = new RedisDistributedLock();
        String lockKey = "database_lock";
        String lockValue = "worker1";

        if (lockExample.acquireLock(lockKey, lockValue, 10)) {
            try {
                // 执行查询操作
                System.out.println("Lock acquired, accessing the database...");
            } finally {
                lockExample.releaseLock(lockKey, lockValue);
            }
        } else {
            System.out.println("Unable to acquire lock, another worker is accessing the database.");
        }
    }
}

4. 会话存储:

如果将Web 服务器设计成有状态,则服务难以扩展,因此引入 Redis 作为会话存储解决这个问题。通过 Redis 存储用户的会话数据,使 Web 服务器变得无状态,从而更容易扩展。
在这里插入图片描述

Redis 作为会话存储工作流程如下:

  • 用户向 Web 服务器发送请求(步骤1)。
  • Web 服务器将用户的会话数据存储在 Redis 中的哈希结构中(步骤2),每个用户的数据均设置了过期时间。
  • 每当用户发送请求时,会自动更新该会话数据的过期时间。

举个例子,假设我们有一个电子商务网站,有大量用户同时浏览和购买商品。为了确保系统能够在高并发环境下稳定运行,我们使用 Redis 作为会话存储。

  • 用户 A 登录网站,Web 服务器将用户 A 的会话数据(如用户 ID、登录状态、购物车内容等)存储在 Redis 中,并设置一个过期时间(例如30分钟)。
  • 用户 A 浏览商品并添加到购物车,每次请求时,Web 服务器都会从 Redis 中读取会话数据,并更新过期时间,确保用户的会话数据在活动期间不会过期。

引入 Redis 作为会话存储后,让系统变得易于扩展:由于 Web 服务器变成了无状态服务,因此可以轻松地添加或移除服务器,从而处理更多的用户请求。

Redis用作会话存储的代码逻辑:

public class RedisSessionStore {
    private Jedis jedis;

    public RedisSessionStore() {
        this.jedis = new Jedis("localhost", 6379); // 连接到 Redis 服务器
    }

    public void storeSessionData(String sessionId, String userData, int expireTime) {
        jedis.hset(sessionId, "data", userData);
        jedis.expire(sessionId, expireTime);
    }

    public String getSessionData(String sessionId) {
        return jedis.hget(sessionId, "data");
    }

    public static void main(String[] args) {
        RedisSessionStore sessionStore = new RedisSessionStore();
        String sessionId = "user123";

        sessionStore.storeSessionData(sessionId, "user_data", 1800);
        String data = sessionStore.getSessionData(sessionId);
        System.out.println(data);
    }
}

5. 限流器

短时间内大量的 API 请求可能会导致 Web 服务器超载。因此他们决定使用 Redis 实现限流器。
在这里插入图片描述

Redis 限流器是怎么运行的?

  • 使用 Redis 哈希数据结构实现计数器
  • 计数器表示在一个时间段内每个 API 端点允许的请求数
  • 每次收到请求时,计数器的值减一。
  • 如果计数器的值变为 0,请求将被拒绝
  • 在特定的时间段后,计数器会重置为初始值。

举个例子,假设我们有一个 API 接口,每分钟最多允许 100 次请求。我们使用 Redis 来实现这个简单的限流器。

  1. 初始设置:在 Redis 中为该 API 接口创建一个计数器,并将其值设置为 100(表示每分钟最多允许 100 次请求)。
  2. 收到请求:当用户 A 发送请求时,计数器的值减一,变为 99。
  3. 继续请求:随着更多用户发送请求,计数器的值不断减小。例如,当计数器的值变为 0 时,表示这一分钟内的请求配额已用完。
  4. 请求被拒绝:当用户 B 发送请求时,由于计数器的值为 0,服务器会拒绝该请求,并返回一个错误消息,提示用户请求过多。5.
  5. 计数器重置:在一分钟结束后,计数器会自动重置为 100,表示新的一分钟内可以接受新的 100 次请求。

使用 Redis 实现限流器后,可以防止恶意用户在短时间内发送大量请求,导致服务器过载,提高了系统的可用性和稳定性。

Redis用作限流器的代码逻辑:

public class RedisRateLimiter {
    private Jedis jedis;

    public RedisRateLimiter() {
        this.jedis = new Jedis("localhost", 6379); // 连接到 Redis 服务器
    }

    public boolean isAllowed(String userId, int maxRequests, int windowSeconds) {
        String key = "rate_limiter:" + userId;
        long currentCount = jedis.incr(key);

        if (currentCount == 1) {
            jedis.expire(key, windowSeconds);
        }

        return currentCount <= maxRequests;
    }

    public static void main(String[] args) {
        RedisRateLimiter rateLimiter = new RedisRateLimiter();
        String userId = "user123";

        if (rateLimiter.isAllowed(userId, 5, 60)) {
            System.out.println("Request allowed");
        } else {
            System.out.println("Rate limit exceeded");
        }
    }
}

最终,当这两名软件工程师将他们开发的应用程序投入生产时 - 它运行得非常好,大功告成 !

  • 17
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值