redis 的应用

Redis是基于 key-value的nosql数据库
Redis支持丰富的数据结构,如字符串、列表、集合、有序集合等,使得它不仅仅是一个缓存系统,更是一个多用途的数据存储引擎。这为开发者提供了更多灵活的选择,能够更好地满足不同应用场景的需求。
使用的场景有:缓存、分布式锁、计数器、排行榜、地理空间数据

缓存

缓存是 redis 最常用的场景
缓存的使用场景有:

  1. 热点数据:当某些查询结果被频繁请求时,可以将这些结果缓存在Redis中,减轻数据库的负担,提高系统性能。 例如如用户信息、商品信息、课程信息、用户UGC 列表、社交网络点赞、粉丝关系、粉丝列表等
  2. 会话存储:登录之后的 token 信息

分布式锁

利用Redis实现分布式锁,也是Redis常见的应用

最简单的分布式锁

1、获取锁

SETNX lock_key unique_value

客户端向Redis发送SETNX命令,尝试将一个特定的键(作为锁的标识)设置为某个唯一值。
如果返回结果为1,表示锁成功被获取,客户端获得了锁。
如果返回结果为0,表示锁已经被其他客户端持有,当前客户端获取锁失败
2、设置锁的超时时间:

EXPIRE lock_key timeout_seconds

为了防止锁被持有的客户端异常退出而永远不释放,可以设置锁的超时时间(过期时间)。
可以使用EXPIRE或PEXPIRE命令为锁键设置过期时间,确保即使锁没有被显式释放,也会在一定时间后自动释放。
3、释放锁:

DEL lock_key

当客户端完成了对共享资源的操作时,需要释放锁。
可以使用DEL命令删除锁键,确保其他客户端能够获取锁。

当前最简单的实现还存在很多缺陷,在常用的 Redisson库中实现了很多分布式锁

redisson 分布式锁

1、普通锁(Rlock)
实现原理: 使用SETNX命令,如果锁的标识符不存在,则成功获取锁。
2、公平锁(RFairLock)
按照请求顺序进行分配的锁。每个线程按照请求锁的时间顺序排队获取锁,以确保公平性
实际存储在有序集合,其中成员是锁的标识符,分数是获取锁的时间戳。

ZADD myFairLock:timestamps 1629820835290 client1
ZADD myFairLock:timestamps 1629820835300 client2
ZADD myFairLock:timestamps 1629820835400 client3
SET myFairLock:lock_key "lock_value"

当一个客户端尝试获取锁时,它会使用 ZADD 命令将自己的标识符和当前时间戳添加到有序集合中。
然后,它会使用 SETNX 命令来尝试获取锁的标识符。如果 SETNX 返回1,表示获取锁成功。
如果获取锁失败,客户端可能会根据需要等待一段时间,然后重试。
3、联锁(RMultLock)
允许同时操作多个锁。它的实现原理基于Lua脚本和Redis的事务机制,确保获取和释放多个锁的原子性。
4、红锁(RRedLock)
基于在多个独立的Redis实例上加锁,通过多数原则确保锁的可用性
5、读写锁(RReadWriteLock)
基于RLock和RSemaphore实现,读操作不阻塞其他读操作,写操作会阻塞所有读和写操作。
6、信号量(RSemaphore):
实现原理: 使用Redis的原子操作,通过INCR和DECR操作控制资源的数量。

计数器

1、简单计数器
通过简单计数器可以记录记录使用频率、浏览量等

local key = KEYS[1] -- 键
local increment = tonumber(ARGV[1]) or 1 -- 增加的值,默认为1
local expireTime = tonumber(ARGV[2]) or 60 -- 过期时间,默认为60秒

-- 增加计数器的值
local currentValue = redis.call('INCRBY', key, increment)

-- 如果计数器是第一次被增加,设置过期时间
if currentValue == increment then
    redis.call('EXPIRE', key, expireTime)
end

return currentValue

2、频率限制器: 利用Redis的计数器特性,可以实现访问频率的限制,防止恶意请求或滥用。

-- 限制频率的 Lua 脚本
local key = KEYS[1] -- 键,可以是请求的 IP 地址或用户 ID
local maxRequests = tonumber(ARGV[1]) -- 允许的最大请求数
local windowSeconds = tonumber(ARGV[2]) -- 时间窗口(秒)

-- 获取当前计数器的值
local currentRequests = tonumber(redis.call('GET', key) or 0)

-- 检查是否超过了限制
if currentRequests + 1 > maxRequests then
    return 0 -- 超过限制,拒绝访问
else
    -- 增加计数器的值,并设置过期时间
    redis.call('INCR', key)
    redis.call('EXPIRE', key, windowSeconds)
    return 1 -- 允许访问
end

消息队列

消息队列的实现方式有多种
1、list
2、pub/sub 点对点消息发布/订阅模式
3、stream

list 模式

redis 中 list数据结构是双向链表
利用 LPUSH结合 RPOP 或者 RPUSH 结合LPOP 实现
事例:
生产者队列
在这里插入图片描述
消费者事例,其中 20最多等待 20s,通过不断定期 rpop 就可以获取到来自 list 新加入的消息内容
在这里插入图片描述
这种实现的消息队列只支持单个消费者(一个消息只能 pop 一次),同时不能保证消息丢失(pop 之后消息就结束了,但消费者没有处理完就宕机了,造成消息丢失)

pub/sub模式

redis 提供了发布/订阅功能,允许多个客户端通过订阅频道(Channel)来接收消息。当一个客户端向频道发布消息时,所有订阅了该频道的客户端都会接收到这条消息。
发布消息:

PUBLISH channel_name message

在这里插入图片描述

订阅消息:

SUBSCRIBE channel_name

在这里插入图片描述

订阅多个频道:

SUBSCRIBE channel_name1 channel_name2

模式订阅:

PSUBSCRIBE channel_pattern*

channel_pattern* 可以匹配到所有以 channel_pattern 开头的频道。

这种消费模式下支持多生产多消费,但是无法避免消息丢失(发送之后无订阅者直接丢失),消息堆积存在上限,超出数据就会丢失

使用场景
  1. 即时消息通知
    在实时聊天应用中,每个用户可以订阅一个频道,当其他用户发送消息时,消息会被发布到这个频道,从而实现即时的消息通知。
PUBLISH chat:user1 "New message from user2: Hello!"
  1. 实时事件通知
    在分布式系统中,可以使用发布/订阅模式实现实时的事件通知。例如,当某个重要的状态改变时,可以将状态更新的消息发布到相应的频道,通知所有关注该状态的模块。
PUBLISH system:status_update "New system status: OK"
  1. 日志订阅
    在日志系统中,各个组件可以订阅特定的日志频道,以实时接收日志消息。这有助于监控系统的运行状况
PUBLISH logs:component1 "Error: Null Pointer Exception"
  1. 实时数据更新
    在实时数据分析中,可以使用发布/订阅模式将实时生成的数据发布到相应的频道,以供订阅者进行实时处理和分析。
PUBLISH analytics:realtime_data_update "New data point: 123.45"
  1. 任务队列通知
    在任务队列系统中,生产者将任务发布到一个频道,而工作节点则订阅该频道,以获取并处理任务。
PUBLISH tasks:queue "New task: Process Image"
  1. 系统事件通知:
    在分布式系统中,可以使用发布/订阅模式实现系统事件的通知,如节点加入、节点离开等事件。
PUBLISH system:node_join "New node joined: Node-123"
java实现发布/订阅

发布消息

import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
import io.lettuce.core.pubsub.api.sync.RedisPubSubCommands;

public class SubscriberExample {

    public static void main(String[] args) {
        RedisClient redisClient = RedisClient.create("redis://your_redis_server:6379");

        // 建立连接
        try (StatefulRedisPubSubConnection<String, String> pubSubConnection = redisClient.connectPubSub()) {
            RedisPubSubCommands<String, String> syncPubSubCommands = pubSubConnection.sync();

            // 订阅频道
            syncPubSubCommands.subscribe("channel_name");

            // 接收消息
            while (true) {
                String message = syncPubSubCommands.receive().getPayload();
                System.out.println("Received message: " + message);
            }
        }
    }
}

订阅消息

import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
import io.lettuce.core.pubsub.StatefulRedisPubSubConnection;
import io.lettuce.core.pubsub.api.sync.RedisPubSubCommands;

public class SubscriberExample {

    public static void main(String[] args) {
        RedisClient redisClient = RedisClient.create("redis://your_redis_server:6379");

        // 建立连接
        try (StatefulRedisPubSubConnection<String, String> pubSubConnection = redisClient.connectPubSub()) {
            RedisPubSubCommands<String, String> syncPubSubCommands = pubSubConnection.sync();

            // 订阅频道
            syncPubSubCommands.subscribe("channel_name");

            // 接收消息
            while (true) {
                String message = syncPubSubCommands.receive().getPayload();
                System.out.println("Received message: " + message);
            }
        }
    }
}

基于stream的消息队列

stream 是 redis 5.0引入的一种新的数据类型,可以实现功能非常完善的消息队列(官网:https://redis.io/docs/data-types/streams/

基本案例
生产消息

XADD key [NOMKSTREAM] [MAXLEN|MINID [=|~] threshold [LIMIT count]] *|id field value [field value …]

  1. [NOMKSTREAM]
    如果队列不存在是否重新创建队列 默认是自动创建
  2. [MAXLEN|MINID [=|~] threshold [LIMIT count]]
    设置消息队列最大消息数量
  3. *|id
    *代表由 redis 自动生成 格式时间戳-递增数字
  4. field value [field value …]
    发送到队列的消息 key-value 键值对,可以是多个。
    在这里插入图片描述###### 消费消息
    XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key …] id [id …]
  5. [COUNT count]
    一次性消费消息数量
  6. [BLOCK milliseconds]
    当没有消息时阻塞时长
  7. STREAMS key
    对应要消费的队列
  8. id [id …]
    起始 id ,只返回比当前 id 大的消息,0 代表从起始开始消费; $代表从最新的消息开始
    在这里插入图片描述
    简单介绍一下,后续打算专门出一章讲讲 stream
延迟队列

延迟队列是一种延迟触发或者在指定时间触发任务的队列,当前实现这种延迟队列的方式有很多,例如定时根据时间点扫描数据库,rocketmq 延迟消息等等,这里讲讲 redis 实现延迟队列
使用 有序集合sorted set来存储任务,并利用任务的执行时间作为分数,通过分数来对成员进行排序。有序集合的实现方式允许我们执行一系列的集合操作,以执行时间作为分数,可以取小于等于当前时间戳的数据成员,从而实现延迟队列

import redis.clients.jedis.Jedis;

public class RedisDelayQueue {

    private static final String DELAYED_QUEUE_KEY = "delayed_queue";

    public static void main(String[] args) {
        // 初始化 Jedis 连接
        Jedis jedis = new Jedis("localhost", 6379);

        // 添加延迟任务
        addDelayedTask(jedis, "Task1", 5000); // 5秒后执行
        addDelayedTask(jedis, "Task2", 10000); // 10秒后执行

        // 模拟消费者轮询
        while (true) {
            processDelayedTasks(jedis);
            try {
                Thread.sleep(1000); // 模拟每秒轮询一次
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void addDelayedTask(Jedis jedis, String task, long delayMillis) {
        long currentTime = System.currentTimeMillis();
        long executeTime = currentTime + delayMillis;

        // 将任务添加到有序集合,使用执行时间作为分数
        jedis.zadd(DELAYED_QUEUE_KEY, executeTime, task);

        System.out.println("Added task: " + task + " with delay of " + delayMillis + " milliseconds");
    }

    private static void processDelayedTasks(Jedis jedis) {
        long currentTime = System.currentTimeMillis();

        // 获取当前时间之前的任务
        Set<String> tasksToProcess = jedis.zrangeByScore(DELAYED_QUEUE_KEY, 0, currentTime);

        // 处理任务
        for (String task : tasksToProcess) {
            System.out.println("Processing task: " + task);

            // 在实际应用中,这里可以执行具体的任务处理逻辑

            // 从有序集合中移除已处理的任务
            jedis.zrem(DELAYED_QUEUE_KEY, task);
        }
    }
}

redission 对延迟队列做了简单的封装

import org.redisson.Redisson;
import org.redisson.api.RBlockingQueue;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.concurrent.TimeUnit;

public class RedissonDelayQueue {

    public static void main(String[] args) {
        // 初始化 Redisson 客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        RedissonClient redisson = Redisson.create(config);

        // 获取延迟队列
        RBlockingQueue<String> delayedQueue = redisson.getBlockingQueue("delayedQueue");

        // 添加延迟任务
        addDelayedTask(delayedQueue, "Task1", 5, TimeUnit.SECONDS);
        addDelayedTask(delayedQueue, "Task2", 10, TimeUnit.SECONDS);

        // 模拟消费者轮询
        while (true) {
            processDelayedTasks(delayedQueue);
            try {
                Thread.sleep(1000); // 模拟每秒轮询一次
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static void addDelayedTask(RBlockingQueue<String> delayedQueue, String task, long delay, TimeUnit timeUnit) {
        // 将任务添加到延迟队列
        delayedQueue.offerAsync(task, delay, timeUnit);
        System.out.println("Added task: " + task + " with delay of " + delay + " " + timeUnit.toString());
    }

    private static void processDelayedTasks(RBlockingQueue<String> delayedQueue) {
        // 获取并处理延迟队列中的任务
        String task = delayedQueue.pollAsync();
        while (task != null) {
            System.out.println("Processing task: " + task);

            // 在实际应用中,这里可以执行具体的任务处理逻辑

            // 获取下一个任务
            task = delayedQueue.pollAsync();
        }
    }
}

当然 redis 实现延迟队列也存在很多缺点,实现的延迟队列 score使用毫秒级数据,如果需要更高精度,建议选择专业的延迟队列;数据没有持久化,redis 数据都在缓存中;sortedset 中 score 是按照升序排列的,如果是相同的 score,无法确保顺序;重点来了,如果任务处理失败了,不能自动重试,需要自己去写重试的方案,否则数据丢失了丢失了,可就惨了

地理位置Geospatial

实际应用中地理位置数据结构的应用。在地图上显示位置、计算周边地点、实时车辆追踪等

  1. 使用 GEOADD 命令将地理位置添加到有序集合中。
GEOADD locations 13.361389 38.115556 "Palermo"
GEOADD locations 15.087269 37.502669 "Catania"

在这里插入图片描述
2. 计算周边地点:
使用 GEORADIUS 命令,以指定地理位置为中心,指定半径,获取周边地点列表

GEORADIUS locations 13.361389 38.115556 50 km

在这里插入图片描述
3. 实时车辆追踪:
实时车辆追踪场景中,不断更新车辆的地理位置。使用 GEOPOS 命令获取车辆的当前地理位置。

GEOADD vehicles 13.361389 38.115556 "Car1"
GEOADD vehicles 15.087269 37.502669 "Car2"
GEOPOS vehicles "Car1"

在这里插入图片描述

。。。。。。。。

。。。。。。。。

今天喝了点白酒,如果写的不对,请多多指点
没写完~ 持续更新~

  • 18
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值