🔥个人主页: 中草药


 一、认识Redis

Redis(Remote Dictionary Server)是一个开源的、基于内存的键值对存储数据库,支持持久化、网络化访问,并提供多种数据结构操作,用作数据缓存。它由Salvatore Sanfilippo开发,凭借其单线程模型高效数据结构,成为高并发场景下的首选解决方案。

核心特性:

  1. 内存存储:数据主要存储在内存中(内存为主,硬盘为辅),读写速度远超传统磁盘数据库,如MySQL(最大特点快)。

  2. 丰富的数据结构:支持String、List、Hash、Set、Zset五种基础类型,以及BitMap、HyperLogLog等扩展类型。

  3. 持久化机制:提供RDB(快照)和AOF(追加日志)两种方式,确保数据安全。

  4. 高可用与扩展:支持主从复制、哨兵监控、集群分片,满足分布式需求。


二、Redis的安装与基础使用

Linux安装步骤

Ubuntu

apt search redis
apt install redis

这里的127.0.0.1的ip意味着只能由当前主机上的客户端访问,无法跨主机访问,我们要手动修改

修改127.0.0.1 成 0.0.0.0

修改protected-mode yes 成 no

修改完成后,重新启动服务器

service redis-server restart

用客户端连接服务器 redis-cli

至此Ubuntu上连接完成

Centos

如果是Centos8,yum仓库中默认的redis版本就是5,直接yum install即可

如果是Centos7,yum仓库中默认的redis版本就是3,版本较旧,此时需要我们安装额外的软件源

yum install centos-release-scl-rh
yum install rh-redis5-redis

建立符号链接

cd /usr/bin
ln -s /opt/rh/rh-redis5/root/usr/bin/redis-cli ./redis-cli
ln -s /opt/rh/rh-redis5/root/usr/bin/redis-sentinel ./redis-sentinel
ln -s /opt/rh/rh-redis5/root/usr/bin/redis-server ./redis-server

cd /etc/
ln -s /etc/opt/rh/rh-redis5/ ./redis

设置工作目录 

mkdir -p /var/lib/redis

设置日志目录

mkdir -p /var/log/redis/

将二进制字节对应到汉字 redis-cli raw

通用命令使用

redis命令很多可以参考-Redis官网 不区分大小写

1、set 设置key和value (set key value [expiration EX seconds | PX milliseconds] [NX|XX]

        NX 如果key不存在才设置,存在则返回nil

        XX 如果key存在才设置,不存在返回nil 

2、get 根据key获取value

        get仅支持字符串类型的value


3、keys 用来查询服务器上匹配的key,通过一些通配符来描述key的模样,匹配上述摸样的key

通配符

  • ? 匹配任意一个字符
  • *   匹配0个或多个字符
  • [ae] 只能匹配ae字符
  • [^a} 排除a字符
  • [a-b] a~b范围区间的字符,包含边界

举例

h?llo 匹配 hello , hallo hxllo

h*llo 匹配 hllo heeeello

h[ae]llo 匹配 hello hallo 但不匹配 hillo

h[^e]llo 匹配 hallo , hbllo , ... 但不匹配 hello

h[a-b]llo 匹配 hallo hbllo

keys命令的时间复杂度是O(n),因此在生成环境上,一般是禁止keys命令,尤其是keys *-查询Redis中所有的key,Redis是一个单线程服务器,防止造成阻塞,无法对其他客户端提供服务


4、exists 判断key是否存在 ( 时间复杂度0(1) ) 

5、del 删除key  ( 时间复杂度0(1) )

6、expire key 设置过期时间 时间单位 秒s  (pexpire ms)

7、ttl 查询过期时间还剩多少 (time to live) -1 表示没有关联过期时间  -2 表示key不存在

8、type 返回key对应的数据类型

数据库管理

在redis之中也有database这样的概念,只不过不像mysql那样,redis中的数据库是现成的,用户不能创建新的数据库,也不能删除已有的数据库,默认的Redis提供了16个数据库,0-15,这16个数据库中的数据是隔离的,相互之间不会产生影响,默认使用的数据库是0号

select dbIndex 切换数据库

flushdb 删除当前数据库中的所有key

flushall 删除所有数据库中的所有key


Redis的高性能秘密

1. 单线程模型

  • 优势:避免多线程竞争和锁开销,通过I/O多路复用(如epoll)处理并发连接10。

  • 瓶颈:CPU密集型操作可能阻塞线程,需避免长耗时命令(如KEYS *)8。

2. 持久化机制

  • RDB:定时生成快照,适合备份和恢复,但可能丢失最后一次快照后的数据4。

  • AOF:记录所有写操作日志,支持每秒同步(appendfsync everysec),数据安全性更高8。

Redis虽然是单线程模型,为啥效率这么高,速度这么快呢?

这个块是相对的,参照物是数据库

1. 基于内存操作,读写速度极快

        Redis 所有数据存储在内存中(内存读写速度比磁盘快几个数量级),避免了传统数据库频繁磁盘 I/O 导致的性能瓶颈。

2、redis的核心功能相较于数据库(MySQL)更简单 

        数据库对于数据的增删改查都有更复杂的功能支持,这些功能会花费更多的开销,redis提供的功能相较于数据库更少

3. 单线程避免了多线程的锁竞争和上下文切换

  • 无锁设计:单线程无需处理多线程间的锁竞争问题,减少了复杂锁机制带来的性能损耗。
  • 上下文切换成本低:单线程模型只需处理一个任务队列,避免了多线程频繁切换上下文(Context Switching)的开销。

4. I/O 多路复用与非阻塞模型

Redis 通过 I/O 多路复用(如 Linux 的 epoll)实现高效的网络事件监听机制:

  • 单线程监听多个连接:主线程通过一个事件循环(Event Loop)同时监听大量客户端的连接请求和命令操作。
  • 非阻塞 I/O:读写网络数据时采用非阻塞模式,避免因某个客户端响应慢而阻塞整个进程。

这种模型将 CPU 密集任务(命令处理)与 I/O 密集任务(网络通信)解耦,最大化主线程的执行效率。

5. 高效数据结构与优化

Redis 内置多种高性能数据结构(如简单动态字符串、跳跃表、压缩列表等),通过算法优化减少操作的时间复杂度:

  • 例如:哈希表使用渐进式 Rehash 避免迁移时的卡顿;有序集合结合哈希表与跳表实现 O(1) 查找和 O(logN) 范围查询。

//6. 多线程的辅助优化(Redis 6.0+)

虽然主线程仍是单线程,但 Redis 6.0 后引入了多线程处理网络 I/O(如接收请求、协议解析)等辅助任务,进一步提升高并发下的吞吐量。

三、redis中key的过期策略

Redis 中 Key 的过期策略是为了自动清理不再需要的键,避免内存浪费。其核心机制包括 惰性删除 和 定期删除,同时结合 内存淘汰策略 应对极端情况。

过期策略的核心机制

1. 惰性删除 (Lazy Expiration)

  • 原理:当客户端尝试访问一个 Key 时,Redis 会先检查该 Key 是否已过期。若过期则立即删除,返回 nil;否则正常返回数据。

  • 优点:对 CPU 友好,只在访问时触发删除操作,无额外资源消耗。

  • 缺点:若大量 Key 过期后未被访问,会长期占用内存(内存泄漏风险)。

2. 定期删除 (Periodic Expiration)

  • 原理:Redis 每隔一段时间(默认每秒 10 次)随机抽取部分设置了过期时间的 Key,删除其中已过期的。具体流程:

    1. 从过期字典中随机选择 20 个 Key。

    2. 删除其中已过期的 Key。

    3. 若超过 25% 的 Key 已过期,则重复该过程(最多持续 25ms,避免阻塞)。

  • 优点:通过抽样减少遍历开销,平衡内存和 CPU 使用。

  • 缺点:无法完全实时删除所有过期 Key,需依赖惰性删除兜底。

内存淘汰策略(补充机制)

当内存不足时(maxmemory 限制被触发),Redis 会根据配置的策略删除 Key,即使它们未过期。常见策略包括:

  • noeviction(默认):拒绝写入新数据,读请求正常。

  • volatile-ttl:优先删除剩余存活时间(TTL)最短的 Key。

  • volatile-lru:从过期 Key 中删除最近最少使用(LRU)的 Key。

  • volatile-lfu:从过期 Key 中删除最不频繁使用(LFU)的 Key。

  • allkeys-lru:从所有 Key 中删除 LRU 的 Key。

  • allkeys-lfu:从所有 Key 中删除 LFU 的 Key。

  • volatile-random / allkeys-random:随机删除过期或所有 Key。

四、数据结构

Redis 对外暴露的数据结构(API)包括:

  • String(字符串)

  • List(列表)

  • Hash(哈希表)

  • Set(集合)

  • Sorted Set(有序集合)

  • Bitmaps(位图)

  • HyperLogLog(基数统计)

  • Stream(流,用于消息队列)

  • Geospatial(地理空间索引)

但底层实现更为复杂,Redis 通过不同的编码(encoding)优化内存和性能。


String(字符串)

  • 用途:缓存、计数器、分布式锁、二进制存储(如图片)。

  • 底层实现

    • int:(8个字节)整数时直接存储为长整型,redis有时可以用来实现“计数”等功能

    • embstr:短字符串(≤39字节)使用连续内存的紧凑结构。

    • raw:(大于39)长字符串使用动态字符串(SDS,Simple Dynamic String)。

  • 特点

    • 最大支持 512MB

    • SDS 支持高效追加操作(O(1) 时间复杂度),预分配内存减少碎片。

命令

命令 执行效果 时间复杂度
set key value 设置 key 的值是 value O(k), k 是键个数
get key 获取 key 的值 O(1)
del key [key ...] 删除指定的 key O(k), k 是键个数
mset key value [key value ...] 批量设置指定的 key 和 value O(k), k 是键个数
mget key [key ...] 批量获取 key 的值 O(k), k 是键个数
incr key 指定的 key 的值 +1 O(1)
decr key 指定的 key 的值 -1 O(1)
incrby key n 指定的 key 的值 +n O(1)
decrby key n 指定的 key 的值 -n O(1)
incrbyfloat key n 指定的 key 的值 +n O(1)
append key value 指定的 key 的值追加 value O(1)
strlen key 获取指定 key 的值的长度 O(1)
setrange key offset value 覆盖指定 key 的从 offset 开始的部分值,如果是不存在的字节则会填充为0x00 O(n), n 是字符串长度, 通常视为 O(1)
getrange key start end 获取指定 key 的从 start 到 end 的部分值, O(n), n 是字符串长度, 通常视为 O(1)

典型应用场景

1、缓存功能

        减少数据库压力,加速数据访问。以下是Redis + MySQL 组成的缓存存储架构

        应用服务器在访问数据的时候,先查询Redis,如果Redis上数据存在,直接从Redis上读取数据交给应用服务器,如果不存在再读取MySQL,并同时将数据也写入Redis,Redis这样的缓存,经常用来存储“热点”数据。

        此时,随着数据的范根,Redis上的key越来越多,这里主要有两方面策略,1、设置过期策略2、Redis在内存不足的时候,提供了淘汰策略(后面会有介绍)

        与 MySQL 等关系型数据库不同的是,Redis 没有表、字段这种命名空间,而且也没有对键名有强制要求(除了不能使用一些特殊字符)。但设计合理的键名,有利于防止键冲突和项目的可维护性,比较推荐的方式是使用 "业务名:对象名:唯⼀标识:属性" 作为键名。
        例如 MySQL 的数据库名为 vs,用户表名为 user_info,那么对应的键可以使
"vs:user_info:6379"、"vs:user_info:6379:name" 来标识,如果当前 Redis 只会被⼀个业务
使用,可以省略业务名 "vs:"。如果键名过程,则可以使用团队内部都认同的缩写替代,例如
"user:6379:friends:messages:5217" 可以被 "u:6379:fr:m:5217" 代替。毕竟键名过长,还
是会导致 Redis 的性能明显下降的。

2、计数功能

        实时统计高频操作(如点赞、浏览、转发),替代数据库的直接计数。

 Redis并不擅长数据统计,比如统计播放量前100的视频有那些,基于Redis就会很麻烦。并且实际上要开发一个成熟,稳定的真实计数系统,要面临的挑战远不止这么简单:防作弊、按照不同维度计数,避免单点问题,数据持久化到底层数据源等

3、共享会话

存储用户登录态,实现分布式系统的会话共享。

        如下图所示,⼀个分布式 Web 服务将用户的 Session 信息(例如用户登录信息)保存在各自的服务器中,但这样会造成⼀个问题:出于负载均衡的考虑,分布式服务会将用户的访问请求均衡到不同的服务器上,并且通常无法保证用户每次请求都会被均衡到同⼀台服务器上,这样当用户刷新一次访问是可能会发现需要重新登录,这个问题是用户无法容忍的。

 我们可以用Redis集中管理session

4、手机验证码功能

临时存储验证码(短信、邮箱)、限制验证频率。

        很多应用出于安全考虑,会在每次进行登录时,让用户输入手机号并且配合给手机发送验证码,然后让用户再次输入收到的验证码并进行验证,从而确定是否是用户本人。为了短信接口不会频繁访问,会限制用户每分钟获取验证码的频率,例如一分钟不能超过5次。

像发送短信这样的操作,都有专门的SDK来实现(第三方提供的短信平台服务)

Hash(哈希表)

  • 用途:存储对象属性(如用户信息)、字段级更新。

  • 底层实现

    • ziplist(小哈希):当哈希中的元素个数小于hash-max-ziplist-entries配置(默认512个),且同时所有value的值都小于hash-max-ziplist-value配置(默认64字节)时,才会使用zipList作为内部实现,ziplist使用更加紧凑的结构实现 字段和值交替存储为连续内存,压缩列表,节省空间。

    • hashtable(大哈希):使用字典(数组 + 链表)实现,支持 O(1) 查找。

  • 特点

    • 单个 Hash 可存储 2^32 -1 个键值对。

    • 支持原子性字段操作(如 HINCRBY)。

哈希类型中的映射关系通常称为field-value,用于区分Redis整体的键值对(key-value),注意这里的value在不同上下文的作用

命令 

命令 执行效果 时间复杂度
hset key field value 设置值(可批量) O(1)
hget key field 获取值 O(1)
hdel key field [field ...] 删除 field (可批量) O(k), k 是 field 个数
hlen key 计算 field 个数 O(1)
hgetall key 获取所有的 field-value O(k), k 是 field 个数
hmget field [field ...] 批量获取 field-value O(k), k 是 field 个数
hmset field value [field value ...] 批量获取 field-value O(k), k 是 field 个数
hexists key field 判断 field 是否存在 O(1)
hkeys key 获取所有的 field O(k), k 是 field 个数
hvals key 获取所有的 value O(k), k 是 field 个数
hsetnx key field value 设置值,但必须在 field 不存在时才能设置成功 O(1)
hincrby key field n 对应 field-value +n O(1)
hincrbyfloat key field n 对应 field-value +n O(1)
hstrlen key field 计算 value 的字符串长度

O(1)

基本上和String类型的操作差不多,只是多了个h 

典型应用场景 

1、作为缓存

存储结构化的数据,使用hash类型更合适一点

相比于使用Json格式的字符串去缓存用户信息。哈希类型变得更加直观,并且在更新操作上变得更加灵活。

关系型数据库

uid name age city gender favor
1 James <null> Beijing <null> sports
2 Johnathan 30 <null> male <null>

哈希型

user1---> uid 1 user:2---> uid 2
name james name johnathan
city Beijing age 30
favor sports gender male

哈希类型是稀疏的,而关系型数据库是完全结构化的。例如,哈希类型每个键可以有不同的 field,而关系型数据库一旦添加新的列,所有行都要为其设置值,即使为 null。

关系数据库可以做复杂的关系查询,而 Redis 去模拟关系型复杂查询,例如联表查询、聚合查询等基本不可能,维护成本高。

List(列表)

  • 用途:消息队列、最新消息排行、阻塞操作。

  • 底层实现

    • QuickList(Redis 3.2+):由多个 listpack(紧凑列表)通过双向链表连接,平衡内存和访问效率。

    • 旧版本使用 ziplist(压缩列表)+ LInkedList双向链表。

  • 特点

    • 支持双向操作(LPUSH/RPUSHLPOP/RPOP)。

    • 可通过 BLPOP/BRPOP 实现阻塞式读取。

    • 列表中的元素是有序的,这意味着可以通过索引下标获取某个元素或者某个范围的元素列表。

    • 列表的元素是允许重复的

list的编码方式并非是一个简单数组,而是更接近于 双端队列(deque) 

列表的获取删除操作

命令

操作类型 命令 作用 时间复杂度
添加 rpush key value [value ...] 右侧添加 O (k),k 是元素个数
lpush key value [value ...] 左侧添加 O (k),k 是元素个数
linsert key before | after pivot value 在某个元素的前/后添加 O (n),n 是pivot距离头尾的距离,从左往右
查找 lrange key start end 0 -1实现全部查找 O (s+n),s 是 start 偏移量,n 是 start 到 end 的范围
lindex key index 根据下标查找 O (n),n 是索引的偏移量
llen key 返回长度 O(1)
删除 lpop key 左侧pop O(1)
rpop key 右侧pop O(1)
lrem key count value 删count个元素value O (k),k 是元素个数
ltrim key start end 保留区间 O (k),k 是元素个数
修改 lset key index value 必须key存在,下标不能越界 O (n),n 是索引的偏移量
阻塞操作 blpop brpop key timeout 可以设置阻塞时间。返回的结果是一个二元组,包括那个key什么value O(1)

blpop 和 brpop 是 lpOP 和 rpop 的阻塞版本,与对应非阻塞版本作用基本一致,区别如下:

 
  • 列表有元素时,阻塞和非阻塞表现一致。列表无元素时,非阻塞版本返回 nil,阻塞版本根据 timeout 阻塞一段时间,期间 Redis 可执行其他命令,但执行该命令的客户端会处于阻塞状态。
  • 命令设置多个键时,会从左向右遍历键,一旦某个键对应的列表能弹出元素,命令立即返回。
  • 多个客户端同时对一个键执行 pop,最先执行命令的客户端获得弹出元素 。

此命令主要是用“消息队列”,但功能比较局限

典型应用场景 

1、作为消息队列

生产者消费者模型

Redis 分频道 阻塞消息队列模型

不同的频道负责不同的数据,实现了数据的解耦合

2、微博 TimeLine

每个用户都有属于自己的 Timeline(微博列表),现需要分页展示文章列表。此时可以考虑使用列表,因为列表不但是有序的,同时支持按照索引范围获取元素。

此方案在实际中可能存在两个问题:

        1 + n 问题。即如果每次分页获取的微博个数较多,需要执行多次 hgetall 操作,此时可以考虑使用 pipeline(流水线)模式批量提交命令,或者微博不采用哈希类型,而是使用序列化的字符串类型,使用 mget 获取。

        分裂获取文章时,lrange 在列表两端表现较好,获取列表中间的元素表现较差,此时可以考虑将列表做拆分。

💡 选择列表类型时,请参考:
同侧存取(lpush + lpop 或者 rpush + rpop)为栈
异侧存取(lpush + rpop 或者 rpush + lpop)为队列

Set(集合)

  • 用途:标签系统、唯一性校验、集合运算(并集、交集)。

  • 底层实现

    • intset(整数集合):元素均为整数时,使用有序数组存储。

    • hashtable(哈希表):字符串等非整数或元素较多时,用字典存储(值为 NULL)。

  • 特点

    • 无序且元素唯一(可以和list对比记忆)。

    • 支持 SINTER(交集)、SUNION(并集)等集合运算。

命令

命令 功能 时间复杂度
sadd key element [element ...] 添加元素 O (k),k 是元素个数
srem key element [element ...] 删除元素 O (k),k 是元素个数
scard key 获取元素个数 O(1)
sismember key element 判断元素是否在集合中 O(1)
srandmember key [count] 随机取出元素 O (n),n 是 count
spop key [count] 随机删除元素 O (n), n 是 count
smembers key 查找集合中所有元素 O (k),k 是元素个数
sinter key [key ...] sinterstore 求交集 到新的集合 O (m * k),k 是几个集合中元素最小的个数,m 是键个数
sunion key [key ...] sunionstore 求并集 O (k),k 是多个集合的元素个数总和
sdiff key [key ...] sdiffstore 求差集 O (k),k 是多个集合的元素个数总和
smove source destination member 将source集合中的元素member移动到destination O(1)

典型应用场景 

 1、使用set来保存用户的“标签”

set方便计算交集

根据用户的历史行为,个人数据,分析出个人的特征,然后提炼为用户的标签,生成用户画像来实现个性化的内容推荐(例如 抖音)

用户画像(User Profile)是通过收集、分析用户的多维度数据,抽象出的一个虚拟的“用户模型”。它用于精准描述用户特征、行为习惯和潜在需求,是推荐系统、广告投放、精准营销等场景的核心基础。

2、用set求用户之间的共同好友 

基于  集合求交集  能实现得出不同用户之间的共同好友,因此拓展后续的推荐好友等功能

3、使用set求UV

UV(Unique Visitor,独立访客)是互联网领域的一个关键指标,用于衡量在一定时间内访问某个网站、应用或页面的唯一用户数量。它与PV(Page View,页面浏览量)共同构成流量分析的基础。

指标 定义 用途 示例
UV 独立用户数(去重) 衡量用户规模与覆盖广度 1天内1000人访问网站,UV=1000
PV 页面被浏览的总次数(不去重) 衡量内容热度或用户活跃度 同一用户刷新5次页面,PV=5
会话 用户单次访问的连续操作集合 分析用户单次访问的深度与行为路径 用户打开APP后浏览3页后退出,会话数=1

Zset (Sorted Set有序集合)

  • 用途:排行榜、带权重的队列、范围查询。

  • 底层实现

    • 跳表(SkipList) + 哈希表

      • 跳表:支持 O(logN) 范围查询和排序。

      • 哈希表:存储成员到分值的映射,实现 O(1) 分值查询。

    • 小规模数据可能使用 ziplist 存储(按分值排序)。

  • 特点

    • 元素唯一,给每一个元素(member)同时引入一个属性---分值(score),按分值排序。

    • 分数相同,根据元素自身字符串的字典序来排列

    • 支持 ZRANGEBYSCOREZRANK 等范围操作。

命令

命令 功能 时间复杂度
zadd key score member [score member ...]

默认返回之后为新增的个数

XX:存在时修改

NX:不存在时修改

LT:新分数比之前分数少更新

GT:新分数比之前分数高更新

CH:影响返回值,包括修改的分数

INCR:实现运算

O (k * log (n)),k 是添加成员的个数,n 是当前有序集合的元素个数
zcard key 获取元素个数 O(1)
zscore key member 获取元素分值 O(1)
zrank key member
zrevrank key member
获取指定元素的排名 O (log (n)),n 是当前有序集合的元素个数
zrem key member [member ...] 删除元素 O (k * log (n)),k 是删除成员的个数,n 是当前有序集合的元素个数
zincrby key increment member 为指定元素添加指定的分数值 O (log (n)),n 是当前有序集合的元素个数

zrange key start end [withscores]

zrevrange key start end [withscores]

默认 升序排列 O (k + log (n)),k 是获取成员的个数,n 是当前有序集合的元素个数
zrangebyscore key min max [withscores]
zrevrangebyscore key max min [withscores]
根据分数区间来排列 默认升序 O (k + log (n)),k 是获取成员的个数,n 是当前有序集合的元素个数
zcount key min max

返回分数在min-max区间的元素个数

O (log (n)),n 是当前有序集合的元素个数

zremrangebyrank key start end 根据下标描述的范围删除 O (k + log (n)),k 是获取成员的个数,n 是当前有序集合的元素个数
zremrangebyscore key min max 根据分数描述的范围删除 O (k + log (n)),k 是获取成员的个数,n 是当前有序集合的元素个数
zinterstore destination numkeys key [key ...] 求交集 O (n * k)+O (m * log (m)),n 是输入的集合最小的元素个数,k 是集合个数,m 是目标集合元素个数
zunionstore destination numkeys key [key ...] 求并集 O (n)+O (m * log (m)),n 是输入集合总元素个数,m 是目标集合元素个数

zpopmax key [count]

zpopmin key [count]

删除最大值/最小值元素 O(log(N)*M)其中N是排序集合中的元素个数,M是弹出的元素个数。

bzpopmax key [key…] timeout

bzpopmin key [key…] timeout

zpopmax的阻塞版本 可以读取多个key O (log (n)),n 是当前有序集合的元素个数
  • zpopmax key [count] 删除最大值元素 针对这一操作,redis确实记录了尾部这样的特定位置,理论上直接操作这个特定位置,可以实现 O(1) 但是在实际删除中,并没有用上这个特性,而是直接调用了一个“通用的删除函数”----》给定一个member的值,进行查找,找到位置再进行删除
  • zcount key min max 返回分数在min-max区间的元素个数,可以用 ( 来将闭区间变为开区间,注意正确的表示方式为zcount  myzset (90 (100  且-inf 为负无穷大(无限趋近于0) inf为无限大。它的时间复杂度为O (log (n)),n 是当前有序集合的元素个数 (并不是通过遍历元素的方式去获取,而是在zset的内部,会记录每个元素的次序,然后相减得到个数)
  • zinter zunion zdiff 这几个命令从redis 6.2 开始支持的
  • ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight
      [weight ...]] [AGGREGATE <SUM | MIN | MAX>] 求交集保存到另一个集合中 numkeys 的含义是后续参与交集运算的key的个数 之所以在这个命令之中涉及到了numkeys,是因为后续的一些[WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>] 需要与其进行区分 声明那些是key(有点类似于Http协议中的请求头和正文的粘包问题)WEIGHTS 的含义是 权重,相当于一个系数,会乘以当前的分数 [AGGREGATE <SUM | MIN | MAX>] 的含义是合并方式

典型应用场景 

1、排行榜系统

排行的要求是实时更新,zset非常适合这种场景,分数的改变也可以用zincrby去修改分数,排行顺序也会随之调整。

thousand--1kb  million--1m 10亿--GB

对于游戏排行榜,前后顺序非常容易确定,但是类似于“微博热度”这种排行榜就是看的综合数值,不同纬度对应着不同的权重,根据不同的 weight权重 得到综合数值,这里就会用到zinterstore/zunionstore


特定场景的数据类型--粗略介绍 

Stream(流)

用途

  • 消息队列:类似于 Kafka 的轻量级实现,支持多消费者组、消息持久化和消息回溯。

  • 事件溯源:记录时序事件(如用户操作日志)。

  • 实时数据处理:处理实时数据流(如 IoT 设备数据)。

核心命令

  • XADD key * field1 value1 ...:添加消息(* 表示自动生成消息 ID)。

  • XREAD [BLOCK ms] STREAMS key start_id:读取消息(支持阻塞模式)。

  • XGROUP CREATE key groupname start_id:创建消费者组。

  • XACK key groupname message_id:确认消息消费。

示例

# 生产者发送消息
XADD orders * user_id 1001 product "Coffee" price 3.5

# 消费者读取消息(阻塞模式,最多等待 5000ms)
XREAD BLOCK 5000 STREAMS orders $

特点

  • 消息 ID:格式为 <时间戳>-<序列号>(如 1630454400000-0),支持按范围查询。

  • 消费者组:支持负载均衡(同一组的消费者共享消息)和消息确认机制。

  • 持久化:消息默认持久化到内存,可配置 RDB/AOF 保证可靠性。

Kafka 是一种分布式流处理平台,它被设计用于处理大规模的实时数据流,可作为消息队列或消息总线使用,在数据管道、消息传递、事件溯源和流处理等场景中广泛应用。

Geospatial(地理空间)

用途

  • LBS(基于位置的服务):如查找附近的餐厅、共享单车、打车服务。

  • 距离计算:计算两点间的距离(如配送费计算)。

  • 地理围栏:判断用户是否进入某个区域。

核心命令

  • GEOADD key longitude latitude member:添加地理位置(经度、纬度、名称)。

  • GEODIST key member1 member2 [unit]:计算两个位置的距离(单位:m/km/mi/ft)。

  • GEORADIUS key longitude latitude radius unit:查找半径内的位置。

  • GEOHASH key member:获取位置的 Geohash 编码。

示例

# 添加位置数据
GEOADD restaurants 116.404269 39.91582 "Pizza Hut"

# 查找距离用户 5km 内的餐厅
GEORADIUS restaurants 116.401245 39.913789 5 km WITHDIST

特点

  • 底层结构:基于 Sorted Set 实现,位置信息通过 Geohash 算法转换为浮点数存储为 score

  • 精度:经纬度支持 6 位小数(精度约 10cm)。

HyperLogLog(基数统计)

用途

  • 只有一个应用场景估算集合中的元素个数

  • 大数据去重统计:如统计每日 UV(set实现,需要存储每个元素,内存占用相较于HyperLogLog更大)、搜索关键词数量。

  • 低内存占用固定使用约 12KB 内存,误差率约 0.81%。

核心命令

  • PFADD key element1 element2 ...:添加元素。

  • PFCOUNT key:统计基数(不重复元素数量)。

  • PFMERGE destkey sourcekey1 sourcekey2 ...:合并多个 HyperLogLog。

示例

# 统计用户访问量
PFADD daily_uv "user1" "user2" "user3"
PFCOUNT daily_uv  # 输出 3

特点

  • 误差率:标准误差 0.81%,适合对精度要求不高的场景。

  • 不可逆:无法获取具体元素,仅统计数量。

  • 是一种思想,而非redis所独有的

Bitmaps(位图)

用途

  • 布尔标记:如用户签到、功能开关、在线状态。

  • 高效统计:统计活跃用户数(如 DAU)。

核心命令

  • SETBIT key offset value:设置位的值(0 或 1)。

  • GETBIT key offset:获取位的值。

  • BITCOUNT key [start end]:统计值为 1 的位数。

  • BITOP operation destkey key1 key2 ...:位运算(AND/OR/XOR/NOT)。

示例

# 记录用户 1001 第 7 天签到
SETBIT user:1001:sign 7 1

# 统计本月签到总天数
BITCOUNT user:1001:sign

特点

  • 空间高效:每个用户每日签到仅需 1 位,1 亿用户每月仅需约 12MB。

  • 稀疏优化:Redis 动态分配内存,稀疏位图实际占用空间更小。

  • 本质:基于 String 的位操作(如用户在线状态、签到统计),属于是Set类型针对整数的特化版本

Bitfield(位域)

用途

  • 紧凑存储数值:如存储多个小整数(如 RGB 颜色、状态标志)。

  • 原子操作:支持对指定位的自增、溢出控制。

核心命令

  • BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment]:读写或修改位域。

  • Type 格式u/i<位数>(如 u8 表示无符号 8 位整数)。

示例

# 存储用户状态:u4(权限等级) + u8(积分)
BITFIELD user:1001:flags SET u4 0 2 SET u8 4 150

# 自增用户积分(溢出时饱和)
BITFIELD user:1001:flags INCRBY u8 4 10 OVERFLOW SAT

特点

  • 灵活位操作:支持有符号/无符号整数,自定义偏移量。

  • 溢出策略:支持 WRAP(折返)、SAT(饱和)、FAIL(拒绝操作)。

  • 可以理解为一串二进制序列


选择数据结构的建议

  1. 高频更新场景

    • 计数器用 StringINCR)。

    • 对象属性更新用 HashHINCRBY)。

  2. 范围查询

    • 时间序列数据用 Sorted Set(按时间戳排序)。

    • 地理位置用 Geospatial

  3. 去重与集合运算

    • 标签系统用 Set

    • 唯一值统计用 HyperLogLog

  4. 消息队列

    • 简单队列用 List

    • 需持久化和消费者组管理用 Stream


 五、渐进式遍历 

Redis 的 渐进式遍历 是一种高效、安全的遍历大数据集的方法,旨在避免使用 KEYSSMEMBERS 等阻塞式命令导致的性能问题。它通过分批次、游标(cursor)的方式逐步遍历数据,确保在遍历过程中不会长时间阻塞 Redis 主线程,适合处理海量数据。


为什么需要渐进式遍历?

  1. 避免阻塞
    KEYS *SMEMBERS 等命令会一次性返回所有数据,如果数据量极大(如百万级),会导致 Redis 主线程长时间阻塞,影响其他请求。

  2. 动态数据集
    如果遍历过程中数据集发生变化(如新增或删除元素),传统命令可能返回不一致的结果。

  3. 内存安全
    一次性返回所有数据可能撑爆客户端内存,而渐进式遍历允许客户端分批处理。


核心命令:SCAN 系列

Redis 提供 SCAN 命令及其衍生命令,支持对键、集合、哈希、有序集合的渐进式遍历:

  • SCAN:遍历数据库中的键。

  • SSCAN:遍历集合(Set)中的元素。

  • HSCAN:遍历哈希(Hash)中的字段。

  • ZSCAN:遍历有序集合(ZSet)中的成员。


SCAN 命令详解

语法

SCAN cursor [MATCH pattern] [COUNT count] [TYPE type]
  • cursor:光标,游标,从 0 开始,遍历结束时返回 0,仅仅是一个字符串,不能和下标混为一谈。

  • MATCH pattern:按模式匹配键(如 user:*),和keys的pattern是一样的。

  • COUNT count建议每批返回的元素数量,默认是10(实际返回数量可能不同,不会差很多)。

  • TYPE type(Redis 6.0+):按数据类型过滤(如 stringhash)。

示例


关键特性

  1. 非阻塞
    每次 SCAN 只返回少量数据,避免长时间占用主线程,且遍历过程可以随时中断。

  2. 游标机制
    客户端需记录游标,直到返回 0 表示遍历完成。

  3. 弱一致性
    遍历过程中若数据发生变化(如新增或删除元素),可能会返回重复或遗漏部分数据。

  4. COUNT 参数
    COUNT 仅是一个建议值,实际返回数量可能不同(Redis 根据内部优化调整)。

  5. 时间复杂度
    每次 SCAN 的时间复杂度为 O(1),完整遍历的时间复杂度为 O(N)(与 KEYS 相同,但分摊了耗时)。


六、JavaClient

在日常开发之中命令行客户端并不是我们日常开发的主要形式,更多是用API去构建自己的客户端,客户端按照对应的应用层协议发送请求,服务器按照这个协议进行解析,再按照这个协议构造响应,客户端再进行解析

 RESPREdis Serialization Protocol

Redis 的 RESPREdis Serialization Protocol)是 Redis 客户端和服务端之间通信的核心协议。其设计目标是保持简单、高效,同时具备可读性。REST API | Docs


1. RESP 的核心设计

  • 基于 TCP:协议通过 TCP 连接传输,但是和TCP没有强耦合,支持请求-响应模型和 Pipeline 批量操作。
  • 二进制安全:允许传输任意二进制数据(比如图片、JSON 等),不依赖特殊字符终止。
  • 类型化数据结构:定义了多种简单但高效的数据类型,每个数据块以特定前缀标识类型。

2. RESP 支持的数据类型

类型 前缀字符 示例 说明
Simple Strings + +OK\r\n 用于成功响应(如OK),不含换行符,只能传输文本。
Errors - -ERR unknown command\r\n 错误信息,客户端应优先处理错误。
Integers : :123\r\n 整数,如 INCR 操作的返回值。
Bulk Strings $ $5\r\nhello\r\n 用于传输二进制安全的字符串或数据块。
Arrays * *3\r\n$3\r\nSET\r\n... 客户端请求通常以数组形式发送命令。
Null $-1\r\n $-1\r\n 表示空值(nil)。

3. 协议特点

  • 性能高效:仅通过前缀字符判断类型,解析速度快。
  • 可读性:人类可读的文本格式(非二进制协议),方便调试(如用 telnet 手动发送命令)。
  • 压缩传输:用长度字段减少冗余字符,优化大数据传输(如 Bulk Strings)。
  • 简单好实现:协议简单并不复杂

4. RESP3(Redis 6 引入的扩展协议)

  • 改进点:增加更多数据类型(如布尔值、浮点数、Map 类型等),支持客户端-服务端双向通信。
  • 兼容性:默认仍使用 RESP2,可通过 HELLO 命令切换版本。

通过理解 RESP,可以深入 Redis 的底层通信机制,优化客户端性能或定制工具。比如在 Java 中使用 Jedis 时,每次命令最终都会被编码为 RESP 格式发送给服务端。


Jedis

GitHub - redis/jedis: Redis Java client

指导手册 jedis 5.3.0-beta1 javadoc (redis.clients)

Jedis 是一个流行的 Redis 官方推荐的 Java 客户端库,用于在 Java 应用程序中与 Redis 服务器进行交互。它提供了简单且直观的 API,支持 Redis 的绝大部分命令和功能。

redis的6379端口默认是被云服务器的防火墙给保护起来的,无法外界进行连接访问,而Redis的端口一旦被公开到公网上,特别容易被入侵,为此,在不开放redis的端口且又能保证我们自己通过外网访问云服务器上的redis,我们有以下两种方式:

1、直接将相关的java程序打成jar包,然后把jar包拷贝到linux服务器上执行(可借助第三方的插件)

2、配置ssh端口转发,把云服务器的redis端口,映射到本地主机(推荐)

SSH 端口转发(Port Forwarding)是一种通过 SSH 加密隧道将网络流量从一个端口转发到另一个端口的技术,常用于突破网络限制、访问内网资源或加密通信。

先配置一下隧道

当ssh连接上了之后,端口的转发才生效

通过netstat命令检查一下

经过如此操作就可以通过访问127.0.0.1操作到云服务器的redis了(包括最开始在安装步骤中的配置绑定的ip和关闭保护模式)

测试代码

public class RedisDemo {
    public static void main(String[] args) {
        JedisPool jedisPool = new JedisPool("tcp://localhost:8888");
        try(Jedis jedis = jedisPool.getResource()) {
            //redis的各种命令就是jedis的各种方法
            System.out.println(jedis.ping());
        }
    }
}

在jedis中的方法与实际命令十分相同,这里不做多以赘述

部分操作Demo

public class RedisDemoSet {
    //sinter sunion
    public static void test2(Jedis jedis){
        jedis.flushAll();
        jedis.sadd("set1","111","222","333");
        jedis.sadd("set2","444","222","333");
        System.out.println(jedis.sinter("set1", "set2"));
        jedis.sinterstore("set3", "set1", "set2");
        System.out.println(jedis.smembers("set3"));
        System.out.println(jedis.sunion("set1", "set2"));
        jedis.sunionstore("set4", "set1", "set2");
        System.out.println(jedis.smembers("set4"));
    }

    // sadd smembers sismember spop
    public static void test1(Jedis jedis){
        jedis.flushAll();
        jedis.sadd("set","111","222","333","444","555","666");
        System.out.println(jedis.smembers("set"));
        System.out.println(jedis.sismember("set", "111"));
        System.out.println(jedis.sismember("set", "777"));
        System.out.println(jedis.spop("set"));
        System.out.println(jedis.spop("set"));
        System.out.println(jedis.spop("set"));
        System.out.println(jedis.smembers("set"));

    }

    public static void main(String[] args) {
        JedisPool jedisPool = new JedisPool("tcp://localhost:8888");
        try(Jedis jedis = jedisPool.getResource()) {
            //test1(jedis);
            test2(jedis);
        }
    }
}

RedisTemplate

Spring Data Redis

RedisTemplate 是 Spring Data Redis 提供的一个高级抽象工具,用于简化 Java 应用与 Redis 的交互。它封装了底层 Redis 客户端(如 Jedis、Lettuce)的复杂性,提供了更符合 Spring 生态的编程风格,支持自动序列化、连接管理和事务等功能。

RedisTemplate 的使用和 jedis 略微存在一些不同,它提供面向不同数据结构的 API,相当于进行了二次封装:

  • 字符串opsForValue()

  • 哈希opsForHash()

  • 列表opsForList()

  • 集合opsForSet()

  • 有序集合opsForZSet()

测试 Demo

@RestController
public class MyController {
    // StringRedisTemplate 是 RedisTemplate 的子类,专门用来处理文本类
    @Autowired
    private StringRedisTemplate redisTemplate;

    @GetMapping("/testString")
    @ResponseBody
    public String testString() {
        redisTemplate.opsForValue().set("hello", "world");
        System.out.println(redisTemplate.opsForValue().get("hello"));
        return "ok";
    }

    @GetMapping("/testList")
    @ResponseBody
    public String testList() {
        //execute的回调方法必须要有return语句
        redisTemplate.execute((RedisConnection connection) -> {
            //进行原生操作
           connection.flushAll();
           return null;
        });
        redisTemplate.opsForList().leftPush("list", "111");
        redisTemplate.opsForList().leftPush("list", "222");
        redisTemplate.opsForList().leftPush("list", "333");
        System.out.println(redisTemplate.opsForList().range("list", 0, -1));
        System.out.println(redisTemplate.opsForList().leftPop("list"));
        System.out.println(redisTemplate.opsForList().rightPop("list"));
        return "ok";
    }

    @GetMapping("/testSet")
    @ResponseBody
    public String testSet() {
        //execute的回调方法必须要有return语句
        redisTemplate.execute((RedisConnection connection) -> {
            //进行原生操作
            connection.flushAll();
            return null;
        });
        redisTemplate.opsForSet().add("set", "aaa");
        redisTemplate.opsForSet().add("set", "bbb");
        redisTemplate.opsForSet().add("set", "ccc");
        System.out.println("set中的元素有:"+redisTemplate.opsForSet().size("set")+"个");
        System.out.println(redisTemplate.opsForSet().members("set"));
        System.out.println("pop出:"+redisTemplate.opsForSet().pop("set"));
        System.out.println(redisTemplate.opsForSet().members("set"));
        return "ok";
    }

    @GetMapping("/testHash")
    @ResponseBody
    public String testHash() {
        //execute的回调方法必须要有return语句
        redisTemplate.execute((RedisConnection connection) -> {
            //进行原生操作
            connection.flushAll();
            return null;
        });
        redisTemplate.opsForHash().put("hash", "AAA", "aaa");
        redisTemplate.opsForHash().put("hash", "BBB", "bbb");
        redisTemplate.opsForHash().put("hash", "CCC", "ccc");
        redisTemplate.opsForHash().put("hash", "DDD", "ddd");
        System.out.println("DDD:"+redisTemplate.opsForHash().get("hash", "DDD"));
        System.out.println("hash里面存储的键值对数量为"+redisTemplate.opsForHash().size("hash"));
        System.out.println(redisTemplate.opsForHash().keys("hash"));
        System.out.println(redisTemplate.opsForHash().hasKey("hash", "AAA"));
        System.out.println(redisTemplate.opsForHash().hasKey("hash", "EEE"));
        return "ok";
    }

    @GetMapping("/testZset")
    @ResponseBody
    public String testZset() {
        //execute的回调方法必须要有return语句
        redisTemplate.execute((RedisConnection connection) -> {
            //进行原生操作
            connection.flushAll();
            return null;
        });
        redisTemplate.opsForZSet().add("zset","A",100);
        redisTemplate.opsForZSet().add("zset","B",30);
        redisTemplate.opsForZSet().add("zset","C",60);
        redisTemplate.opsForZSet().add("zset","D",80);
        System.out.println("zset的元素个数为"+redisTemplate.opsForZSet().size("zset"));
        System.out.println("member C 的位置在"+redisTemplate.opsForZSet().rank("zset", "C"));
        System.out.println(redisTemplate.opsForZSet().range("zset", 0, -1));
        System.out.println(redisTemplate.opsForZSet().rangeWithScores("zset", 0, -1));
        System.out.println("C的分数为"+redisTemplate.opsForZSet().score("zset", "C"));
        redisTemplate.opsForZSet().popMax("zset");
        System.out.println(redisTemplate.opsForZSet().rangeWithScores("zset", 0, -1));
        return "ok";
    }
}

但愿每次回忆,对生活都不感到负疚         —— 郭小川

🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀

以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐

  制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸 

Logo

欢迎加入我们的广州开发者社区,与优秀的开发者共同成长!

更多推荐