引言
在当今数据驱动的时代,高效的数据存储和检索对于各类应用程序至关重要。Redis(Remote Dictionary Server)作为一款开源的内存键值数据库,凭借其出色的性能、丰富的数据结构和灵活的特性,在众多场景中得到了广泛应用。本文将深入探讨 Redis 的基本概念、核心特性、常用命令以及实际应用案例,帮助读者全面了解和掌握 Redis
一.Redis相关命令详解及其原理
1.redis是什么?
Redis 是 Remote Dictionary Service 的简称;也是远程字典服务,通过tcp和redis建立连接交互,通过字典的方式索引存储数据。
Redis 是内存数据库,KV 数据库,数据结构数据库,数据都在内存中。
2.redis中存储数据的数据结构都有哪些?
①string:是一个安全的二进制字符串。
②双端队列(链表)list :有序(插入有序)。
③散列表 hash :对顺序不关注, field 是唯一的;
④无序集合 set :对顺序不关注,里面的值都是唯一的;
⑤有序集合 zset :对顺序是关注的,里面的值是唯一的;根据member 来确定唯一;根据 score 来确定有序;
3.redis的存储结构(KV)

4.reidis中value编码

数据量 少 的时候,存储效率高为主;数据量 多 的时候,运行速度快;
5.string的基本原理和相关命令
5.1基本原理
字符数组,该字符串是动态字符串 raw ,字符串长度小于 1M时,加倍扩容;超过 1M 每次只多扩 1M ;字符串最大长度为 512M ;注意: redis 字符串是二进制安全字符串;可以存储图片,二进制协议等二进制数据;
5.2基础命令
# 设置 key 的 value 值
SET key val
# 获取 key 的 value
GET key
# 执行原子加一的操作
INCR key
# 执行原子加一个整数的操作
INCRBY key increment
# 执行原子减一的操作
DECR key
# 执行原子减一个整数的操作
DECRBY key decrement
# 如果key不存在,这种情况下等同SET命令。当key存在时,什
么也不做
# set Not eXist ok 这个命令是否执行了 0,1 是不是操
作结果是不是成功
SETNX key value
# 删除 key val 键值对
DEL key
# 设置或者清空key的value(字符串)在offset处的bit值。
setbit embstr raw int
# 动态字符串能够节约内存
SETBIT key offset value
# 返回key对应的string在offset处的bit值
GETBIT key offset
# 统计字符串被设置为1的bit数.
BITCOUNT key
我们来举一些简单的例子
5.3string存储结构
字符串长度小于等于 20 且能转成整数,则使用 int 存储;字符串长度小于等于 44 ,则使用 embstr 存储;字符串长度大于 44 ,则使用 raw 存储;
5.4应用场景
对象存储
SET role:10001 '{["name"]:"lion",["sex"]:"male",
["age"]:30}'
SET role:10002 '{["name"]:"xiaoyu",["sex"]:"female",
["age"]:30}'
我们应该如何设置Key 才能让它更有意义呢?单个功能的一个key:取有意义名字的key相同功能的多个key:我们可以以:作为分割


# 统计阅读数 累计加1
incr reads
# 累计加100
incrby reads 100
分布式锁
# 加锁 加锁 和 解析 redis 实现是 非公平锁 ectd
zk 用来实现公平锁
# 阻塞等待 阻塞连接的方式
# 介绍简单的原理: 事务
setnx lock 1 # 不存在才能设置 定义加锁行为 占用锁
setnx lock uuid # expire 30 过期
set lock uuid nx ex 30
# 释放锁
del lock
if (get(lock) == uuid)del(lock);
位运算
# 猜测一下 string 是用的 int 类型 还是 string 类型
# 月签到功能 10001 用户id 202106 2021年6月份的签到 6月
份的第1天
setbit sign:10001:202106 1 1
# 计算 2021年6月份 的签到情况
bitcount sign:10001:202106
# 获取 2021年6月份 第二天的签到情况 1 已签到 0 没有签到
getbit sign:10001:202106 2
6.list的基本原理和相关命令
6.1基本原理
双向链表实现,列表首尾操作(删除和增加)时间复杂度O(1) ;查找中间元素时间复杂度为 O(n) ;列表中数据是否压缩的依据:1. 元素长度小于 48 ,不压缩;2. 元素压缩前后长度差不超过 8 ,不压缩;
6.2基础命令
# 从队列的左侧入队一个或多个元素
LPUSH key value [value ...]
# 从队列的左侧弹出一个元素
LPOP key
# 从队列的右侧入队一个或多个元素
RPUSH key value [value ...]
# 从队列的右侧弹出一个元素
RPOP key
# 返回从队列的 start 和 end 之间的元素 0, 1 2 负索引
LRANGE key start end
# 从存于 key 的列表里移除前 count 次出现的值为 value 的
元素
# list 没有去重功能 hash set zset
LREM key count value
# 它是 RPOP 的阻塞版本,因为这个命令会在给定list无法弹出
任何元素的时候阻塞连接
BRPOP key timeout # 超时时间 + 延时队列
举栗子
我们采用lpush从左边插入五个人名
我们采用的是从左边插入的方法 也就是所谓的头插法 简易链表展示如下
lion
xiaoyu-->lion
xiaolan-->xiaoyu-->lion
xiaohuang-->xiaolan-->xiaoyu-->lion
xiaobai-->xiaohuang-->xiaolan-->xiaoyu-->lion
我们分别从左边和右边弹出一个元素 结果如下
6.3存储结构
/* Minimum ziplist size in bytes for attempting
compression. */
#define MIN_COMPRESS_BYTES 48
/* quicklistNode is a 32 byte struct describing a
ziplist for a quicklist.
* We use bit fields keep the quicklistNode at 32
bytes.
* count: 16 bits, max 65536 (max zl bytes is
65k, so max count actually < 32k).
* encoding: 2 bits, RAW=1, LZF=2.
* container: 2 bits, NONE=1, ZIPLIST=2.
* recompress: 1 bit, bool, true if node is
temporary decompressed for usage.
* attempted_compress: 1 bit, boolean, used for
verifying during testing.
* extra: 10 bits, free for future use; pads out
the remainder of 32 bits */
typedef struct quicklistNode {struct quicklistNode *prev;struct quicklistNode *next;unsigned char *zl;unsigned int sz; /* ziplist size
in bytes */unsigned int count : 16; /* count of
items in ziplist */unsigned int encoding : 2; /* RAW==1 or
LZF==2 */unsigned int container : 2; /* NONE==1 or
ZIPLIST==2 */unsigned int recompress : 1; /* was this node
previous compressed? */unsigned int attempted_compress : 1; /* node
can't compress; too small */unsigned int extra : 10; /* more bits to
steal for future usage */
} quicklistNode;
typedef struct quicklist {quicklistNode *head;quicklistNode *tail;unsigned long count; /* total count of
all entries in all ziplists */unsigned long len; /* number of
quicklistNodes */int fill : QL_FILL_BITS; /* fill
factor for individual nodes */unsigned int compress : QL_COMP_BITS; /*
depth of end nodes not to compress;0=off */unsigned int bookmark_count: QL_BM_BITS;quicklistBookmark bookmarks[];
} quicklist;
6.4应用场景
LPUSH + LPOP
# 或者
RPUSH + RPOP
LPUSH + RPOP
# 或者
RPUSH + LPOP
LPUSH + BRPOP
# 或者
RPUSH + BLPOP
-- redis lua脚本
local record = KEYS[1]
redis.call("LPUSH", "says", record)
redis.call("LTRIM", "says", 0, 4)
7.hash的基本原理和相关命令
7.1基本原理
散列表,在很多高级语言当中包含这种数据结构; c++unordered_map 通过 key 快速索引 value ;
7.2基础命令
# 获取 key 对应 hash 中的 field 对应的值
HGET key field
# 设置 key 对应 hash 中的 field 对应的值
HSET key field value
# 设置多个hash键值对
HMSET key field1 value1 field2 value2 ... fieldn
valuen
# 获取多个field的值
HMGET key field1 field2 ... fieldn
# 给 key 对应 hash 中的 field 对应的值加一个整数值
HINCRBY key field increment
# 获取 key 对应的 hash 有多少个键值对
HLEN key
# 删除 key 对应的 hash 的键值对,该键为field
HDEL key field
举栗子
7.3存储结构
节点数量大于 512 ( hash-max-ziplist-entries ) 或所有字符串长度大于 64 ( hash-max-ziplist-value ),则使用 dict 实现;节点数量小于等于 512 且有一个字符串长度小于 64 ,则使用ziplist 实现;
7.4应用场景
存储对象
hmset hash:10001 name lion age 18 sex male
# 与 string 比较
set hash:10001 '{["name"]:"lion",["sex"]:"male",
["age"]:18}'
# 假设现在修改 lion的年龄为19岁
# hash:hset hash:10001 age 19
# string:get hash:10001# 将得到的字符串调用json解密,取出字段,修改 age 值# 再调用json加密set hash:10001 '{["name"]:"lion",
["sex"]:"male",["age"]:19}'
# 将用户id作为 key
# 商品id作为 field
# 商品数量作为 value
# 注意:这些物品是按照我们添加顺序来显示的;# 添加商品:hmset MyCart:10001 40001 1 cost 5099 desc "戴尔
笔记本14-3400"lpush MyItem:10001 40001
# 增加数量:
hincrby MyCart:10001 40001 1hincrby MyCart:10001 40001 -1 // 减少数量1
# 显示所有物品数量:hlen MyCart:10001
# 删除商品:hdel MyCart:10001 40001lrem MyItem:10001 1 40001
# 获取所有物品:lrange MyItem:10001# 40001 40002 40003hget MyCart:10001 40001hget MyCart:10001 40002hget MyCart:10001 40003
8.set的基本原理和相关命令
8.1基本原理
集合;用来存储唯一性字段,不要求有序;存储不需要有序,操作(交并差集的时候排序)?
8.2基础命令
# 添加一个或多个指定的member元素到集合的 key中
SADD key member [member ...]
# 计算集合元素个数
SCARD key
# SMEMBERS key
SMEMBERS key
# 返回成员 member 是否是存储的集合 key的成员
SISMEMBER key member
# 随机返回key集合中的一个或者多个元素,不删除这些元素
SRANDMEMBER key [count]
# 从存储在key的集合中移除并返回一个或多个随机元素
SPOP key [count]# 返回一个集合与给定集合的差集的元素
SDIFF key [key ...]
# 返回指定所有的集合的成员的交集
SINTER key [key ...]
# 返回给定的多个集合的并集中的所有成员
SUNION key [key ...]
举栗子
8.3存储结构
元素都为整数且节点数量小于等于 512 ( set-max-intsetentries ),则使用整数数组存储;元素当中有一个不是整数或者节点数量大于 512 ,则使用字典存储;
8.4应用场景
抽奖
# 添加抽奖用户sadd Award:1 10001 10002 10003 10004 10005
10006sadd Award:1 10009
# 查看所有抽奖用户smembers Award:1
# 抽取多名幸运用户srandmember Award:1 10
# 如果抽取一等奖1名,二等奖2名,三等奖3名,该如何操作?
共同关注
sadd follow:A mark king darren mole vico
sadd follow:C mark king darren
sinter follow:A follow:C
sadd follow:A mark king darren mole vico
sadd follow:C mark king darren
# C可能认识的人:sdiff follow:A follow:C
9.zset的基本原理和相关命令
9.1基本原理
有序集合;用来实现排行榜;它是一个有序唯一;
9.2基础命令
# 添加到键为key有序集合(sorted set)里面
ZADD key [NX|XX] [CH] [INCR] score member [score
member ...]
# 从键为key有序集合中删除 member 的键值对
ZREM key member [member ...]
# 返回有序集key中,成员member的score值
ZSCORE key member
# 为有序集key的成员member的score值加上增量increment
ZINCRBY key increment member
# 返回key的有序集元素个数
ZCARD key
# 返回有序集key中成员member的排名
ZRANK key member
# 返回存储在有序集合key中的指定范围的元素 order by id
limit 1,100
ZRANGE key start stop [WITHSCORES]
# 返回有序集key中,指定区间内的成员(逆序)
ZREVRANGE key start stop [WITHSCORES]
举栗子
9.3存储结构
节点数量大于 128 或者有一个字符串长度大于 64 ,则使用跳表( skiplist );节点数量小于等于 128 ( zset-max-ziplist-entries )且所有字符串长度小于等于 64 ( zset-max-ziplist-value ),则使用ziplist 存储;数据少的时候,节省空间O(n)数据多的时候,访问性能O(1) or O(logn)
9.4应用场景
百度热搜
# 点击新闻:zincrby hot:20230612 1 10001zincrby hot:20230612 1 10002zincrby hot:20230612 1 10003zincrby hot:20230612 1 10004zincrby hot:20230612 1 10005zincrby hot:20230612 1 10006zincrby hot:20230612 1 10007zincrby hot:20230612 1 10008zincrby hot:20230612 1 10009zincrby hot:20230612 1 10010# 获取排行榜:zrevrange hot:20230612 0 9 withscores
将消息序列化成一个字符串作为 zset 的 member ;这个消息的到期处理时间作为 score ,然后用多个线程轮询 zset 获取到期的任务进行处理。
def delay(msg):msg.id = str(uuid.uuid4()) #保证 member 唯一value = json.dumps(msg)retry_ts = time.time() + 5 # 5s后重试redis.zadd("delay-queue", retry_ts, value)
# 使用连接池
def loop():while True:values = redis.zrangebyscore("delayqueue", 0, time.time(), start=0, num=1)if not values:time.sleep(1)continuevalue = values[0]success = redis.zrem("delay-queue",
value)if success:msg = json.loads(value)handle_msg(msg)
# 缺点:loop 是多线程竞争,两个线程都从zrangebyscore获
取到数据,但是zrem一个成功一个失败,
# 优化:为了避免多余的操作,可以使用lua脚本原子执行这两个
命令
# 解决:漏斗限流
分布式定时器
生产者将定时任务 hash 到不同的 redis 实体中,为每一个redis 实体分配一个 dispatcher 进程,用来定时获取 redis 中超时事件并发布到不同的消费者中;
时间窗口限流
系统限定用户的某个行为在指定的时间范围内(动态)只能发生 N 次;
# 指定用户 user_id 的某个行为 action 在特定时间内
period 只允许发生该行为做大次数 max_count
local function is_action_allowed(red, userid,
action, period, max_count)local key = tab_concat({"hist", userid,
action}, ":")local now = zv.time()red:init_pipeline()-- 记录行为red:zadd(key, now, now)-- 移除时间窗口之前的行为记录,剩下的都是时间窗口内的
记录red:zremrangebyscore(key, 0, now - period
*100)-- 获取时间窗口内的行为数量red:zcard(key)-- 设置过期时间,避免冷用户持续占用内存 时间窗口的长
度+1秒red:expire(key, period + 1)local res = red:commit_pipeline()return res[3] <= max_count
end# 维护一次时间窗口,将窗口外的记录全部清理掉,只保留窗口内
的记录;# 缺点:记录了所有时间窗口内的数据,如果这个量很大,不适合
做这样的限流;漏斗限流
# 注意:如果用 key + expire 操作也能实现,但是实现的是熔
断限流,这里是时间窗口限流的功能;score
10.redis的抽象层次
补充:redis设置过期时间
①expire
②setex
③ex
二.协议与异步方式
1.redis pipeline
redis pipeline 是一个客户端提供的机制,而不是服务端提供的;pipeline 不具备事务性;目的:节约网络传输时间;通过一次发送多次请求命令,从而减少网络传输的时间一次性发送多个请求包,redis按序响应与http1.1解决的问题类似
2.发布订阅模式
2.1基本原理
为了支持消息的多播机制, redis 引入了发布订阅模块;消息不一定可达;分布式消息队列;stream 的方式确保一定可 达;
2.2基础命令
# 订阅频道
subscribe 频道
# 订阅模式频道
psubscribe 频道
# 取消订阅频道
unsubscribe 频道
# 取消订阅模式频道
punsubscribe 频道
# 发布具体频道或模式频道的内容
publish 频道 内容
# 客户端收到具体频道内容
message 具体频道 内容
# 客户端收到模式频道内容
pmessage 模式频道 具体频道 内容
举栗子
2.3应用场景
发布订阅功能一般要区别命令连接重新开启一个连接;因为命令连接严格遵循请求回应模式;而 pubsub 能收到 redis 主动推送的内容;所以实际项目中如果支持 pubsub 的话,需要 另开一条连接 用于处理发布订阅;
2.4缺点
发布订阅的生产者传递过来一个消息, redis 会直接找到相应的消费者并传递过去;假如没有消费者,消息直接丢弃;假如开始有 2 个消费者,一个消费者突然挂掉了,另外一个消费者依然能收到消息,但是如果刚挂掉的消费者重新连上后,在断开连接期间的消息对于该消费者来说彻底丢失了;另外, redis 停机重启, pubsub 的消息是不会持久化的,所有的消息被直接丢弃;
3.redis事务
3.1什么是事务,事务具有什么特性
事务:用户定义一系列数据库操作,这些操作视为一个完整的逻辑处理工作单元 ,要么全部执行,要么全部不执行,是不可分割的工作单元。MULTI 开启事务,事务执行过程中,单个命令是入队列操作,直到调用 EXEC 才会一起执行;乐观锁实现,所以失败需要重试,增加业务逻辑的复杂度;acid①原子性②一致性③隔离性④持久性
3.2实现事务的基础命令(不常用)
MULTI
开启事务
begin / start transactionEXEC
提交事务
commitDISCARD
取消事务
rollbackWATCH
检测 key 的变动,若在事务执行中,key 变动则取消事务;在事
务开启前调用,乐观锁实现(cas);
若被取消则事务返回 nil ;
3.3应用场景
事务实现 zpop
WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC
WATCH score:10001
val = GET score:10001
MULTI
SET score:10001 val*2
EXEC
4.lua脚本(常用)
4.1lua脚本怎么实现
lua 脚本实现原子性;redis 中加载了一个 lua 虚拟机;用来执行 redis lua 脚本; redislua 脚本的执行是原子性的;当某个脚本正在执行的时候,不会有其他命令或者脚本被执行;lua 脚本当中的命令会直接修改数据状态;lua 脚本 mysql 存储区别: MySQL 存储过程不具备事务性,所以也不具备原子性;注意 :如果项目中使用了 lua 脚本,不需要使用上面的事务命令;
4.2.lua脚本一些基础命令
# 从文件中读取 lua脚本内容
cat test1.lua | redis-cli script load --pipe
# 加载 lua脚本字符串 生成 sha1
> script load 'local val = KEYS[1]; return val'
"b8059ba43af6ffe8bed3db65bac35d452f8115d8"
# 检查脚本缓存中,是否有该 sha1 散列值的lua脚本
> script exists
"b8059ba43af6ffe8bed3db65bac35d452f8115d8"
1) (integer) 1
# 清除所有脚本缓存
> script flush
OK
# 如果当前脚本运行时间过长(死循环),可以通过 script kill
杀死当前运行的脚本
> script kill
(error) NOTBUSY No scripts in execution right
now.
EVAL
# 测试使用
EVAL script numkeys key [key ...] arg [arg ...]
# 线上使用
EVALSHA sha1 numkeys key [key ...] arg [arg ...]


4.3应用场景
# 1: 项目启动时,建立redis连接并验证后,先加载所有项目中使用的lua脚本(script load);# 2: 项目中若需要热更新,通过redis-cli script flush;然后可以通过订阅发布功能通知所有服务器重新加载lua脚本;# 3:若项目中lua脚本发生阻塞,可通过script kill暂停当前阻塞脚本的执行;
5.ACID特性分析
A 原子性;事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败; redis 不支持回滚;即使事务队列中的某个命令在执行期间出现了错误,整个事务也会继续执行下去,直到将事务队列中的所有命令都执行完毕为止。C 一致性;事务的前后,所有的数据都保持一个一致的状态,不能违反数据的一致性检测;这里的一致性是指预期的一致性而不是异常后的一致性;所以 redis 也不满足;这个争议很大:redis 能确保事务执行前后的数据的完整约束;但是并不满足业务功能上的一致性;比如转账功能,一个扣钱一个加钱;可能出现扣钱执行错误,加钱执行正确,那么最终还是会加钱成功;系统凭空多了钱;I 隔离性;各个事务之间互相影响的程度; redis 是单线程执行,天然具备隔离性;D 持久性; redis 只有在 aof 持久化策略的时候,并且需要在redis.conf 中 appendfsync=always 才具备持久性;实际项目中几乎不会使用 aof 持久化策略;面试时候回答: lua 脚本满足原子性和隔离性;一致性和持久性不满足;
6.redis异步连接
6.1redis协议图

协议实现的第一步需要知道如何界定数据包:1. 长度 + 二进制流2. 二进制流 + 特殊分隔符
6.2异步连接
同步连接方案采用阻塞 io 来实现;优点是代码书写是同步的,业务逻辑没有割裂;缺点是阻塞当前线程,直至 redis 返回结果;通常用多个线程来实现线程池来解决效率问题;异步连接方案采用非阻塞 io 来实现;优点是没有阻塞当前线程, redis 没有返回,依然可以往 redis 发送命令;缺点是代码书写是异步的(回调函数),业务逻辑割裂,可以通过协程解决( openresty , skynet );配合 redis6.0 以后的 io 多线程(前提是有大量并发请求),异步连接池,能更好解决应用层的数据访问性能;
6.3实现方案







6.4原理是什么
hiredis 提供异步连接方式,提供 可以替换 IO 检测 的接口;关键替换 addRead , delRead , addWrite , delWrite ,cleanup , scheduleTimer ,这几个检测接口;其他 io 操作,比如 connect , read , write , close 等都交由 hiredis 来处理;同时需要提供连接建立成功以及断开连接的回调;用户可以使用当前项目的网络框架来替换相应的操作;从而实现跟项目网络层兼容的异步连接方案;
三.存储原理与数据模型
1.redis是单线程还是多线程
我们通过gdb调试redis 可以查看 起线程明显有多个
这些线程的作用分别如下图所示
2.redis中说的单线程究竟是什么?
命令处理是在一个单线程中
处理客户端请求的核心流程是单线程的 。
即从接收客户端请求(网络读取 )、解析命令、执行命令到返回结果(网络写入 ),都由一个主线程顺序串行处理 。
所有命令在这个过程中逐个执行,不会有两条命令被同时执行,确保了操作的原子性,也避免了线程安全问题和不必要的上下文切换。
比如执行
SET key value
、GET key
等各种数据操作命令时,都是在这个单线程流程中完成 。
3.命令处理为什么是单线程
3.1单线程的局限性(redis要避免)
不能有耗时的操作:cpu运算和阻塞的io
对于redis而言会影响性能
3.2redis有没有io密集型和cpu密集型(有的)
IO密集型分为磁盘IO和网络IO
redis在磁盘IO中可以fork子进程,在子进程中做持久化,还可以异步刷盘
redis在网络IO中服务多个客户,造成IO密集,数据请求和返回量比较大,redis会开启IO多线程处理这个问题
4.命令处理为什么不采用多线程
加锁复杂,锁的粒度不好控制
频繁的cpu上下文切换,抵消多线程的优势
5.单线程是怎么做到这么快的呢?
5.1采用了哪些机制
内存型数据库
数据的组织方式O(1):hashtable 可以扩容,缩容,还能渐进式rehash在cpu运算中,每步操作rehash,定时rehash 1ms 之类的.
数据高效:在执行效率与空间占用保持平衡,数据结构切换
高效的reactor网络模型
5.2做了哪些优化
分治思想
把rehash分摊到每一个操作步骤当中
在定时器当中,以100为步长最大rehash 1ms
耗时阻塞的操作,在其他线程处理
对象类型采用不同的数据结构实现
6.负载因子,扩缩容机制,渐进式rehash
负载因子 = used / size ; used 是数组存储元素的个数,size 是数组的长度;负载因子越小,冲突越小;负载因子越大,冲突越大;redis 的负载因子是 1 ;
扩容
如果负载因子 > 1 ,则会发生扩容;扩容的规则是翻倍;如果正在 fork (在 rdb 、 aof 复写以及 rdb-aof 混用情况下)时,会阻止扩容;但是此时若负载因子 > 5 ,索引效率大大降低, 则马上扩容;这里涉及到写时复制原理;
缩容
如果负载因子 < 0.1 ,则会发生缩容;缩容的规则是 恰好 包含used 的 ;恰好 的理解:假如此时数组存储元素个数为 9 ,恰好包含该元素的就是 ,也就是 16 ;
渐进式rehash
当 hashtable 中的元素过多的时候,不能一次性 rehash 到ht[1] ;这样会长期占用 redis ,其他命令得不到响应;所以需要使用渐进式 rehash ;rehash 步骤 :将 ht[0] 中的元素重新经过 hash 函数生成 64 位整数,再对ht[1] 长度进行取余,从而映射到 ht[1] ;渐进式规则 :1. 分治的思想,将 rehash 分到之后的每步增删改查的操作当中;2. 在定时器中,最大执行一毫秒 rehash ;每次步长 100 个数组槽位;面试 :处于渐进式 rehash 阶段时,是否会发生扩容缩容?不会!
7.scan
scan cursor [MATCH pattern] [COUNT count] [TYPE
type]
采用高位进位加法的遍历顺序, rehash 后的槽位在遍历顺序上是相邻的;遍历目标是:不重复,不遗漏 ;会出现一种重复的情况:在 scan 过程当中,发生两次缩容的时候,会发生数据重复;注意 :上课时有一个问题表述错误;关于 scan , scan 要达到的目的是从 scan 开始那刻起 redis 已经存在的数据进行遍历,不会重复和遗漏(例外是 scan 过程中两次缩容可能造成数据重复), 因为比如我 scan 已经快结束了,现在插入大量数据,这些数据肯定遍历不到;扩容和缩容造成映射算法发生改变,但是使用高位进位累加的算法,可以对 scan 那刻起已经存在数据的遍历不会出错;
举栗子
8.expire
# 只支持对最外层key过期;
expire key seconds
pexpire key milliseconds
ttl key
pttl key
分布在每一个命令操作时检查 key 是否过期;若过期删除 key ,再进行命令操作;
定时删除
在定时器中检查库中指定个数(25)个 key;
#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /*
Keys for each DB loop. */
/*The default effort is 1, and the maximum
configurable effort* is 10. */
config_keys_per_loop =
ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
int activeExpireCycleTryExpire(redisDb *db,
dictEntry *de, long long now);
举栗子
9.大Key
在 redis 实例中形成了很大的对象,比如一个很大的 hash 或很大的 zset ,这样的对象在扩容的时候,会一次性申请更大的一块内存,这会导致卡顿;如果这个大 key 被删除,内存会一次性回收,卡顿现象会再次产生;如果观察到 redis 的内存大起大落,极有可能因为大 key 导致的;
# 每隔0.1秒 执行100条scan命令
redis-cli -h 127.0.0.1 --bigkeys -i 0.1
10.对象编码(补充)
11.skiplist(用在zset中)
从节约内存出发, redis 考虑牺牲一点时间复杂度让跳表结构更加变扁平,就像二叉堆改成四叉堆结构;并且 redis 还限制了跳表的最高层级为 32 ;节点数量大于 128 或者有一个字符串长度大于 64 ,则使用跳表( skiplist );
数据结构
#define ZSKIPLIST_MAXLEVEL 32 /* Should be enough
for 2^64 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4
*/
/* ZSETs use a specialized version of Skiplists
*/
typedef struct zskiplistNode {sds ele;double score; // WRN: score 只能是浮点数struct zskiplistNode *backward;struct zskiplistLevel {struct zskiplistNode *forward;unsigned long span; // 用于 zrank} level[];
} zskiplistNode;
typedef struct zskiplist {struct zskiplistNode *header, *tail;unsigned long length; // zcardint level; // 最高层
} zskiplist;
typedef struct zset {dict *dict; // 帮助快速索引到节点zskiplist *zsl;
} zset;

