Redis是基于 key-value的nosql数据库
Redis支持丰富的数据结构,如字符串、列表、集合、有序集合等,使得它不仅仅是一个缓存系统,更是一个多用途的数据存储引擎。这为开发者提供了更多灵活的选择,能够更好地满足不同应用场景的需求。
使用的场景有:缓存、分布式锁、计数器、排行榜、地理空间数据
缓存
缓存是 redis 最常用的场景
缓存的使用场景有:
- 热点数据:当某些查询结果被频繁请求时,可以将这些结果缓存在Redis中,减轻数据库的负担,提高系统性能。 例如如用户信息、商品信息、课程信息、用户UGC 列表、社交网络点赞、粉丝关系、粉丝列表等
- 会话存储:登录之后的 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 开头的频道。
这种消费模式下支持多生产多消费,但是无法避免消息丢失(发送之后无订阅者直接丢失),消息堆积存在上限,超出数据就会丢失
使用场景
- 即时消息通知
在实时聊天应用中,每个用户可以订阅一个频道,当其他用户发送消息时,消息会被发布到这个频道,从而实现即时的消息通知。
PUBLISH chat:user1 "New message from user2: Hello!"
- 实时事件通知
在分布式系统中,可以使用发布/订阅模式实现实时的事件通知。例如,当某个重要的状态改变时,可以将状态更新的消息发布到相应的频道,通知所有关注该状态的模块。
PUBLISH system:status_update "New system status: OK"
- 日志订阅
在日志系统中,各个组件可以订阅特定的日志频道,以实时接收日志消息。这有助于监控系统的运行状况
PUBLISH logs:component1 "Error: Null Pointer Exception"
- 实时数据更新
在实时数据分析中,可以使用发布/订阅模式将实时生成的数据发布到相应的频道,以供订阅者进行实时处理和分析。
PUBLISH analytics:realtime_data_update "New data point: 123.45"
- 任务队列通知
在任务队列系统中,生产者将任务发布到一个频道,而工作节点则订阅该频道,以获取并处理任务。
PUBLISH tasks:queue "New task: Process Image"
- 系统事件通知:
在分布式系统中,可以使用发布/订阅模式实现系统事件的通知,如节点加入、节点离开等事件。
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 …]
- [NOMKSTREAM]
如果队列不存在是否重新创建队列 默认是自动创建 - [MAXLEN|MINID [=|~] threshold [LIMIT count]]
设置消息队列最大消息数量 - *|id
*代表由 redis 自动生成 格式时间戳-递增数字 - field value [field value …]
发送到队列的消息 key-value 键值对,可以是多个。
###### 消费消息
XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key …] id [id …] - [COUNT count]
一次性消费消息数量 - [BLOCK milliseconds]
当没有消息时阻塞时长 - STREAMS key
对应要消费的队列 - 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
实际应用中地理位置数据结构的应用。在地图上显示位置、计算周边地点、实时车辆追踪等
- 使用 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"
。。。。。。。。
。。。。。。。。
今天喝了点白酒,如果写的不对,请多多指点
没写完~ 持续更新~