Redis: 全名Remote Dictionary Server
,翻译叫 远程字典服务器
,
字典是因为存储数据是 键值对Key-Value
,提供Key查找Value,类似Map,Redis (Remote Dictionary Server) 是一个开源的、基于内存的 键值对存储系统
既然是系统那就是可以部署服务,这个系统部署完了就是一个服务器,就是Redis服务器(相同类型的举例mysql,Oracle也都是这样)
,这个服务器里的数据都是
而叫远程服务器
,是因为,比如我们部署一个服务系统A,服务器系统A想要Redis数据调用,而这个Redis数据来源是从Redis服务器来的,这种跨服务系统获取到的流程,是远程服务,但是注意啊:这个跨系统的调用不是直接从服务系统A调用的Redis服务系统的
,往下看
redis最大特性就是快
,Redis的数据主要存储在内存(RAM)中
,比如我们部署一个服务系统运行(服务系统运行是在内存中
),想要获取一个oracle表中的数据是流程 是先从数据库找到--然后系统服务读取到转换存入内存中
,如果数据量大 或者这个 表体量大 或者处理逻辑复制都需要时间
而从Redis数据获取直接拿到存储在内存中的值 通常达到微秒级别
,比如部署服务系统A启动之后会读取Redis服务器里 将Redis数据结构缓存在内存中
好你可以说 数据表可以存储多个字段 那系统查找某个表可以一次获取多个字段,Redis也可以
Redis的数据结构丰富
存储键值对
,这个K作为唯一
的键值 这个和Map一样,一般用String作K(因为String是final)
而这个V就丰富了:
1.String字符串
JSON字符串
是个String字符串,数据结构丰富可以有映射关系,例如:{“name”: “John”, “age”: 30}
或[1, 2, 3]
或 [“apple”, “banana”]
又或 JSON支持六种基本的数据类型:number、string、boolean、null、array、object
2. HTML 片段 这个 一般比较长了
// 单单存储String (字符串)
jedis.set("user:1000:name", "Alice"); // 存储
String name = jedis.get("user:1000:name"); // 读取
//存储会话信息 带JSON字符串的
// 存储会话键名 "session:user123" 过期时间(秒)1800 会话数据JSON字符串
jedis.setex("session:user123", 1800, "{'username':'Alice', 'role':'admin'}");
// 获取会话 如果会话过期或不存在,sessionData 将返回 null
String session = jedis.get("session:user123");
2.List(列表)
一个有序
的字符串集合。元素按插入顺序排序。可从列表的头部 (Left)
或尾部 (Right)
添加/移除元素
// 这里也看出来了 不同的数据结构有不同的方法 List的方法是rpush尾巴
jedis.rpush("tasks", "email", "report", "meeting"); // 存储 (右侧插入)
List<String> tasks = jedis.lrange("tasks", 0, -1); // 读取 (获取全部)
3.Set (集合)
一个无序
的字符串集合。元素是唯一
的(不允许重复)
jedis.sadd("tags:article123", "tech", "redis", "database"); // 存储
Set<String> tags = jedis.smembers("tags:article123"); // 读取
4.Sorted Set (有序集合, ZSet)
类似于 Set(元素唯一),但每个元素都关联一个分数 (score),一个浮点
数。集合根据这个分数从小到大排序
。分数可以相同,此时按元素的字典序排序
// 4. Sorted Set (有序集合)
jedis.zadd("leaderboard", 2500, "player1"); // 存储 (分数+成员)
Set<String> topPlayers = jedis.zrevrange("leaderboard", 0, 2); // 读取 (前3名)
5.Hash (哈希表 / 字典)
存储字段-值 (key-value) 对
的集合。非常适合表示对象(如用户信息)
// 5. Hash (哈希)
jedis.hset("user:1000", "name", "Alice", "email", "[email protected]"); // 存储
String email = jedis.hget("user:1000", "email"); // 读取单个字段
6.Bitmaps (位图)
不是独立的数据类型
,而是基于 String 类型提供的一套位级操作命令
。可以将 String 视为一个巨大的位数组(bit array)
// 6. Bitmaps (位图)
jedis.setbit("login:20231001", 1000, true); // 存储 (用户1000在2023-10-01登录)
boolean loggedIn = jedis.getbit("login:20231001", 1000); // 读取
7.HyperLogLog (HLL)
不是独立的数据类型
,是一种概率性数据结构
,用于高效地估算一个集合的基数
(集合中唯一元素的数量)。特点是占用空间极小
且固定(约 12KB),无论集合大小
// 7. HyperLogLog (基数统计)
jedis.pfadd("visitors:20231001", "192.168.1.1", "10.0.0.5"); // 存储
long uniqueVisitors = jedis.pfcount("visitors:20231001"); // 读取估算值
8.Geospatial (地理空间)
不是独立的数据类型
,基于 Sorted Set (ZSet) 实现,用于存储和查询地理位置(经度、纬度)
信息
// 8. Geospatial (地理空间)
jedis.geoadd("cities", 116.40, 39.90, "Beijing"); // 存储 (经度,纬度,名称)
List<GeoCoordinate> position = jedis.geopos("cities", "Beijing"); // 读取坐标
9.Stream (流)
Redis 5.0 引入的持久化、可追加
的消息队列数据结构。设计用于可靠地消费和处理事件流
// 9. Stream (流)
StreamEntryID id = jedis.xadd("order:events", null, Map.of("type", "created", "amount", "99.9")); // 存储消息
List<StreamEntry> entries = jedis.xrange("order:events", "-", "+", 1); // 读取最新消息
这里面可以看出 Redis的 应用场景上可以作为 : 内存数据库、缓存、消息代理和流处理引擎
我们按照部署测试一个单机服务系统时候,里面一般会带有kafka,Redis, 一个主流数据库如mysql,还有es之类的,这里单独针对Redis有个问题
既然Redis是数据结构是直接存储在内存中的,那重启服务系统时候内存会清除重置,那存储在内存中的Redis数据是不是没有了?
答案是 会,当重启服务时候所有在内存中的数据都会清除 自然包括内存中的Redis数据结构
,但是注意是清除掉缓存在内存中的Redis数据,但不会清除掉Redis服务器里面的数据(两个独立的系统)
而重启后redis数据可以重新从Redis服务中获取然后加载到内存中 就依赖于Redis的持久化机制
持久化: redis 数据在内存中,但它提供可选的持久化机制(RDB 快照和 AOF 日志),可以将内存数据保存到磁盘,防止服务器重启导致数据丢失
但是需要注意的是,Redis的持久化需要配置,即Redis 缓存是否会在服务重启后清空,取决于 Redis 的持久化配置
1.未配置任何持久化 (默认行为)
会清空!
重启后,Redis 将从一个空数据集开始
2.配置了 RDB 持久化
RDB是Redis DataBase的缩写,即内存快照
:将Redis在内存中的数据库状态保存到磁盘里面
。RDB文件是一个二进制文件dump.rdb称快照
,RDB在特定时间点创建整个数据库的快照
并保存到磁盘
重启服务后
a.如果Redis 正常关闭
(接收到 SHUTDOWN 命令),它会自动触发一次 RDB 快照保存(如果配置允许)。重启后会加载这个最新的 RDB 文件,数据基本保留
b.如果 Redis 异常崩溃
,或者在最后一次配置的自动保存时间点之后没有成功触发过 RDB保存
,那么重启后会加载最近一次成功保存的RDB文件。而自上次 RDB 保存后到崩溃前的所有数据更改都会丢
服务器重启时会自动执行载入RDB文件。RDB持久化功能既可以手动执行,也可以通过配置文件定期执行。RDB通过生成一个经过压缩的二进制文件,保存数据库状态,可以通过该文件进行还原
所以RDB可能保留,也可能部分丢失,取余于Redis服务是否正常关闭
3.配置了 AOF 持久化
AOF 记录所有写操作命令(以 Redis 协议格式),并追加写入到一个日志文件(.aof 文件)中
重启后
:
Redis 重启时会重新执行 AOF 文件中的所有写命令来重建内存数据集。
只要 AOF 文件没有损坏,就能恢复到最后一次写操作被记录的状态
但是也不能完全保证所有数据都能记录,比如AOF 文件损坏了
3.同时配置 RDB + AOF
同时启用两种持久化方式,最安全(推荐生产环境使用)
说白了就是缓存备份文件同时用两份
重启后:
1.Redis 优先使用 AOF 文件来恢复数据。因为AOF通常包含更完整的数据集(更新到更近的时间点)
2.只有在AOF功能关闭 (appendonly no) 或 AOF 文件不存在时,才会加载 RDB 文件。
优势: 结合了 RDB 的快照备份和快速恢复特性,以及 AOF 的接近实时的持久性。提供了最高的数据安全性。
Redis 是独立服务,可以在多个应用之间共享
例如:分布式Session
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
既然Redis数据可以写入,那么在分布式系统部署中,如果多个服务器写入Rediss数据, 如何保证Redis数据的准确性呢
//同时操作
服务器A jedis.set("user:1000:name", "Alice"); // 存储
服务器B jedis.set("user:1000:name", "Alice"); // 存储
服务器C jedis.set("user:1000:name", "Alice"); // 存储
Redis特性原子操作 :对于简单的操作,在 Redis2.6.12版本开始,string的set命令增加了一些参数,这些单个命令的操作是原子性的
EX:设置键的过期时间(单位为秒)
PX:设置键的过期时间(单位为毫秒)
NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
XX :只在键已经存在时,才对键进行设置操作
这些操作是原子性的,可简单以此实现一个分布式的锁 如set LOCK_KEY, LOCK_VALUE
对于复杂操作
,它支持 Lua 脚本
执行,保证脚本内的命令序列原子性执行
基于Redis的分布式锁示例
public class RedisLockExample {
private static final String LOCK_KEY = "my_distributed_lock";
private static final String LOCK_VALUE = "locked";
private static final int LOCK_EXPIRE = 30000; // 锁超时时间(毫秒)
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
try {
// 尝试获取锁(SETNX + 超时时间)
String result = jedis.set(LOCK_KEY, LOCK_VALUE,
SetParams.setParams().nx().px(LOCK_EXPIRE));
if ("OK".equals(result)) {
System.out.println("进程获取到Redis锁,开始执行临界区操作...");
Thread.sleep(5000); // 模拟耗时操作
} else {
System.out.println("获取锁失败,其他进程正在运行");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放锁(需使用Lua脚本确保原子性)
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, LOCK_KEY, LOCK_VALUE);
jedis.close();
}
}
}
上面我们讲了多个服务器对应一个Redis服务,就一个Redis服务器,就是单机,这种模式叫单机模式
那问题又来了,如果多个服务器获取Redis的频率比较高,那一个Redis服务器不能支撑,那两个三个
这种通过引入多个服务器(多个节点)分别部署redis-server进程,就是Redis的分布式系统
部署
问题又来了,读取的性能问题解决了,那写入的安全问题呢
//同时操作
服务器A 写入Redis1:jedis.set("user:1000:name", "Alice"); // 存储
服务器B 写入Redis2:jedis.set("user:1000:name", "Alice"); // 存储
主从模式
1.一个Redis实例作为主机,主机支持数据的写入和读取等各项操作
2.其余的实例作为备份机,数据从主机备份来,从机只能读取不能写入
优点
:读写分离,提高服务器性能同时保证缓存并发安全性
缺点
:
1.主节点故障问题:一旦Master节点由于故障不能提供服务,需要人工将Slave节点晋升为Master节点,在这个人工切换等待过程中 很容易丢失数据
2.对于写入数据而言是个单机,如写入数据量特别大一个主是存储不了
针对第一个问题,衍生出了哨兵模式
它是主从复制模式的进阶版本
,当主节点故障或者别的原因不可用适合,会自动从哨兵点里面选择一个作为新的主节点,并让其它的从节点从新的主节点复制数据,哨兵模式解决了主从模式中需要人工干预进行切换的问题
但是这种主从复制模式
或者进化后的哨兵模式
解决了频繁读取性能问题,但是写入场景而言 还是单机啊,如果写入数据量大的情况下,一个主机存储有压力,这种主从复制模式适合 读多写少 的场景
,而且两种
为了解决这个问题 又引入了Redis集群部署的模式
Redis集群模式
主要由两部分组成:集群客户端
和集群服务器
集群客户端
:也叫 分片机
,采用一致性hash算法将数据分为n个槽(这点类似hashMap的哈希桶
),通俗的讲 地盘划分
数据用hash算法计算,每个槽对应一定范围的数据,不同的节点负责处理不同的槽,比如
槽1:0-100000
槽2:100001- 20000
......
集群服务器
:集群服务器采用了多主复制
方式,就是每个Redis服务节点(真正读取和写入redis数据的节点
),每个节点都是一个主节点,并且同时可以充当其他节点的从节点
当写入时候,根据hash算法计算应该写入到哪个节点,从而保证并发安全性
当读取时候,根据负载均衡选择其中任一节点查询
也就是说每个节点: 一定范围的写入权限 + 从其他节点复制来的可被读取的数据
每个节点既是其他节点的主节点(在数据属于它管理范围 它是主)
又是其他节点的从节点在数据 不属于它管理范围 它是从)
当其中一个节点宕机了,会从其余的节点选择一个接替自己,那么客户端会调整分片将槽加给它
同事离职了,领导让你接手他的活,你就变成干两个人的活了
这就是Redis集群提供了一种自动数据迁移机制
当一个节点加入或离开集群时,会自动迁移该节点上负责的所有槽位数据到其他节点
另外Redis集群可扩展性
:集群模式支持动态添加或移除节点
,可以根据业务需求灵活扩展存储容量和处理能力
Redis全局ID通常用于在分布式系统中生成全局唯一ID,也计算一个键值对的Redis数据一个ID
首先全局ID用处
1.比如上面我们redis集群 分片,如果使用key计算哈希值,可能有哈希冲突,但如果使用我们特定算法的全局ID,可以减少哈希冲突的可能性,这是一种扰动哈希的思想
2.同样查找上是需要先定位槽,越精准唯一的哈希结果值得到结果的速度越快,比如查找ID哈希值为1000的数据,它会快速定位到槽对于的Redis节点,然后查找
为了增加全局ID的不重复的可能性,一般我们不用redis自带的序列号,基于时间戳 和 位移生成
public class TimestampBasedIdGenerator {
private static final long EPOCH = 1420041600000L; // 任意起始时间戳
public static long generateId() {
long timestamp = System.currentTimeMillis();
long sequence = ThreadLocalRandom.current().nextLong(1000);
return ((timestamp - EPOCH) << 32) | sequence;
}
}
RedisTemplate.opsForHash()方法用于获取操作哈希数据类型的接口下辖方法
put:设置哈希字段的值
putAll:设置多个哈希字段的值
get:获取哈希字段的值
multiGet:获取多个哈希字段的值
hasKey:判断哈希中是否存在指定的字段
keys:获取哈希的所有字段
values:获取哈希的所有值
entries:获取哈希的所有字段和对应的值
increment:自增哈希字段的值
delete:删除哈希字段的值2。
RedisTemplate.opsForSet():提供了操作set类型的 API
add:向集合中添加一个或多个元素;
members:获取集合中的所有成员;
isMember:判断元素是否是集合的成员;
randomMember:获取集合中的随机元素;
pop:弹出并返回集合中的一个随机元素;
remove:从集合中移除一个或多个元素;
intersect:计算多个集合的交集,并返回结果集合;
union:计算多个集合的并集,并返回结果集合;
difference:计算两个集合的差集,并返回结果集合。
RedisTemplate.opsForList() 提供了操作List类型的 API,支持双向链表操作(如队列、栈)
leftPush(K key, V value) 头部插入元素 LPUSH key value
rightPush(K key, V value) 尾部插入元素 RPUSH key value
leftPop(K key) 移除并返回头部元素 LPOP key
rightPop(K key) 移除并返回尾部元素 RPOP key
range(K key, long start, long end) 获取范围元素 (0 到 -1 为全部) LRANGE key start end
size(K key) 获取列表长度 LLEN key
index(K key, long index) 获取指定索引位置的元素 LINDEX key index
remove(K key, long count, Object value) 删除元素 (count=0 删所有;count>0 从头部删;count<0 从尾部删) LREM key count value
trim(K key, long start, long end) 保留指定范围内的元素(裁剪列表)
使用示例
@Autowired
private RedisTemplate<String, String> redisTemplate; // 注入 RedisTemplate
// 获取 List 操作对象
ListOperations<String, String> listOps = redisTemplate.opsForList();
// 1. 头部插入元素
listOps.leftPush("myList", "A"); // 结果: ["A"]
listOps.leftPush("myList", "B"); // 结果: ["B", "A"]
// 2. 尾部插入元素
listOps.rightPush("myList", "C"); // 结果: ["B", "A", "C"]
// 3. 获取全部元素
List<String> allItems = listOps.range("myList", 0, -1); // ["B", "A", "C"]
// 4. 头部弹出元素
String head = listOps.leftPop("myList"); // 弹出 "B",剩余: ["A", "C"]
// 5. 删除指定元素(删除2个 "A")
listOps.remove("myList", 2, "A"); // 删除成功返回删除数量
// 6. 裁剪列表(保留索引1到2)
listOps.trim("myList", 1, 2); // 保留第2个到第3个元素(索引从0开始)
// 7. 获取列表长度
Long size = listOps.size("myList"); // 返回剩余元素数量
利用Redis的List数据结构,可以实现FIFO(先进先出)队列
,适用于任务队列、日志处理等
LPUSH
(从左边插入)或RPUSH
(从右边插入)命令添加消息。BRPOP
(阻塞式右弹出)或BLPOP
(阻塞式左弹出)命令获取消息Spring Boot示例
生产者:
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void sendMessage(String queueName, String message) {
redisTemplate.opsForList().leftPush(queueName, message);
}
消费者(阻塞式):
public void startConsumer(String queueName) {
new Thread(() -> {
while (true) {
// 阻塞式右弹出,等待时间为0表示无限等待
String message = redisTemplate.opsForList().rightPop(queueName, 0, TimeUnit.SECONDS);
if (message != null) {
processMessage(message);
}
}
}).start();
}
private void processMessage(String message) {
// 处理消息逻辑
System.out.println("Received: " + message);
}
该模式允许消息的发布者
将消息发送到特定的频道
,而订阅者
可以监听一个或多个频道并接收消息。这种模式实现了消息的广播,适用于实时消息推送、事件通知
等场景
角色 | 说明 |
---|---|
发布者 (Publisher) | 向指定频道发送消息的客户端 |
订阅者 (Subscriber) | 监听一个或多个频道的客户端,实时接收消息 |
频道 (Channel) | 消息传递的通道(命名空间) |
模式 (Pattern) | 使用通配符订阅匹配的多个频道(如 news.* ) |
Redis 原生命令
命令 | 作用 |
---|---|
PUBLISH channel message |
向频道发布消息 |
SUBSCRIBE channel1 channel2 |
订阅一个或多个频道 |
PSUBSCRIBE pattern |
使用通配符订阅频道(如 news.* ) |
UNSUBSCRIBE [channel] |
退订指定频道 |
PUNSUBSCRIBE [pattern] |
退订模式匹配的频道 |
关键特性
1.实时性 :消息即时推送,订阅者收到消息的延迟通常在毫秒级。
2.无持久化 :消息不持久化:订阅者离线期间的消息会丢失,不适合关键业务消息。
3.广播机制 :一条消息会被所有订阅该频道的客户端接收。
4.通配符订阅 :支持 PSUBSCRIBE
订阅模式(如 sensor.*
匹配 sensor.temp
和 sensor.humidity
)
适用场景
场景 | 说明 |
---|---|
实时通知 | 用户在线消息推送、系统报警通知 |
事件驱动架构 | 微服务间的事件通知(如订单创建触发库存更新) |
聊天室/群组广播 | 向所有在线成员广播消息 |
配置更新 | 服务配置变更时通知所有节点刷新配置 |
在 Spring Data Redis 中的实现
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void publishMessage(String channel, String message) {
// 向指定频道发送消息
redisTemplate.convertAndSend(channel, message);
}
// 使用示例
publishMessage("news", "Breaking: Redis 7.0 released!");
Step 1: 创建消息监听器
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
public class NewsMessageListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
// 获取消息内容
String channel = new String(message.getChannel());
String body = new String(message.getBody());
System.out.printf("Received [%s]: %s%n", channel, body);
}
}
Step 2: 配置订阅容器
@Configuration
public class RedisPubSubConfig {
@Bean
public RedisMessageListenerContainer container(
RedisConnectionFactory connectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
// 订阅具体频道
container.addMessageListener(listenerAdapter, new ChannelTopic("news"));
// 订阅模式匹配的频道(通配符)
// container.addMessageListener(listenerAdapter, new PatternTopic("news.*"));
return container;
}
@Bean
public MessageListenerAdapter listenerAdapter(NewsMessageListener listener) {
return new MessageListenerAdapter(listener);
}
@Bean
public NewsMessageListener messageListener() {
return new NewsMessageListener();
}
}
特点:支持消费者组、消息持久化
、ACK 机制
Redis 命令
> XADD orders:stream * orderId 1001 # 添加消息
> XGROUP CREATE orders:stream order-group $ # 创建消费者组
> XREADGROUP GROUP order-group consumer1 STREAMS orders:stream > # 消费消息
Spring Boot 实现
@Bean
public StreamMessageListenerContainer<String,
MapRecord<String, String, String>> streamContainer(RedisConnectionFactory factory) {
// 容器配置
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> options =
StreamMessageListenerContainer.StreamMessageListenerContainerOptions
.builder()
.pollTimeout(Duration.ofSeconds(1))
.build();
// 创建容器
StreamMessageListenerContainer<String,
MapRecord<String, String, String>> container
=StreamMessageListenerContainer.create(factory, options);
// 创建消费者组(如果不存在)
try {
redisTemplate.opsForStream().createGroup("orders:stream", "order-group");
} catch (RedisSystemException e) {
// 组已存在时忽略
}
// 消费者配置
StreamMessageListenerContainer.StreamReadRequest<String> request =
StreamMessageListenerContainer.StreamReadRequest
.builder(StreamOffset.create("orders:stream", ReadOffset.lastConsumed()))
.consumer(Consumer.from("order-group", "consumer1"))
.autoAcknowledge(false) // 手动ACK
.cancelOnError(e -> true)
.build();
// 消息处理器
container.register(request, (message) -> {
Map<String, String> msg = message.getValue();
System.out.println("处理订单: " + msg.get("orderId"));
// 手动ACK
redisTemplate.opsForStream()
.acknowledge("orders:stream", "order-group", message.getId());
});
container.start();
return container;
}
特点:实现定时任务,有序集合按分数(时间戳)排序
Redis 命令:
> ZADD delay:queue 1690000000 "task1" # 添加延时任务(时间戳)
> ZRANGEBYSCORE delay:queue 0 1690000000 # 获取到期任务
Spring Boot 实现:
// 添加延时任务
public void addDelayedTask(String task, long delaySeconds) {
long executeTime = System.currentTimeMillis() + delaySeconds * 1000;
redisTemplate.opsForZSet().add("delay:queue", task, executeTime);
}
// 扫描执行任务
@Scheduled(fixedRate = 5000)
public void processDelayedTasks() {
long now = System.currentTimeMillis();
Set<String> tasks = redisTemplate.opsForZSet()
.rangeByScore("delay:queue", 0, now);
for (String task : tasks) {
System.out.println("执行延时任务: " + task);
redisTemplate.opsForZSet().remove("delay:queue", task);
}
}
特性 | List 队列 | Pub/Sub | Streams | ZSet 延时队列 |
---|---|---|---|---|
消息持久化 | ✓ | ✗ | ✓ | ✓ |
消费者组 | ✗ | ✗ | ✓ | ✗ |
消息确认(ACK) | ✗ | ✗ | ✓ | ✗ |
阻塞式消费 | ✓ | ✓ | ✓ | ✗ |
延时消息 | ✗ | ✗ | ✓(有限) | ✓ |
消息回溯 | ✗ | ✗ | ✓ | ✗ |
适用场景 | 简单任务队列 | 实时广播通知 | 企业级可靠队列 | 定时任务/订单超时 |
适用场景
场景 | 推荐方案 | 说明 |
---|---|---|
订单处理队列 | List 或 Streams | 保证顺序处理,Streams 更可靠 |
库存变更通知 | Pub/Sub | 实时广播给多个服务 |
支付超时关闭 | ZSet 延时队列 | 30分钟未支付自动取消订单 |
用户行为日志收集 | Streams | 高吞吐量,支持多消费者组 |
秒杀库存扣减 | List + Lua 脚本 | 保证原子性操作 |
incr :将指定的key的值加一,如果key不存在,那么Redis将自动创建key,并将value初始化为1
incrby:与INCR命令类似,不过该命令不是加1操作,它表示在原数的基础上进行指定数值的自增运算
decr :与INCR命令相反,它对数值执行减1操作
decrby:与INCRBY命令相反,它表示在原数值的基础上进行指定数值的自减运算
incrbyFloat :string中唯一操作浮点数的命令,浮点数可以为正数或者负数,从而实现对数值的加减操作
业务需求中经常有需要用到计数器的场景:实时获取文章的阅读量、微博点赞数,如果我们从数据库查找 有一定的时间延迟,从内存中查找就比较快没有延迟达到实时效果
代码实现(Spring Boot + Redis + MySQL)
1.实时写入Redis(Service层)
@Service
public class StatsService {
// 注入Redis操作模板
private final RedisTemplate<String, Object> redisTemplate;
// 定义Redis中存储文章阅读量的Hash结构key
private static final String ARTICLE_VIEWS = "stats:article:views";
// 定义Redis中存储微博点赞数的Hash结构key
private static final String WEIBO_LIKES = "stats:weibo:likes";
// 构造器依赖注入
public StatsService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
// 增加文章阅读量方法
public void incrementArticleView(Long articleId) {
// 使用Redis的Hash结构原子递增操作:
// 1. 指定Hash结构的key (ARTICLE_VIEWS)
// 2. 使用文章ID作为field
// 3. 每次增加1个计数
redisTemplate.opsForHash().increment(ARTICLE_VIEWS, articleId.toString(), 1);
}
// 增加微博点赞数方法
public void incrementWeiboLike(Long weiboId) {
// 同上,对微博点赞数进行原子递增
redisTemplate.opsForHash().increment(WEIBO_LIKES, weiboId.toString(), 1);
}
}
2.定时同步任务
@Component
public class StatsSyncTask {
// 日志记录器
private static final Logger logger = LoggerFactory.getLogger(StatsSyncTask.class);
// 依赖注入
private final RedisTemplate<String, Object> redisTemplate;
private final ArticleRepository articleRepo;
private final WeiboRepository weiboRepo;
// 构造器注入依赖项
public StatsSyncTask(RedisTemplate<String, Object> redisTemplate,
ArticleRepository articleRepo,
WeiboRepository weiboRepo) {
this.redisTemplate = redisTemplate;
this.articleRepo = articleRepo;
this.weiboRepo = weiboRepo;
}
// 定时任务注解:每5分钟执行一次同步
@Scheduled(fixedRate = 5 * 60 * 1000)
public void syncStatsToDatabase() {
// 同步文章阅读量
syncArticleViews();
// 同步微博点赞数
syncWeiboLikes();
}
// 文章阅读量同步方法
private void syncArticleViews() {
// 当前数据的主Key
String currentKey = "stats:article:views";
// 创建备份Key(添加时间戳保证唯一性)
String backupKey = "stats:article:views:backup_" + System.currentTimeMillis();
// 1. 原子重命名操作:将当前Key重命名为备份Key
try {
// 检查主Key是否存在
if (Boolean.TRUE.equals(redisTemplate.hasKey(currentKey))) {
// 原子重命名操作:保证在重命名瞬间不会有新数据写入
redisTemplate.rename(currentKey, backupKey);
} else {
// 没有数据需要同步,直接返回
return;
}
} catch (Exception e) {
// 重命名失败记录日志并退出
logger.error("重命名文章阅读量Key失败", e);
return;
}
// 2. 从备份Key中获取所有数据(字段=文章ID, 值=阅读量增量)
Map<Object, Object> viewsMap = redisTemplate.opsForHash().entries(backupKey);
// 3. 遍历处理每个文章的阅读量增量
viewsMap.forEach((articleIdObj, countObj) -> {
try {
// 转换文章ID为Long类型
Long articleId = Long.parseLong((String) articleIdObj);
// 转换增量值为Integer类型
Integer increment = ((Number) countObj).intValue();
// 4. 更新数据库(增加文章的阅读量)
articleRepo.incrementViews(articleId, increment);
} catch (Exception e) {
// 5. 数据库更新失败处理
logger.error("更新文章阅读量失败: {}={}", articleIdObj, countObj, e);
// 失败回退:将增量数据重新加回主Key
redisTemplate.opsForHash().increment(
currentKey, // 主Key(新数据会写入这里)
articleIdObj, // 文章ID字段
((Number) countObj).longValue() // 需要回退的增量值
);
}
});
// 6. 同步完成后删除备份Key(无论成功失败)
redisTemplate.delete(backupKey);
}
// 微博点赞数同步方法(实现逻辑与文章阅读量相同)
private void syncWeiboLikes() {
// 实现代码与syncArticleViews类似
// ...
}
}
3.数据库操作(JPA示例)
// JPA仓库接口
public interface ArticleRepository extends JpaRepository<Article, Long> {
// 自定义更新方法注解
@Modifying
// JPQL更新语句:增加文章的阅读量
@Query("UPDATE Article SET viewCount = viewCount + :increment WHERE id = :id")
// 参数绑定
void incrementViews(@Param("id") Long id, @Param("increment") Integer increment);
}
关键步骤:
1.原子重命名操作 (核心保障)
重命名的目的主要是为了将当前需要处理的数据“快照”出来,然后去处理这个快照,而新的请求会继续写入到原来的currentKey中,这样就不会阻塞实时写入
redisTemplate.rename(currentKey, backupKey);
原子性:Redis的RENAME命令是原子操作,执行瞬间完成
零数据丢失:重命名后新数据会写入原currentKey
一致性保证:备份Key包含重命名时刻的完整数据集
备份Key设计
String backupKey = "stats:article:views:backup_" + System.currentTimeMillis();
唯一性:使用时间戳确保每次备份Key唯一
安全性:避免多实例运行时Key冲突
可追溯:Key中包含时间信息便于排查问题
读取备份Key的数据 遍历之后往数据库插入完成后删除备份
2.增量回退机制 (容错处理)
redisTemplate.opsForHash().increment(currentKey, articleIdObj, count);
作用:当数据库更新失败时,将数据加回Redis
重要性:防止因数据库异常导致数据丢失
位置:在catch块中执行,确保异常时恢复数据
场景1:IP访问频率限制
@Service
public class AccessControlService {
private final RedisTemplate<String, Object> redisTemplate;
// IP访问计数Key格式:access:ip:{ip}
private static final String IP_ACCESS_KEY = "access:ip:%s";
// 黑名单Key
private static final String IP_BLACKLIST_KEY = "blacklist:ip";
public AccessControlService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 检查IP是否允许访问
* @param ip 客户端IP地址
* @return 是否允许访问
*/
public boolean isIpAllowed(String ip) {
// 1. 检查IP是否在黑名单中
if (Boolean.TRUE.equals(redisTemplate.opsForSet().
isMember(IP_BLACKLIST_KEY, ip)))
{
return false; // 黑名单IP直接拒绝
}
String key = String.format(IP_ACCESS_KEY, ip);
// 2. 获取当前访问计数
Long count = redisTemplate.opsForValue().increment(key, 1);
// 3. 如果是第一次访问,设置过期时间(1分钟)
if (count != null && count == 1) {
redisTemplate.expire(key, 1, TimeUnit.MINUTES);
}
// 4. 检查访问频率
if (count != null && count > 100) {
// 超过阈值加入黑名单(24小时)
redisTemplate.opsForSet().add(IP_BLACKLIST_KEY, ip);
redisTemplate.expire(IP_BLACKLIST_KEY, 24, TimeUnit.HOURS);
return false;
}
return true;
}
/**
* 重置IP访问限制
* @param ip 要重置的IP地址
*/
public void resetIpAccess(String ip) {
String key = String.format(IP_ACCESS_KEY, ip);
redisTemplate.delete(key);
redisTemplate.opsForSet().remove(IP_BLACKLIST_KEY, ip);
}
}
场景2:用户操作频率限制
@Service
public class UserActionLimiter {
private final RedisTemplate<String, Object> redisTemplate;
// 用户操作计数Key格式:user_action:{userId}:{actionType}
private static final String USER_ACTION_KEY = "user_action:%s:%s";
// 用户操作锁Key格式:user_action_lock:{userId}:{actionType}
private static final String USER_ACTION_LOCK_KEY = "user_action_lock:%s:%s";
public UserActionLimiter(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 检查用户是否可以执行操作
* @param userId 用户ID
* @param actionType 操作类型(如:like, comment, post等)
* @param maxAttempts 最大尝试次数
* @param lockTime 锁定时间(秒)
* @return 是否允许操作
*/
public boolean canUserAction(String userId, String actionType,
int maxAttempts, int lockTime) {
String lockKey = String.format(USER_ACTION_LOCK_KEY, userId, actionType);
// 1. 检查用户是否被锁定
if (Boolean.TRUE.equals(redisTemplate.hasKey(lockKey))) {
return false; // 用户被锁定,不允许操作
}
String countKey = String.format(USER_ACTION_KEY, userId, actionType);
// 2. 获取当前操作计数
Long count = redisTemplate.opsForValue().increment(countKey, 1);
// 3. 如果是第一次操作,设置过期时间(1分钟)
if (count != null && count == 1) {
redisTemplate.expire(countKey, 1, TimeUnit.MINUTES);
}
// 4. 检查操作次数是否超过限制
if (count != null && count > maxAttempts) {
// 超过限制,锁定用户
redisTemplate.opsForValue().set(lockKey, "locked",
lockTime, TimeUnit.SECONDS);
// 重置计数器
redisTemplate.delete(countKey);
return false;
}
return true;
}
/**
* 获取用户剩余操作次数
* @param userId 用户ID
* @param actionType 操作类型
* @return 剩余操作次数
*/
public long getRemainingActions(String userId, String actionType) {
String countKey = String.format(USER_ACTION_KEY, userId, actionType);
Long count = redisTemplate.opsForValue().increment(countKey, 0); // 获取当前值但不增加
if (count == null) {
return 10; // 默认返回最大次数
}
return Math.max(0, 10 - count); // 假设最大次数为10
}
}
譬如一个手机号一天限制发送5条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。使用Redis的Incr方法实现
以一个接口一天限制调用10次为例
//是否拒绝服务
private boolean denialOfService(String userId){
long count=
JedisUtil.setIncr(
DateUtil.getDate()+"&"+userId+"&"+"queryCarViolation", 86400);
if(count<=10){
return false;
}
return true;
}
在Redis中bitcount命令来统计二进制位中1的个数,是处理大规模二进制数据统计的理想工具,
BITCOUNT key [start end [BYTE | BIT]]
特别适用于:
1.需要高效计算二进制特征数量的场景
2.内存敏感的超大规模用户系统
3.实时分析二进制状态数据
这个命令使用前 需要使用SETBIT 设置位为1的特征
用法示例
SETBIT 设置位为1
> SETBIT user:1000:active 0 1
(integer) 0
> SETBIT user:1000:active 3 1
(integer) 0
> BITCOUNT user:1000:active
(integer) 2 # 有2位被设置为1
范围统计
> SET mybitmap "\xff\xf0\x00" # 二进制: 11111111 11110000 00000000
> BITCOUNT mybitmap
(integer) 12 # 前8位全1 + 中间4位1
> BITCOUNT mybitmap 0 0 # 只统计第一个字节
(integer) 8
> BITCOUNT mybitmap 1 1 # 只统计第二个字节
(integer) 4
按位范围(Redis 7.0+)
> BITCOUNT mybitmap 0 11 BIT # 统计前12位
(integer) 12
> BITCOUNT mybitmap 8 15 BIT # 统计第9-16位
(integer) 4
这个命令可以用于各种有趣的用途,比如计算在线用户数量、统计访问IP地址等。BITCOUNT命令的时间复杂度是O(N),其中N是被统计的二进制位数。这意味着,如果你有一个非常大的位图,BITCOUNT可能会消耗相当多的时间来完成统计
public class UserActivityTracker {
private final Jedis jedis; // Redis客户端实例
private static final String KEY_PREFIX = "activity:daily:"; // 键名前缀
// 标记用户在某一天活跃
public void markUserActive(String userId, int dayOfYear) {
// 构建键名: activity:daily:年份
String key = KEY_PREFIX + Year.now().getValue();
// 使用SETBIT命令设置位图中对应天数的位为1
// dayOfYear: 一年中的第几天(1-366)
jedis.setbit(key, dayOfYear, true);
}
// 获取用户一年中的活跃天数
public long getActiveDays(String userId, int year) {
String key = KEY_PREFIX + year;
// 使用BITCOUNT统计整个位图中值为1的位的数量
return jedis.bitcount(key);
}
// 获取用户在指定日期范围内的活跃天数
public long getActiveDaysInRange(String userId, int year, int startDay, int endDay) {
String key = KEY_PREFIX + year;
// 使用BITCOUNT的字节范围参数统计部分数据
// 参数说明: startDay-起始字节索引, endDay-结束字节索引
return jedis.bitcount(key.getBytes(), startDay, endDay);
}
}
关键点
// 使用SETBIT命令设置位图中对应天数的位为1
// dayOfYear: 一年中的第几天(1-366)
jedis.setbit(key, dayOfYear, true);
import redis
# 创建Redis连接
r = redis.Redis(host='localhost', port=6379, db=0)
# 用户ID
user_id = 1001
# 用户特征字典
features = {
0: True, # 特征0: 已实名认证
1: False, # 特征1: 未绑定银行卡
2: True, # 特征2: 最近7天有登录
3: True, # 特征3: 完成新手任务
# 可扩展更多特征...
}
# 遍历特征字典,设置位图
for bit, value in features.items():
# 构建键名: user:1001:features
key = f"user:{user_id}:features"
# 使用SETBIT设置特征位
# bit: 特征索引位置, int(value): 转换为0/1值
r.setbit(key, bit, int(value))
# 计算活跃特征数量
# 使用BITCOUNT统计值为1的位的数量
active_features = r.bitcount(f"user:{user_id}:features")
print(f"用户 {user_id} 具有 {active_features} 个活跃特征")
list作为双向链表,不光可以作为队列使用。如果将它用作栈便可以成为一个公用的时间轴
1.用户时间轴
(User Timeline):显示用户自己的动态(如微博个人主页)
2.主页时间轴
(Home Timeline):显示用户关注的人的动态(如微博首页)
推模式(Fan-out-on-write)
:用户发帖时,将帖子推送到所有粉丝的主页时间轴中
优点:读操作简单快速(直接读取自己的时间轴)
缺点:发帖时开销大(尤其大V用户)
适用场景:关注数较少的普通用户
拉模式(Fan-out-on-read)
:用户读取主页时间轴时,去查询关注人的最新动态并聚合
优点:发帖操作轻量
缺点:读操作复杂,可能慢(尤其关注人多时)
适用场景:大V用户(粉丝量巨大)
混合模式
:结合推拉两种方式,例如普通用户用推模式,大V用户用拉模式
适用场景:综合解决方案
以下是在 Redis 命令行界面 (redis-cli
) 中操作不同数据结构的简单示例,展示其基本语法和样式:
# 连接到本地 Redis 服务器 (默认端口 6379)
$ redis-cli
127.0.0.1:6379>
# ---------------------- 字符串 (Strings) ----------------------
# 设置一个键 'greeting' 的值为 'Hello, Redis!'
127.0.0.1:6379> SET greeting "Hello, Redis!"
OK
# 获取键 'greeting' 的值
127.0.0.1:6379> GET greeting
"Hello, Redis!"
# 设置一个带过期时间(10秒)的键
127.0.0.1:6379> SET access_token "abc123" EX 10
OK
# ---------------------- 列表 (Lists) ----------------------
# 从左侧('L')向列表 'tasks' 插入元素
127.0.0.1:6379> LPUSH tasks "task1" "task2" "task3"
(integer) 3 # 返回当前列表长度
# 从右侧('R')取出并移除一个元素 (队列行为)
127.0.0.1:6379> RPOP tasks
"task1"
# 获取列表 'tasks' 索引 0 到 -1 (即全部) 的元素
127.0.0.1:6379> LRANGE tasks 0 -1
1) "task3" # 注意:LPUSH 是头插,所以 task3 在最左(头)
2) "task2"
# ---------------------- 集合 (Sets) ----------------------
# 向集合 'unique_tags' 添加元素 (自动去重)
127.0.0.1:6379> SADD unique_tags "redis" "database" "cache" "nosql" "redis"
(integer) 4 # 'redis' 被添加一次,返回实际添加的数量
# 检查元素是否存在
127.0.0.1:6379> SISMEMBER unique_tags "cache"
(integer) 1 # 存在返回 1
# 获取集合所有成员
127.0.0.1:6379> SMEMBERS unique_tags
1) "nosql"
2) "cache"
3) "database"
4) "redis" # 顺序是随机的,集合无序
# 计算两个集合的交集 (假设有另一个集合 'popular_tags')
127.0.0.1:6379> SADD popular_tags "redis" "python" "cloud"
(integer) 3
127.0.0.1:6379> SINTER unique_tags popular_tags
1) "redis" # 两个集合都包含 'redis'
# ---------------------- 哈希表 (Hashes) ----------------------
# 设置哈希 'user:1000' 的字段值 (存储对象)
127.0.0.1:6379> HSET user:1000 username "alice" email "[email protected]" age 30
(integer) 3 # 设置了多少个字段
# 获取哈希 'user:1000' 的单个字段 'username' 的值
127.0.0.1:6379> HGET user:1000 username
"alice"
# 获取哈希 'user:1000' 的所有字段和值
127.0.0.1:6379> HGETALL user:1000
1) "username" # 字段名
2) "alice" # 字段值
3) "email"
4) "[email protected]"
5) "age"
6) "30"
# ---------------------- 有序集合 (Sorted Sets) ----------------------
# 向有序集合 'leaderboard' 添加成员和分数
127.0.0.1:6379> ZADD leaderboard 2500 "player1" 1800 "player2" 3200 "player3"
(integer) 3
# 按分数从低到高获取排名 (升序, 默认)
127.0.0.1:6379> ZRANGE leaderboard 0 -1 WITHSCORES
1) "player2"
2) "1800"
3) "player1"
4) "2500"
5) "player3"
6) "3200"
# 按分数从高到低获取排名 (降序, 取前2名)
127.0.0.1:6379> ZREVRANGE leaderboard 0 1 WITHSCORES
1) "player3"
2) "3200"
3) "player1"
4) "2500"
# 获取 'player3' 的排名 (按降序排名的名次, 0表示第1名)
127.0.0.1:6379> ZREVRANK leaderboard "player3"
(integer) 0
# ---------------------- 键管理 ----------------------
# 查找所有以 'user:' 开头的键 (生产环境慎用 KEYS *)
127.0.0.1:6379> KEYS user:*
1) "user:1000"
# 删除键 'greeting'
127.0.0.1:6379> DEL greeting
(integer) 1 # 删除的键数量
# 退出 redis-cli
127.0.0.1:6379> QUIT
需求:用户可以对内容(如帖子、文章)点赞,取消点赞,查询点赞数,判断是否点过赞。
方案:
1.使用 Set 存储每个内容被哪些用户点赞过(内容ID为Key,用户ID为成员)
2.如果需要记录点赞时间,则使用 ZSet(时间戳作为分数)
Redis命令示例:
# 用户1001点赞了帖子2001
SADD post:likes:2001 1001
# 用户1001取消点赞
SREM post:likes:2001 1001
# 获取帖子2001的点赞用户列表
SMEMBERS post:likes:2001
# 获取帖子2001的点赞数量
SCARD post:likes:2001
# 判断用户1001是否点赞了帖子2001
SISMEMBER post:likes:2001 1001
Spring Boot 示例:
@Component
public class LikeService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 点赞
public void like(String postId, String userId) {
redisTemplate.opsForSet().add("post:likes:" + postId, userId);
}
// 取消点赞
public void unlike(String postId, String userId) {
redisTemplate.opsForSet().remove("post:likes:" + postId, userId);
}
// 是否点赞
public boolean isLiked(String postId, String userId) {
return Boolean.TRUE.equals(redisTemplate.opsForSet().
isMember("post:likes:" + postId, userId));
}
// 获取点赞数量
public Long getLikeCount(String postId) {
return redisTemplate.opsForSet().size("post:likes:" + postId);
}
}
需求:用户每日签到,可以查看当月签到情况、连续签到天数等。
方案:
1.用 Bitmap 来存储每个用户的签到记录。每个月一个bitmap,每一位代表一天(0未签,1已签)
2.Key设计:sign:userid:yyyyMM
,例如:sign:1001:202507
Redis命令示例:
# 用户1001在2025年7月3日签到(偏移量2,因为从0开始,所以3号是2)
SETBIT sign:1001:202507 2 1
# 查看用户1001在2025年7月3日是否签到
GETBIT sign:1001:202507 2
# 统计用户1001在2025年7月签到次数
BITCOUNT sign:1001:202507
# 获取用户1001整个7月的签到情况(返回的是一个字节数组,需要客户端解析)
GET sign:1001:202507
Spring Boot 示例:
@Component
public class SignService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 用户签到(dayOfMonth: 1-31,实际存储偏移量为dayOfMonth-1)
public void sign(String userId, int year, int month, int dayOfMonth) {
String key = String.format("sign:%s:%04d%02d", userId, year, month);
int offset = dayOfMonth - 1; // 偏移量从0开始,所以日期减1
redisTemplate.opsForValue().setBit(key, offset, true);
}
// 检查某天是否签到
public Boolean checkSign(String userId, int year, int month, int dayOfMonth) {
String key = String.format("sign:%s:%04d%02d", userId, year, month);
int offset = dayOfMonth - 1;
return redisTemplate.opsForValue().getBit(key, offset);
}
// 统计某月签到次数
public Long countSigns(String userId, int year, int month) {
String key = String.format("sign:%s:%04d%02d", userId, year, month);
return (Long) redisTemplate.execute((RedisCallback<Long>) conn ->
conn.bitCount(key.getBytes())
);
}
// 获取连续签到天数(需要客户端计算)
// 注意:这里我们获取整个月的签到数据,然后从当天开始往前计算连续签到
public int getContinuousSignCount(String userId, int year, int month, int dayOfMonth) {
String key = String.format("sign:%s:%04d%02d", userId, year, month);
int offset = dayOfMonth - 1;
// 获取从0到当前日期的所有位
byte[] bytes = (byte[]) redisTemplate.execute((RedisCallback<byte[]>) conn ->
conn.getRange(key.getBytes(), 0, offset)
);
// 从当前位开始往前数连续1的个数
int count = 0;
for (int i = offset; i >= 0; i--) {
// 计算在字节数组中的位置
int byteIndex = i / 8;
int bitIndex = i % 8;
byte b = bytes[byteIndex];
if (((b >> bitIndex) & 1) == 1) {
count++;
} else {
break;
}
}
return count;
}
}
需求:用户每天可以多次打卡(如上班打卡、下班打卡),需要记录打卡时间,查询打卡记录。
方案:
1.使用 Hash 存储每日打卡记录(Key为打卡日期,field为打卡类型,value为时间戳)。
2.或者使用 ZSet 存储每个用户的打卡记录(成员为打卡类型+时间戳,分数为时间戳)。
示例1:Hash(适合一天内多次不同类型的打卡)
# 用户1001在2025-07-03 09:00:00上班打卡
HSET punch:1001:20250703 type1 "2025-07-03 09:00:00"
# 用户1001在2025-07-03 18:00:00下班打卡
HSET punch:1001:20250703 type2 "2025-07-03 18:00:00"
# 获取用户1001在2025-07-03的所有打卡记录
HGETALL punch:1001:20250703
示例2:ZSet(适合按时间排序的打卡记录)
# 用户1001在2025-07-03 09:00:00上班打卡(时间戳作为分数)
ZADD punch:1001 1720000000 "上班打卡:2025-07-03 09:00:00"
# 用户1001在2025-07-03 18:00:00下班打卡
ZADD punch:1001 1720044000 "下班打卡:2025-07-03 18:00:00"
# 获取用户1001某天的打卡记录(通过分数范围)
ZRANGEBYSCORE punch:1001 1720000000 1720086400
Spring Boot 示例(使用ZSet):
@Component
public class PunchService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 打卡
public void punch(String userId, String punchType, long timestamp) {
// 实际中可以用更紧凑的格式
String record = punchType + ":" + new Date(timestamp).toString();
redisTemplate.opsForZSet().add("punch:" + userId, record, timestamp);
}
// 获取某用户某时间段的打卡记录
public Set<String> getPunchRecords(String userId, long startTime, long endTime) {
return redisTemplate.opsForZSet().rangeByScore("punch:" + userId,
startTime, endTime);
}
}
业务需求关键点
1.参与抽奖的用户如何记录(防止重复抽奖)
2.如何存储奖品信息(包括奖品名称、数量等)
3.抽奖过程(随机抽取,支持不同的抽奖策略)
4.中奖结果记录
抽奖的场景分为即时开奖(大乐透)
,定时开奖(双色球)
,排名抽奖(投票)
利用set结构的无序性,通过Redis Spop 命令用于移除集合中的指定 key 的一个或多个随机元素,移除后会返回移除的元素
Redis基础命令
操作 | 命令示例 | 说明 |
---|---|---|
创建活动 | HSET lottery:event:1001 ... |
设置活动参数 |
添加奖品 | ZADD lottery:prizes:1001 ... |
设置奖品概率 |
用户参与 | SADD participants:1001 user001 |
记录参与者 |
执行抽奖 | EVAL lua_script |
核心抽奖逻辑 |
查询中奖 | ZRANGE winners:1001 0 -1 |
获取中奖名单 |
1.活动配置
# 活动信息
HSET lottery:event:1001 title "618抽奖" start 1688131200 end 1688227200
# 奖品池
ZADD lottery:prizes:1001
0.1 "1:iPhone14" // 10%概率
0.3 "2:100元券" // 30%概率
0.6 "3:谢谢参与" // 60%概率
2.用户参与
// 记录参与用户(Set)
SADD lottery:participants:1001 user001
// 用户参与次数计数(Hash)
HINCRBY lottery:user_counts:1001 user001 1
3.执行抽奖(Lua脚本)
-- 抽奖核心逻辑
local prizes = redis.call('ZRANGE', 'lottery:prizes:'..eventId, 0, -1, 'WITHSCORES')
local total = 0
for i=2, #prizes, 2 do total = total + prizes[i] end
local rand = math.random() * total
local current = 0
for i=1, #prizes, 2 do
current = current + prizes[i+1]
if rand <= current then
return prizes[i] // 返回中奖奖品
end
end
4.记录结果
// 记录中奖信息
ZADD lottery:winners:1001 1688131500 "user001:1"
5.奖品发放
// 减少奖品库存
HINCRBY lottery:prize_stock:1001 "1" -1
// 活动结束前收集参与者
SADD event:1001:participants user001
// 活动结束后开奖
winners = SRANDMEMBER event:1001:participants 10
// 按用户贡献值排序
ZADD event:1001:contribution 500 user001
// 抽取前100名中的10人
top_users = ZRANGE event:1001:contribution 0 99
winners = random_select(top_users, 10)
防作弊措施
1.频率限制
// 每分钟最多5次请求
INCR rate_limit:user001
EXPIRE rate_limit:user001 60
2.设备验证
// 记录设备ID
SADD lottery:devices:1001 "device_hash"
涉及到的Redis命令
操作 | 数据结构 | 命令 |
---|---|---|
商品标签存储 | Hash | HSET/HGET |
标签反向索引 | Set | SADD/SMEMBERS |
标签热度排行 | Sorted Set | ZINCRBY/ZREVRANGE |
多标签组合查询 | Set运算 | SINTERSTORE/SUNIONSTORE |
标签自动补全 | Sorted Set | ZRANGEBYLEX |
核心数据结构展示
# 商品标签映射 (Hash)
HSET item:1001 tags "新品,热销,电子产品"
# 标签反向索引 (Set)
SADD tag:新品 items 1001 1002 1003
SADD tag:热销 items 1005 1001 1007
SADD tag:电子产品 items 1001 1008 1010
# 标签热度 (Sorted Set)
ZINCRBY tag:hotness 1 "新品"
ZINCRBY tag:hotness 1 "热销"
关键操作
1.打标签
function add_tags(item_id, tags):
# 存储商品标签
redis.hset(f"item:{item_id}", "tags", tags.join(","))
# 更新标签索引
for tag in tags:
redis.sadd(f"tag:{tag}:items", item_id)
redis.zincrby("tag:hotness", 1, tag)
2.按标签查询商品
function get_items_by_tag(tag):
# 获取标签对应的所有商品ID
item_ids = redis.smembers(f"tag:{tag}:items")
return get_item_details(item_ids)
3.多标签组合查询
function get_items_by_tags(tags, operator="AND"):
# 创建临时键
temp_key = "temp:intersection"
if operator == "AND":
# 求交集 (同时包含所有标签)
redis.sinterstore(temp_key, [f"tag:{tag}:items" for tag in tags])
else: # OR
# 求并集 (包含任意标签)
redis.sunionstore(temp_key, [f"tag:{tag}:items" for tag in tags])
# 获取结果
item_ids = redis.smembers(temp_key)
redis.delete(temp_key)
return get_item_details(item_ids)
4.获取商品标签
function get_item_tags(item_id):
tags_str = redis.hget(f"item:{item_id}", "tags")
return tags_str.split(",") if tags_str else []
5.热门标签排行
function get_hot_tags(limit=10):
return redis.zrevrange("tag:hotness", 0, limit-1, "WITHSCORES")
应用场景示例
1.商品管理后台
# 添加新商品时打标签
add_tags(2001, ["新品", "限时优惠", "家居用品"])
# 修改商品标签
remove_tags(1001, ["热销"]) # 先移除
add_tags(1001, ["清仓"]) # 再添加
2.商品筛选页面
# 用户选择"电子产品"和"促销"标签
items = get_items_by_tags(["电子产品", "促销"], "AND")
# 显示热门标签
hot_tags = get_hot_tags(5)
3.相关商品推荐
function get_related_items(item_id):
# 获取当前商品标签
tags = get_item_tags(item_id)
# 随机选择一个标签
if tags:
random_tag = random.choice(tags)
return get_items_by_tag(random_tag, exclude=[item_id])
return []
优化技巧
1.标签标准化
# 存储前统一转为小写
normalized_tag = tag.strip().lower()
2.分页查询
# 使用ZSET实现分页
ZADD temp:items 0 item1 0 item2 ...
ZRANGE temp:items start end
3.标签自动补全
# 使用Sorted Set存储所有标签
ZADD all_tags 0 "新品" 0 "热销"
# 搜索时
ZRANGEBYLEX all_tags "[搜索词" "[搜索词\xff"