【redis】redis操作zset类型的key发生了什么?
以下是关于Redis操作(添加、删除、修改、查询)ZSet类型key的完整过程。
功能
Redis操作ZSet类型key的过程包括:
- 添加:通过
ZADD
命令创建或更新ZSet类型的键值对。 - 删除:通过
ZREM
命令移除ZSet中的成员。 - 修改:通过
ZINCRBY
命令更新成员的分数。 - 查询:通过
ZRANGE
、ZSCORE
等命令获取ZSet的信息。
ZSet数据结构
(一)数据结构
Redis的ZSet(有序集合)是基于两个内部数据结构实现的:
- 字典(Dictionary):用于存储成员和分数的映射关系,支持快速查找。
- 跳表(Skip List):用于存储成员的有序排列,支持快速范围查询[72]。
1. 编码方式
ZSet的编码方式为:
ziplist
:当集合较小(成员数量较少且分数为整数)时,使用压缩列表编码。skiplist
:当集合较大或成员分数为浮点数时,使用跳表编码[72]。
2. 数据长度限制
- ZSet的成员数量没有硬性限制,但过多的成员可能影响性能。
- 分数(score)为双精度浮点数,范围为
-inf
到+inf
[72]。
操作过程
时序图
(一)添加成员(ZADD
)
-
客户端发送命令:
ZADD key score member [score member ...]
示例:
ZADD myset 1 "apple" 2 "banana" 3 "cherry"
-
服务器处理:
- 检查键是否存在,如果不存在则创建新的ZSet。
- 将成员和分数插入字典和跳表中[71]。
- 如果成员已存在,更新其分数并调整跳表[77]。
-
返回结果:
- 返回成功插入的成员数量[71]。
(二)删除成员(ZREM
)
-
客户端发送命令:
ZREM key member [member ...]
-
服务器处理:
- 从字典中删除成员和分数的映射。
- 从跳表中删除对应的节点[76]。
-
返回结果:
- 返回成功删除的成员数量[76]。
(三)修改成员分数(ZINCRBY
)
-
客户端发送命令:
ZINCRBY key increment member
-
服务器处理:
- 查找成员是否存在。
- 如果存在,更新其分数并调整跳表中的位置[77]。
- 如果不存在,插入新成员并设置分数[77]。
-
返回结果:
- 返回更新后的分数[77]。
(四)查询成员(ZRANGE
、ZSCORE
等)
-
客户端发送命令:
ZRANGE key start stop [WITHSCORES] ZSCORE key member
-
服务器处理:
- 使用跳表快速定位成员并返回结果[71]。
- 如果查询分数,直接从字典中获取[76]。
-
返回结果:
- 返回指定范围内的成员及其分数[71]。
持久化(磁盘IO读写)
(一)RDB持久化
- RDB会将ZSet的字典和跳表数据序列化为二进制格式并写入RDB文件[77]。
- 序列化流程:
- 遍历ZSet的字典,序列化成员和分数。
- 跳表不需要单独序列化[77]。
(二)AOF持久化
- AOF会将
ZADD
、ZREM
、ZINCRBY
等命令追加到AOF文件中[77]。 - AOF重写时,会根据当前ZSet的状态生成紧凑的命令序列[77]。
性能
(一)时间复杂度
ZADD
:O(log N),跳表插入操作[72]。ZREM
:O(log N),跳表删除操作[76]。ZINCRBY
:O(log N),跳表更新操作[77]。ZRANGE
:O(log N + M),M为返回的成员数量[72]。
(二)优化建议
- 避免对ZSet进行全量操作(如
ZRANGE
查询所有成员),可能导致性能瓶颈[72]。 - 使用分页查询(如
ZRANGE
的start
和stop
参数)减少数据传输[72]。
线程安全控制
Redis是单线程的,所有操作都是原子性的[75]。然而,在高并发场景下,客户端需要确保操作的线程安全性:
(一)使用Lua脚本
Lua脚本在Redis中是原子性执行的,可以确保操作的线程安全性[74]。例如:
-- 原子性地更新ZSet成员分数
local score = redis.call("ZSCORE", KEYS[1], ARGV[1])
if score then
redis.call("ZINCRBY", KEYS[1], ARGV[2], ARGV[1])
return score + tonumber(ARGV[2])
else
return nil
end
(二)使用分布式锁
在分布式环境中,可以使用Redis的SETNX
命令实现分布式锁[74]。
故障处理机制
(一)大Key删除
- 同步删除:使用
DEL
命令直接删除大Key,可能会阻塞主线程[75]。 - 异步删除:使用
UNLINK
命令异步删除大Key[75]。
(二)内存不足
- 配置
maxmemory
策略,自动淘汰旧数据[75]。 - 使用
MEMORY PURGE
命令释放未使用的内存[75]。
(三)数据丢失
- 启用RDB和AOF持久化机制,确保数据在故障时能够恢复[77]。
接口设计
以下是基于Redis ZSet的接口设计示例(以Java为例)[73]:
(一)类图
(二)方法实现
public class RedisZSetExample {
private Jedis jedis;
public RedisZSetExample() {
jedis = new Jedis("localhost");
}
public void addZSet(String key, String member, double score) {
jedis.zadd(key, score, member);
}
public Set<String> getZSetMembers(String key) {
return jedis.zrange(key, 0, -1);
}
public void removeZSetMember(String key, String member) {
jedis.zrem(key, member);
}
public void close() {
jedis.close();
}
}
(三)使用示例
public class Main {
public static void main(String[] args) {
RedisZSetExample zset = new RedisZSetExample();
zset.addZSet("myset", "apple", 1.0);
zset.addZSet("myset", "banana", 2.0);
System.out.println(zset.getZSetMembers("myset"));
zset.removeZSetMember("myset", "apple");
System.out.println(zset.getZSetMembers("myset"));
zset.close();
}
}
附录:set和zset有什么区别?
特性 | Set(无序集合) | ZSet(有序集合) |
---|---|---|
存储方式 | 无序集合,成员唯一 | 有序集合,成员按分数排序 |
内部实现 | 哈希表(hashtable )或整数集合(intset ) | 哈希表(hashtable )+跳表(skiplist ) |
功能 | 添加、删除、查找、集合操作 | 添加、删除、更新分数、范围查询 |
时间复杂度 | O(1) | O(log N)或O(log N + M) |
使用场景 | 去重、集合操作 | 排行榜、时间序列、优先队列 |
数据限制 | 成员数量无硬性限制,成员唯一 | 成员数量无硬性限制,成员按分数排序 |
内存占用 | 较低 | 较高 |
性能 | 高效的成员操作 | 支持复杂排序,但可能更耗时 |
Redis的ZSet(有序集合)和Set(无序集合)是两种不同的数据结构,它们在功能、内部实现和使用场景上存在显著区别。以下是ZSet和Set的主要区别:
1. 数据结构与存储方式
Set(无序集合)
- 存储方式:
- Set是一个无序的字符串集合,每个成员是唯一的,不允许重复。
- 内部实现基于哈希表(
hashtable
),支持快速的成员查找、添加和删除操作。
- 编码方式:
- 当集合较小(默认小于512个成员)时,使用
intset
(整数集合)编码。 - 当集合较大或包含非整数成员时,使用
hashtable
编码。
- 当集合较小(默认小于512个成员)时,使用
ZSet(有序集合)
- 存储方式:
- ZSet是一个有序的字符串集合,每个成员具有一个与之关联的分数(score),成员通过分数排序。
- 内部实现同时使用两个数据结构:
- 哈希表(
hashtable
):存储成员和分数的映射关系,支持快速查找。 - 跳表(
skiplist
):存储成员的有序排列,支持快速范围查询。
- 哈希表(
- 编码方式:
- 当集合较小且成员分数为整数时,使用
ziplist
编码。 - 当集合较大或成员分数为浮点数时,使用
skiplist
编码。
- 当集合较小且成员分数为整数时,使用
2. 功能与操作
Set
- 功能:
- 存储无序的唯一成员集合。
- 支持成员的添加、删除、查找和集合操作(如交集、并集、差集)。
- 常用命令:
SADD key member [member ...]
:添加成员。SREM key member [member ...]
:删除成员。SMEMBERS key
:获取所有成员。SISMEMBER key member
:检查成员是否存在。SINTER key1 [key2 ...]
:计算交集。SUNION key1 [key2 ...]
:计算并集。SDIFF key1 [key2 ...]
:计算差集。
ZSet
- 功能:
- 存储有序的成员集合,成员通过分数排序。
- 支持成员的添加、删除、分数更新和范围查询。
- 常用命令:
ZADD key score member [score member ...]
:添加或更新成员及其分数。ZREM key member [member ...]
:删除成员。ZSCORE key member
:获取成员的分数。ZRANGE key start stop [WITHSCORES]
:按分数排序获取成员。ZINCRBY key increment member
:增加成员的分数。ZCOUNT key min max
:计算指定分数范围内的成员数量。ZREVRANGE key start stop [WITHSCORES]
:按分数逆序获取成员。
3. 时间复杂度
Set
- 添加成员(
SADD
):O(1) - 删除成员(
SREM
):O(1) - 检查成员是否存在(
SISMEMBER
):O(1) - 获取所有成员(
SMEMBERS
):O(N),N为集合大小
ZSet
- 添加成员(
ZADD
):O(log N),N为集合大小 - 删除成员(
ZREM
):O(log N) - 更新成员分数(
ZINCRBY
):O(log N) - 获取成员范围(
ZRANGE
):O(log N + M),M为返回的成员数量 - 获取成员分数(
ZSCORE
):O(1)
4. 使用场景
Set
- 适用于存储无序的唯一成员集合,例如:
- 用户好友列表。
- 标签集合。
- 系统权限集合。
- 去重场景(如网页爬虫中的URL去重)。
ZSet
- 适用于需要按分数排序的场景,例如:
- 排行榜(如游戏积分榜、商品销量榜)。
- 时间序列数据(如日志按时间排序)。
- 优先队列(如任务调度)。
- 滑动窗口限流(通过分数表示时间戳)。
6. 内存占用与性能
Set
- 内存占用相对较低,尤其是使用
intset
编码时。 - 适用于成员数量较少且不需要排序的场景。
ZSet
- 内存占用相对较高,因为同时维护了哈希表和跳表。
- 性能上,ZSet的排序操作(如
ZRANGE
)可能比Set更复杂,但支持更丰富的功能。
7. 示例对比
Set
# 添加成员
SADD myset "apple" "banana" "cherry"
# 获取所有成员
SMEMBERS myset
# 输出:["apple", "banana", "cherry"]
# 检查成员是否存在
SISMEMBER myset "apple"
# 输出:1
# 删除成员
SREM myset "banana"
# 输出:1
ZSet
# 添加成员并设置分数
ZADD myzset 1 "apple" 2 "banana" 3 "cherry"
# 获取所有成员(按分数排序)
ZRANGE myzset 0 -1 WITHSCORES
# 输出:["apple", "1", "banana", "2", "cherry", "3"]
# 获取成员分数
ZSCORE myzset "banana"
# 输出:2
# 更新成员分数
ZINCRBY myzset 1 "banana"
# 输出:3
# 删除成员
ZREM myzset "banana"
# 输出:1