🔥个人主页: 中草药
一、认识Redis
Redis(Remote Dictionary Server)是一个开源的、基于内存的键值对存储数据库,支持持久化、网络化访问,并提供多种数据结构操作,用作数据缓存。它由Salvatore Sanfilippo开发,凭借其单线程模型和高效数据结构,成为高并发场景下的首选解决方案。
核心特性:
-
内存存储:数据主要存储在内存中(内存为主,硬盘为辅),读写速度远超传统磁盘数据库,如MySQL(最大特点快)。
-
丰富的数据结构:支持String、List、Hash、Set、Zset五种基础类型,以及BitMap、HyperLogLog等扩展类型。
-
持久化机制:提供RDB(快照)和AOF(追加日志)两种方式,确保数据安全。
-
高可用与扩展:支持主从复制、哨兵监控、集群分片,满足分布式需求。
二、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,删除其中已过期的。具体流程:
-
从过期字典中随机选择 20 个 Key。
-
删除其中已过期的 Key。
-
若超过 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、共享会话
存储用户登录态,实现分布式系统的会话共享。
我们可以用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
/RPUSH
、LPOP
/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
),按分值排序。 -
分数相同,根据元素自身字符串的字典序来排列
-
支持
ZRANGEBYSCORE
、ZRANK
等范围操作。
-
命令
命令 | 功能 | 时间复杂度 |
---|---|---|
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
(拒绝操作)。 -
可以理解为一串二进制序列
选择数据结构的建议
-
高频更新场景:
-
计数器用 String(
INCR
)。 -
对象属性更新用 Hash(
HINCRBY
)。
-
-
范围查询:
-
时间序列数据用 Sorted Set(按时间戳排序)。
-
地理位置用 Geospatial。
-
-
去重与集合运算:
-
标签系统用 Set。
-
唯一值统计用 HyperLogLog。
-
-
消息队列:
-
简单队列用 List。
-
需持久化和消费者组管理用 Stream。
-
五、渐进式遍历
Redis 的 渐进式遍历 是一种高效、安全的遍历大数据集的方法,旨在避免使用 KEYS
、SMEMBERS
等阻塞式命令导致的性能问题。它通过分批次、游标(cursor)的方式逐步遍历数据,确保在遍历过程中不会长时间阻塞 Redis 主线程,适合处理海量数据。
为什么需要渐进式遍历?
-
避免阻塞
KEYS *
、SMEMBERS
等命令会一次性返回所有数据,如果数据量极大(如百万级),会导致 Redis 主线程长时间阻塞,影响其他请求。 -
动态数据集
如果遍历过程中数据集发生变化(如新增或删除元素),传统命令可能返回不一致的结果。 -
内存安全
一次性返回所有数据可能撑爆客户端内存,而渐进式遍历允许客户端分批处理。
核心命令: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+):按数据类型过滤(如string
、hash
)。
示例
关键特性
-
非阻塞
每次SCAN
只返回少量数据,避免长时间占用主线程,且遍历过程可以随时中断。 -
游标机制
客户端需记录游标,直到返回0
表示遍历完成。 -
弱一致性
遍历过程中若数据发生变化(如新增或删除元素),可能会返回重复或遗漏部分数据。 -
COUNT 参数
COUNT
仅是一个建议值,实际返回数量可能不同(Redis 根据内部优化调整)。 -
时间复杂度
每次SCAN
的时间复杂度为O(1)
,完整遍历的时间复杂度为O(N)
(与KEYS
相同,但分摊了耗时)。
六、JavaClient
在日常开发之中命令行客户端并不是我们日常开发的主要形式,更多是用API去构建自己的客户端,客户端按照对应的应用层协议发送请求,服务器按照这个协议进行解析,再按照这个协议构造响应,客户端再进行解析
RESP(REdis Serialization Protocol)
Redis 的 RESP(REdis 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";
}
}
但愿每次回忆,对生活都不感到负疚 —— 郭小川
🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀🍀
以上,就是本期的全部内容啦,若有错误疏忽希望各位大佬及时指出💐
制作不易,希望能对各位提供微小的帮助,可否留下你免费的赞呢🌸