Redis 详细介绍

Redis

  • Redis 是什么
  • 为什么要用Redis
  • Redis 的持久化
  • Redis 数据共享分布式
  • Redis 缓存的安全性保证(分布式锁)
  • Redis 的部署模式分类
  • Redis的全局ID
  • RedisTemplate 常用方法
  • Redis的应用
    • Redis 在消息队列中应用
      • 方式一:基于 List 的队列
      • 方式二:Redis发布/订阅 (Pub/Sub)模式
        • Redis 发布消息
        • Redis 订阅消息(配置监听器)
      • 方式三:基于 Streams 的可靠队列 (Redis 5.0+)
      • 方式四:基于 ZSet 延时队列
      • 四种消息队列实现模式总结
    • Redis 计数器incr系列方法应用
      • 业务场景一示例:实时统计数
      • 业务场景二示例:拦截功能(可能是防止刷赞、刷阅读量等)
      • 业务场景三示例:限流
    • Redis 位统计 bitcount 命令
      • BITCOUNT 应用场景一:用户活跃度统计
      • BITCOUNT 应用场景二:实时特征统计
    • Redis在好友关系、用户关注、推荐模型应用
      • 时间轴(Timeline)应用 模式一:推模式
      • 时间轴(Timeline)应用 模式二:拉模式
      • 时间轴(Timeline)应用 模式二:混合模式
    • Redis 点赞功能的应用
    • Redis 签到功能的应用
    • Redis 打卡功能的应用
    • Redis 抽奖功能的应用
      • 抽奖场景一:即时开奖(如幸运大转盘)
      • 抽奖场景二:定时开奖(如双色球)
      • 抽奖场景三:排名抽奖(投票)
    • Redis 商品标签系统的应用

Redis 是什么

Redis: 全名Remote Dictionary Server,翻译叫 远程字典服务器

字典是因为存储数据是 键值对Key-Value,提供Key查找Value,类似Map,Redis (Remote Dictionary Server) 是一个开源的、基于内存的 键值对存储系统

既然是系统那就是可以部署服务,这个系统部署完了就是一个服务器,就是Redis服务器(相同类型的举例mysql,Oracle也都是这样),这个服务器里的数据都是键值对这样的,就是Redis数据结构

而叫远程服务器 ,是因为,比如我们部署一个服务系统A,服务器系统A想要Redis数据调用,而这个Redis数据来源是从Redis服务器来的,这种跨服务系统获取到的流程,是远程服务,但是注意啊:这个跨系统的调用不是直接从服务系统A调用的Redis服务系统的,往下看

为什么要用Redis

redis最大特性就是Redis的数据主要存储在内存(RAM)中,比如我们部署一个服务系统运行(服务系统运行是在内存中),想要获取一个oracle表中的数据是流程 是先从数据库找到--然后系统服务读取到转换存入内存中,如果数据量大 或者这个 表体量大 或者处理逻辑复制都需要时间

而从Redis数据获取直接拿到存储在内存中的值 通常达到微秒级别,比如部署服务系统A启动之后会读取Redis服务器里 将Redis数据结构缓存在内存中

服务系统A启动
读取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的 应用场景上可以作为 : 内存数据库、缓存、消息代理和流处理引擎

Redis 的持久化

我们按照部署测试一个单机服务系统时候,里面一般会带有kafka,Redis, 一个主流数据库如mysql,还有es之类的,这里单独针对Redis有个问题

既然Redis是数据结构是直接存储在内存中的,那重启服务系统时候内存会清除重置,那存储在内存中的Redis数据是不是没有了?

答案是 会,当重启服务时候所有在内存中的数据都会清除 自然包括内存中的Redis数据结构,但是注意是清除掉缓存在内存中的Redis数据,但不会清除掉Redis服务器里面的数据(两个独立的系统)

服务系统A启动
读取Redis服务器中的数据
重启服务器A
存储到内存

而重启后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 数据共享分布式

Redis 是独立服务,可以在多个应用之间共享

Redis服务器
应用服务器A
应用服务器B
应用服务器C

例如:分布式Session

<dependency> 
 <groupId>org.springframework.session</groupId> 
 <artifactId>spring-session-data-redis</artifactId> 
</dependency>

Redis 缓存的安全性保证(分布式锁)

既然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服务器不能支撑,那两个三个

这种通过引入多个服务器(多个节点)分别部署redis-server进程,就是Redis的分布式系统部署

Redis服务器1
应用服务器A
应用服务器B
应用服务器C
Redis服务器2

问题又来了,读取的性能问题解决了,那写入的安全问题呢

//同时操作
服务器A 写入Redis1:jedis.set("user:1000:name", "Alice");  // 存储
服务器B 写入Redis2:jedis.set("user:1000:name", "Alice");  // 存储

主从模式
1.一个Redis实例作为主机,主机支持数据的写入和读取等各项操作
2.其余的实例作为备份机,数据从主机备份来,从机只能读取不能写入

Redis主
Redis从1
Redis从2
Redis从3

优点:读写分离,提高服务器性能同时保证缓存并发安全性
缺点
1.主节点故障问题:一旦Master节点由于故障不能提供服务,需要人工将Slave节点晋升为Master节点,在这个人工切换等待过程中 很容易丢失数据
2.对于写入数据而言是个单机,如写入数据量特别大一个主是存储不了

针对第一个问题,衍生出了哨兵模式

Redis主
Redis哨兵1
Redis哨兵2
Redis哨兵3

它是主从复制模式的进阶版本,当主节点故障或者别的原因不可用适合,会自动从哨兵点里面选择一个作为新的主节点,并让其它的从节点从新的主节点复制数据,哨兵模式解决了主从模式中需要人工干预进行切换的问题

但是这种主从复制模式或者进化后的哨兵模式 解决了频繁读取性能问题,但是写入场景而言 还是单机啊,如果写入数据量大的情况下,一个主机存储有压力,这种主从复制模式适合 读多写少 的场景,而且两种

为了解决这个问题 又引入了Redis集群部署的模式
Redis集群模式主要由两部分组成:集群客户端集群服务器

Redis客户端
Redis服务器1
Redis服务器哨兵2
Redis服务器哨兵3

集群客户端:也叫 分片机,采用一致性hash算法将数据分为n个槽(这点类似hashMap的哈希桶),通俗的讲 地盘划分

数据用hash算法计算,每个槽对应一定范围的数据,不同的节点负责处理不同的槽,比如
槽1:0-100000
槽2:100001- 20000
......

集群服务器:集群服务器采用了多主复制方式,就是每个Redis服务节点(真正读取和写入redis数据的节点),每个节点都是一个主节点,并且同时可以充当其他节点的从节点

当写入时候,根据hash算法计算应该写入到哪个节点,从而保证并发安全性
当读取时候,根据负载均衡选择其中任一节点查询

也就是说每个节点: 一定范围的写入权限 + 从其他节点复制来的可被读取的数据
每个节点既是其他节点的主节点(在数据属于它管理范围 它是主)
又是其他节点的从节点在数据 不属于它管理范围 它是从)

当其中一个节点宕机了,会从其余的节点选择一个接替自己,那么客户端会调整分片将槽加给它

同事离职了,领导让你接手他的活,你就变成干两个人的活了

这就是Redis集群提供了一种自动数据迁移机制
当一个节点加入或离开集群时,会自动迁移该节点上负责的所有槽位数据到其他节点

另外Redis集群可扩展性:集群模式支持动态添加或移除节点,可以根据业务需求灵活扩展存储容量和处理能力

Redis的全局ID

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 常用方法

RedisTemplate.opsForHash()方法用于获取操作哈希数据类型的接口下辖方法
put:设置哈希字段的值
putAll:设置多个哈希字段的值
get:获取哈希字段的值
multiGet:获取多个哈希字段的值
hasKey:判断哈希中是否存在指定的字段
keys:获取哈希的所有字段
values:获取哈希的所有值
entries:获取哈希的所有字段和对应的值
increment:自增哈希字段的值
delete:删除哈希字段的值2RedisTemplate.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的应用

Redis 在消息队列中应用

方式一:基于 List 的队列

利用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);
}

方式二:Redis发布/订阅 (Pub/Sub)模式

该模式允许消息的发布者将消息发送到特定的频道,而订阅者可以监听一个或多个频道并接收消息。这种模式实现了消息的广播,适用于实时消息推送、事件通知等场景

角色 说明
发布者 (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.tempsensor.humidity

适用场景

场景 说明
实时通知 用户在线消息推送、系统报警通知
事件驱动架构 微服务间的事件通知(如订单创建触发库存更新)
聊天室/群组广播 向所有在线成员广播消息
配置更新 服务配置变更时通知所有节点刷新配置

在 Spring Data Redis 中的实现

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!");
Redis 订阅消息(配置监听器)

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();
    }
}

方式三:基于 Streams 的可靠队列 (Redis 5.0+)

特点:支持消费者组、消息持久化、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;
}

方式四:基于 ZSet 延时队列

特点:实现定时任务,有序集合按分数(时间戳)排序
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 脚本 保证原子性操作

Redis 计数器incr系列方法应用

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 命令

在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 + 中间41
> 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可能会消耗相当多的时间来完成统计

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);

BITCOUNT 应用场景二:实时特征统计

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} 个活跃特征")

Redis在好友关系、用户关注、推荐模型应用

list作为双向链表,不光可以作为队列使用。如果将它用作栈便可以成为一个公用的时间轴
1.用户时间轴(User Timeline):显示用户自己的动态(如微博个人主页)
2.主页时间轴(Home Timeline):显示用户关注的人的动态(如微博首页)

时间轴(Timeline)应用 模式一:推模式

推模式(Fan-out-on-write):用户发帖时,将帖子推送到所有粉丝的主页时间轴中
优点:读操作简单快速(直接读取自己的时间轴)
缺点:发帖时开销大(尤其大V用户)

适用场景:关注数较少的普通用户

用户A Redis 粉丝B 发布动态 存储到个人时间轴 推送到粉丝主页时间轴 loop [每个粉丝] 读取主页时间轴 直接返回预构建的时间轴 用户A Redis 粉丝B

时间轴(Timeline)应用 模式二:拉模式

拉模式(Fan-out-on-read):用户读取主页时间轴时,去查询关注人的最新动态并聚合
优点:发帖操作轻量
缺点:读操作复杂,可能慢(尤其关注人多时)

适用场景:大V用户(粉丝量巨大)

大V用户 Redis 粉丝C 发布动态 存储到个人时间轴 读取主页时间轴 拉取关注对象的最新动态 loop [每个关注对象] 聚合排序动态 返回聚合结果 大V用户 Redis 粉丝C

时间轴(Timeline)应用 模式二:混合模式

混合模式:结合推拉两种方式,例如普通用户用推模式,大V用户用拉模式

适用场景:综合解决方案

少于阈值
超过阈值
用户发帖
粉丝数量
推模式
拉模式
写入粉丝主页时间轴
仅写入个人时间轴
用户读时间轴
获取推模式数据
获取大V最新动态
合并排序返回
  • 发布/订阅 (Pub/Sub): 支持消息的发布与订阅模式。
  • 主要用途:
    • 缓存: 最常见的用途,将热点数据存储在内存中,极大减轻后端数据库压力,提升应用响应速度。
    • 会话存储 (Session Store): 存储用户会话信息,方便在分布式系统中共享。
    • 排行榜/计数器: 利用有序集合轻松实现实时排行,利用原子操作实现计数器(如点赞、浏览量)。
    • 消息队列: 利用列表或流实现简单的消息队列。
    • 实时数据处理: 利用发布/订阅或流处理实时事件(如聊天室、实时通知)。
    • 存储复杂对象: 使用哈希结构存储对象属性。
  1. 样式展示 (Redis CLI 交互示例)

以下是在 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

Redis 点赞功能的应用

需求:用户可以对内容(如帖子、文章)点赞,取消点赞,查询点赞数,判断是否点过赞。
方案
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);
    }
}

Redis 签到功能的应用

需求:用户每日签到,可以查看当月签到情况、连续签到天数等。
方案
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;
    }
}

Redis 打卡功能的应用

需求:用户每天可以多次打卡(如上班打卡、下班打卡),需要记录打卡时间,查询打卡记录。
方案
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);
    }
}

Redis 抽奖功能的应用

业务需求关键点
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 商品标签系统的应用

涉及到的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"

你可能感兴趣的:(redis,数据库,缓存)