文章目录
目标
- redis的两个持久化策略
- redis的事务
- redis的哨兵
- redis的乐观锁
- redis的应用场景
- redis的底层数据结构(5+3)
1. Linux安装
1. 安装
上传 alt+p put xxx
解压 tar -zxvf xxx
删除压缩包 rm -rf xxx
移动 mv xxx apps
发现是redis源码文件夹,c语言为gcc编译,redis也有make prefix=usr/local/myredis install
启动 进入bin目录, ./redis-server redis-conf
,redis-conf
需要拷贝过来
2. 变成后台程序
vim redis.conf
进入后,看到daemonize
daemon
是守护进程,也就是后台进程。
daemonize yes
3. 常见指令
keys *
set k2 1
incr k2
get k2 //2
select 15 //第15个数据库
flushall //清空所有库
flushdb //清空所在库
2. redis的持久化策略
4.0已经有混合持久化,通过info Persistence
查看最近的持久化信息。
1. RDB
1. 什么是rdb?
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是snapshot快照。默认名称dump.rdb。它恢复时是将快照文件直接读到内存中。
2. 原理
bgsave
redis会fork一个子进程来进行持久化
具体流程:
- 主进程调用fork方法创建子进程,在创建过程中redis主进程阻塞,所以不能响应客户端请求
- 子进程创建完成以后,bgsave命令返回“Background saving started”,此时标志着redis可以响应客户端请求了
- 子进程根据主进程的内存副本创建临时快照文件,当快照文件完成以后对原快照文件进行替换
- 子进程发送信号给redis主进程完成快照操作,返回“Background saving terminated with success”,主进程更新统计信息(info Persistence可查看),子进程退出
3. 持久化策略
两种:主动和被动
主动是Save和BGsave,save是全阻塞,BGsave是异步操作,
被动策略有三种,默认情况900s1个key改变,300s10个key改变,60s1k个key改变触发。
save
save时只管保存,其它不管,全部阻塞
BGSAVE:Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。可以通过lastsave 命令获取最后一次成功执行快照的时间
flush操作 shutdown 操作也会save
stop-writes-on-bgsave-error :如果配置成no,表示你不在乎数据不一致或者有其他的手段发现和控制
对rdb文件的操作
rdbcompression 对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩。如果你不想消耗CPU来进行压缩的话,可以设置为关闭此功能
rdbchecksum: 在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
相关配置:
save m n
#配置快照(rdb)促发规则,格式:save <seconds> <changes>
#save 900 1 900秒内至少有1个key被改变则做一次快照
#save 300 10 300秒内至少有300个key被改变则做一次快照
#save 60 10000 60秒内至少有10000个key被改变则做一次快照
#关闭该规则使用svae “”
dbfilename dump.rdb
#rdb持久化存储数据库文件名,默认为dump.rdb
stop-write-on-bgsave-error yes
#yes代表当使用bgsave命令持久化出错时候停止写RDB快照文件,no表明忽略错误继续写文件。
rdbchecksum yes
#在写入文件和读取文件时是否开启rdb文件检查,检查是否有无损坏,如果在启动是检查发现损坏,则停止启动。
dir "/etc/redis"
#数据文件存放目录,rdb快照文件和aof文件都会存放至该目录,请确保有写权限
rdbcompression yes
#是否开启RDB文件压缩,该功能可以节约磁盘空间
rdb文件格式
在逻辑上可划分为头信息区、数据存储区、尾信息区
- 头信息区存储RDB版本、Redis版本、文件创建时间等信息
- 数据存储区保存的是用户存储的key-value对。也会保存数据库编号和key的数量
- 尾信息区存储结束标识符和CRC64校验码
具体
- 1-5字节是redis的魔术,就是redis
- 6-9字节是rdb的版本号
- 然后是各种操作码和被操作数据
- 最后8个字节的校验和
2. AOF
1. aof是什么
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件。对应磁盘文件(appendonly.aof)。AOF采用文本协议,采用RESP 3协议编码
2. aof的持久化过程
- 追加写 redis将每一条写命令以redis通讯协议添加至缓冲区aof_buf,这样的好处在于在大量写请求情况下,采用缓冲区暂存一部分命令随后根据策略一次性写入磁盘,这样可以减少磁盘的I/O次数
- 同步命令到硬盘 redis会将缓冲区的命令写入到文件,redis提供了三种同步策略,由配置参数appendfsync决定: no always everysec 生产默认everysec即可
- 文件重写(bgrewriteaof) 随着时间推移,AOF文件会越来越大,触发条件后,重写。
3. aof的重写
1. 重写原理
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
2. 触发条件
手动触发 执行 bgrewriteaof命令
被动触发 两个配置需要同时满足才会生效,默认配置是当AOF文件大小是上次rewrite后大小的一倍并and文件大于64M时触发
- auto-aof-rewrite-min-size: 64M
- auto-aof-rewrite-percentage:是原来的2倍时
重写aof文件 ,防止aof文件过大
3. 重写过程
类似rdb,也是fork子进程
- 开始bgrewriteaof,判断当前有没有bgsave命令(RDB持久化)/bgrewriteaof在执行
- 主进程fork出子进程,在这一个短暂的时间内,redis是阻塞的
- 主进程fork完子进程继续接受客户端请求,所有写命令依然写入AOF文件缓冲区并根据appendfsync策略同步到磁盘,保证原有AOF文件完整和正确。由于fork的子进程仅仅只共享主进程fork时的内存,因此Redis使用采用重写缓冲区(aof_rewrite_buf)机制保存fork之后的客户端的写请求,防止新AOF文件生成期间丢失这部分数据。此时,客户端的写请求不仅仅写入原来aof_buf缓冲,还写入重写缓冲区(aof_rewrite_buf)
- 子进程通过内存快照,按照命令重写策略写入到新的AOF文件
- 使用新的AOF文件覆盖旧的AOF文件,标志AOF重写完成
4. aof对应配置文件
auto-aof-rewrite-min-size 64mb
#AOF文件最小重写大小,只有当AOF文件大小大于该值时候才可能重写,4.0默认配置64mb。
auto-aof-rewrite-percentage 100
#当前AOF文件大小和最后一次重写后的大小之间的比率等于或者等于指定的增长百分比,如100代表当前AOF文件是上次重写的两倍时候才重写。
appendfsync everysec
#no:不使用fsync方法同步,而是交给操作系统write函数去执行同步操作,在linux操作系统中大约每30秒刷一次缓冲。这种情况下,缓冲区数据同步不可控,并且在大量的写操作下,aof_buf缓冲区会堆积会越来越严重,一旦redis出现故障,数据
#always:表示每次有写操作都调用fsync方法强制内核将数据写入到aof文件。这种情况下由于每次写命令都写到了文件中, 虽然数据比较安全,但是因为每次写操作都会同步到AOF文件中,所以在性能上会有影响,同时由于频繁的IO操作,硬盘的使用寿命会降低。
#everysec:数据将使用调用操作系统write写入文件,并使用fsync每秒一次从内核刷新到磁盘。 这是折中的方案,兼顾性能和数据安全,所以redis默认推荐使用该配置。
aof-load-truncated yes
#当redis突然运行崩溃时,会出现aof文件被截断的情况,Redis可以在发生这种情况时退出并加载错误,以下选项控制此行为。
#如果aof-load-truncated设置为yes,则加载截断的AOF文件,Redis服务器启动发出日志以通知用户该事件。
#如果该选项设置为no,则服务将中止并显示错误并停止启动。当该选项设置为no时,用户需要在重启之前使用“redis-check-aof”实用程序修复AOF文件在进行启动。
appendonly no
#yes开启AOF,no关闭AOF
appendfilename appendonly.aof
#指定AOF文件名,4.0无法通过config set 设置,只能通过修改配置文件设置。
dir /etc/redis
#RDB文件和AOF文件存放目录
5. 混合持久化
当我们开启了混合持久化时,启动redis依然优先加载aof文件,aof文件加载可能有两种情况如下:
aof文件开头是rdb的格式, 先加载 rdb内容再加载剩余的 aof。
aof文件开头不是rdb的格式,直接以aof格式加载整个文件
3. redis协议RESP
首先Redis是以行来划分,每行以\r\n行结束。每一行都有一个消息头,消息头共分为5种分别如下:
(+)
表示一个正确的状态信息,具体信息是当前行+后面的字符。
(-)
表示一个错误信息,具体信息是当前行-后面的字符。
(*)
表示消息体总共有多少行,不包括当前行,*后面是具体的行数。
($)
表示下一行数据长度,不包括换行符长度\r\n,$后面则是对应的长度的数据。
(:)
表示返回一个数值,:后面是相应的数字节符
redis6.0 将RESP2升级为RESP3协议,
- 新增多种数据类型,RESP2 浮点数和布尔值返回的string和integer
- 支持服务端主动推送,可以实现客户端缓存‘
4. redis的事务
1. 概念
可以一次执行多个命令,本质是一组命令的集合(队列)。一个事务中的所有命令都会序列化,按顺序地串行化执行而不会被其它命令插入,不许加塞。
2. 特点
redis的事务是弱原子性,弱事务的;关系型数据库是强原子性的。
弱原子性的表现
1. 正常执行
127.0.0.1:6379[15]> multi
OK
127.0.0.1:6379[15]> set k1 v1
QUEUED
127.0.0.1:6379[15]> set k2 v2
QUEUED
127.0.0.1:6379[15]> get k1
QUEUED
127.0.0.1:6379[15]> exec
1) OK
2) OK
3) "v1"
2. 放弃事务
127.0.0.1:6379[15]> multi
OK
127.0.0.1:6379[15]> set k1 v1
QUEUED
127.0.0.1:6379[15]> set k2 v2
QUEUED
127.0.0.1:6379[15]> discard
OK
127.0.0.1:6379[15]> get k1
(nil)
2. 全体连坐
语法错误,会导致全体连坐,都失败
127.0.0.1:6379[15]> multi
OK
127.0.0.1:6379[15]> set k1 v1
QUEUED
127.0.0.1:6379[15]> set k2 v2
QUEUED
127.0.0.1:6379[15]> set k3
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379[15]> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379[15]> get k1
(nil)
3. 冤有头债有主
逻辑错误,针对个体的逻辑错误,找该
127.0.0.1:6379[15]> multi
OK
127.0.0.1:6379[15]> set k1 v1
QUEUED
127.0.0.1:6379[15]> set k2 2
QUEUED
127.0.0.1:6379[15]> incr k2
QUEUED
127.0.0.1:6379[15]> incr k1
QUEUED
127.0.0.1:6379[15]> exec
1) OK
2) OK
3) (integer) 3
4) (error) ERR value is not an integer or out of range
3. 乐观锁机制
watch
监视一个或多个key,如果执行事物之前被其他key修改,那么事物会被打断 不能
如果没有监视这个key,那么在事物中这个key被其他操作修改后,能成功吗? 能unwatch 取消watch命令对所有key的监视
代码
两个线程
set balance 1000
watch balance
multi
set balance 1
exec
watch balance
multi
set balance 2
exec (结果为nil)
先执行exec的修改成功,因为进入事务后尚未提交,watch认为没有改变,就可以提交;线程2提交的时候发现已经和预期不一样了。
5. 主从
1. 读写分离
读写分离基本原理:让主数据库处理事务性增、改、删操作(INSERT、UPDATE、DELETE)操作,而从数据库处理SELECT查询操作。
读写分离的好处:
- 将读操作和写操作分离到不同的数据库上,避免主服务器出现性能瓶颈;
- 主服务器进行写操作时,不影响查询应用服务器的查询性能,降低阻塞,提高并发;
- 数据拥有多个容灾副本,提高数据安全性,同时当主服务器故障时,可立即切换到其他服务器,提高系统可用性;
2. redis主从
配从不配主
命令:
查看当前主机状态
127.0.0.1:6379[15]> info replication
# Replication
role:master
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
如何变成主机变成别的从机: slaveof 主库 ip 主库端口
SLAVEOF 127.0.0.1 6379
修改的内容:pid 端口 日志 dump文件
衍生概念
增量更新
全局更新
3. 投票
需要哨兵
- 创建sentinel.conf文件
sentinel monitor host6379(被监控数据库名字(自己起名字)) 127.0.0.1 6379 1
- 投票
./redis-sentinel sentinel.conf
主机断开后,投票选举新主机的过程
4. redis的集群模式
redis5以后开始有集群模式,直接配置即可
/data/airedis/cluster/bin/redis-cli --cluster create 10.4.xx.xx:6371 10.4..xx.xx:6372 10.4..xx.xx:6371 10.4..xx.xx:6372 --cluster-replicas 1 -a cmVkaXM=
集群模式的配置文件如下,其中cluster-replica-validity-factor 0
设置为零,副本将始终认为自己有效,cluster-require-full-coverage no
设置为 no,即使只能处理有关密钥子集的请求,集群仍将提供查询服务
protected-mode yes
port 6371
tcp-backlog 511
timeout 0
tcp-keepalive 300
daemonize yes
supervised no
pidfile /var/run/redis_6371.pid
loglevel notice
logfile /data/airedis/cluster/log/redis-6371.log
databases 16
always-show-logo yes
save 900 1
save 300 10
save 60 10000
stop-writes-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
dbfilename dump.rdb
dir /data/airedis/cluster/6371/data/
masterauth cmVkaXM=
replica-serve-stale-data yes
replica-read-only yes
repl-diskless-sync no
repl-diskless-sync-delay 5
repl-disable-tcp-nodelay no
replica-priority 100
requirepass cmVkaXM=
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
replica-lazy-flush no
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
aof-load-truncated yes
aof-use-rdb-preamble yes
lua-time-limit 5000
cluster-enabled yes
cluster-config-file nodes-6371.conf
cluster-replica-validity-factor 0
cluster-require-full-coverage no
slowlog-max-len 128
latency-monitor-threshold 0
notify-keyspace-events ""
list-max-ziplist-size -2
list-compress-depth 0
set-max-intset-entries 512
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
hll-sparse-max-bytes 3000
stream-node-max-bytes 4096
stream-node-max-entries 100
activerehashing yes
client-output-buffer-limit normal 0 0 0
client-output-buffer-limit replica 256mb 64mb 60
client-output-buffer-limit pubsub 32mb 8mb 60
hz 10
dynamic-hz yes
aof-rewrite-incremental-fsync yes
rdb-save-incremental-fsync yes
6. redis的缓存穿透
1. 定义
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
2. 如何解决
- 限制ip(运维角度)
- 将所有商品的商品id 放到集合中,当用户过来查询时,先判断要查询的商品是否存在集合中,如果集合中有对应数据,才进入下一步。
3. 缺陷
集合太大,假设10亿商品,每个商品id都是15位整型的id,需要大概500g的缓存,明显不可能, 如何解决?采用布隆过滤器。
4. 布隆过滤器
bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中。对于每个key,只需要k个比特位,每个存储一个标志,用来判断key是否在集合中。
算法:
- 首先需要k个hash函数,每个函数可以把key散列成为1个整数
- 初始化时,需要一个长度为n比特的数组,每个比特位初始化为0
- 某个key加入集合时,用k个hash函数计算出k个散列值,并把数组中对应的比特位置为1
- 判断某个key是否在集合时,用k个hash函数计算出k个散列值,并查询数组中对应的比特位,如果所有的比特位都是1,认为在集合中。
优点:不需要存储key,节省空间
缺点:
- 算法判断key在集合中时,有一定的误判概率(哈希冲突)
- 无法删除
典型的应用场景:
电商数据库查询、垃圾邮件过滤、爬虫去重
5. 信号锁
1. 计算这10个线程总耗时
public class CountDownLatchDemo {
// 信号锁:底层维护一个变量,当该变量不为0时,对应线程会进入到等待状态
// 该线程为0 时,线程接着往下执行
public static void main(String[] args) throws InterruptedException {
CountDownLatch cdl = new CountDownLatch(10);
MyResource mr = new MyResource(cdl);
long startTime = System.currentTimeMillis();
for (int i = 0; i < 10; i++) {
new Thread(mr).start();
}
cdl.await();
long endTime = System.currentTimeMillis();
System.out.println(endTime-startTime+"毫秒值");
}
}
class MyResource implements Runnable{
private CountDownLatch cdl = null;
public MyResource(CountDownLatch cdl){
this.cdl=cdl;
}
@Override
public void run() {
for (int i = 0; i < 5000; i++) {
System.out.println(i);
}
cdl.countDown();
}
}
7. redis的底层数据结构
redis的有序集合sorted set,也叫zset,通过跳表实现。
1. zset
1. 跳表(skip list)
概念
一款优秀的动态数据结构,支持快速的插入、删除、查找操作,实现也简单。
对链表建立索引,每若干个节点提取一个节点搭配上一级,down指针指向下一级节点。链表加多级索引的结构,就是跳表
跳表查询有多快?时间复杂度为O(logn),和二分查找的复杂度是一样的。前提是建立多级索引,空间换时间。实际软件开发,原始链表中存储的可能是很大的对象,而索引节点只需要存储关键值和几个指针,不需要存储对象,索引占用的额外空间可以忽略。
跳表索引的动态更新
假设不停的往跳表中添加数据,如果不更新索引,可能出现2个索引节点之间数据非常多,极端情况,退化为单链表。
如何维护索引和原始链表大小之间的平衡?
当往跳表中插入数据,可以选择同时将这个数据插入到部分索引层,加入到哪些索引层?
通过一个随机函数,决定这个节点插入哪几级索引,如随机函数生成值K,就将这个节点添加到第一级到第K级这K级索引中。
跳表的优点
相对于红黑树,跳表的实现非常灵活,可以通过改变索引构建策略,有效平衡执行效率和内存消耗。
跳表的实现相对简单,可读性高,不易出错。
2. zset的应用
排行榜
Redis Zincrby 命令对有序集合中指定成员的分数加上增量 increment
ZINCRBY key increment member
Redis Zrevrangebyscore 返回有序集中指定分数区间内的所有的成员,有序集成员按分数值递减(从大到小)的次序排列。
public class LOLBoxPlayer {
public static void main(String[] args) {
Jedis jedis = RedisUtils.getConnectionJedis("127.0.0.1", 6379);
Random rm = new Random();
String[] heros = {"易大师","德邦","剑姬","盖伦","阿卡丽","金克斯","提莫","猴子","亚索"};
while(true){
int heroNum = rm.nextInt(heros.length);
try{
Thread.sleep(1000);
jedis.zincrby("hero:phb",1,heros[heroNum]);
System.out.println("这哥们玩了一次"+heros[heroNum]);
}catch (Exception e){
e.printStackTrace();
}
}
}
}
public class LOLBoxViewer {
public static void main(String[] args) throws InterruptedException {
Jedis jedis = RedisUtils.getConnectionJedis("127.0.0.1", 6379);
int i = 1;
while (true){
Thread.sleep(2000);
System.out.println("第"+i+"次查看排行榜");
Set<Tuple> tuples = jedis.zrevrangeWithScores("hero:phb", 0, 2);
for (Tuple tuple : tuples) {
System.out.println(tuple.getElement()+"出现了"+tuple.getScore()+"次");
}
i++;
}
}
}
2. list的应用
消息队列,生产者生产task_queue,消费者消费,需要有监听,看是否消费成功,如果消费失败,需要将消息再次放入队列,确保成功
采用temp_queue
public class RedisUtils {
public static Jedis getConnectionJedis(String ip,int port){
return new Jedis(ip,port);
}
}
public class TaskProducer {
public static void main(String[] args) throws InterruptedException {
// 先模拟生产任务--产生一个任务id
String taskId = UUID.randomUUID().toString();
Jedis jedis = RedisUtils.getConnectionJedis("127.0.0.1", 6379);
while(true){
Thread.sleep(500);
// 往队列中推送数据
jedis.lpush("task_queue",taskId);
System.out.println("生产者已经生产了"+taskId+"条任务");
}
}
}
public class TaskConsumer {
public static void main(String[] args) throws InterruptedException {
Random rm = new Random();
Jedis jedis = RedisUtils.getConnectionJedis("127.0.0.1", 6379);
while(true){
int i = rm.nextInt(500);
Thread.sleep(500+i);
String taskId = jedis.rpoplpush("task_queue", "temp_queue");//保证了原子性
// 模拟成功失败
int randomNum = rm.nextInt(7);
if(randomNum==6){
// 7分之1的概率 执行失败
jedis.rpoplpush("temp_queue","task_queue");
System.out.println(taskId+"执行失败");
}else{
// 执行成功
jedis.lpop("temp_queue");
System.out.println(taskId+"执行成功,已在队列中弹出");
}
}
}
}
3. set的应用
微博利用redis求共同好友、不同好友
public class TestSet {
public static void main(String[] args) {
Jedis jedis = new Jedis("127.0.0.1",6379);
jedis.sadd("friends:xc","tom","jack","pony");
jedis.sadd("friends:jl","john","trump","jack");
// 不同好友
Set<String> sdiff = jedis.sdiff("friends:xc", "friends:jl");
// 所有好友
Set<String> sunion = jedis.sunion("friends:xc", "friends:jl");
// 共同好友
Set<String> sinter = jedis.sinter("friends:xc", "friends:jl");
System.out.println("不同好友:");
for (String s : sdiff) {
System.out.println(s);
}
System.out.println("所有好友:");
for (String s : sunion) {
System.out.println(s);
}
System.out.println("共同好友:");
for (String s : sinter) {
System.out.println(s);
}
}
}
redis为什么这么快
1. 内存存储
Redis 将数据存储在内存中,这使得数据读写操作非常快,因为内存的访问速度远远快于磁盘。
2. 简单的数据结构
Redis 提供了五种简单的数据结构:字符串(String)、哈希表(Hash)、列表(List)、集合(Set)和有序集合(Sorted Set)。这些数据结构是高度优化的,并且操作这些数据结构的时间复杂度通常是 O(1) 或 O(log N),这保证了操作的高效性。
3. reactor模型,单线程模型+IO多路复用
Redis 采用单线程模型处理客户端请求,避免了多线程的上下文切换和竞态条件问题,从而简化了实现并提高了性能。单线程模型依赖于高效的 I/O 多路复用技术,如 epoll
,来处理大量并发连接。
5. 高效的序列化和反序列化
Redis 使用自己的协议(RESP:Redis Serialization Protocol)来进行客户端和服务器之间的数据交换。RESP 协议设计简单且高效,减少了序列化和反序列化的开销。
6. 持久化机制
虽然 Redis 是内存数据库,但它提供了多种持久化机制(如 RDB 快照和 AOF 日志),可以将数据定期或实时地保存到磁盘。这些机制在设计上尽量减少对主线程的影响,从而保持高性能。
如采用BGSAVE非阻塞方式创建RDB文件
AOF的appendfsync选项拥有always、everysec和no,默认everysec,停机时最多只会丢失1s的数据
8. 管道和批处理
Redis 支持管道(Pipelining)和批处理,这允许客户端在一次请求中发送多个命令,从而减少网络延迟和提高吞吐量。
redis对象
redisobj
redis抽象出一个对象结构体robj,用来存储所有kv数据。
type有哪些?
encoding是对象的编码,也就是对象底层存储的数据结构,一共11种
在对象的整个生命周期中,编码不是一成不变的,如集合对象。当集合中的所有元素都可以用整数表示时,底层数据结构采用整数集合;当执行SADD命令向集合添加元素时,Redis总会校验待添加元素是否可以解析为整数,如果解析失败,则会将集合存储结构转换为字典
lru用于缓存淘汰策略
refcount引用计数,用来实现对象的共享,共享对象时,refcount+1,删除对象时,refcount-1,为0时,释放对象空间。
ptr字段,指向实际存储的数据结构,如果是long,直接存ptr中。
当robj存储的数据类型为字符串对象时,需分配两次内存,即为robj结构体与sds结构。这样会存在两个问题
- 两次分配效率低下
- 数据分离存储降低计算机高速缓存的效率
如何解决?
OBJ_ENCODING_EMBSTR编码,字符串内容较短时,只分配一次内存,robj和sds连续存储
String字符串
最基本的数据类型,它能存储任何形式的字符串,包括二进制数据。你可以用字符串类型存储用户的邮箱、JSON 序列化的对象甚至是一张图片。一个字符串类型键允许存储的数据的最大容量是512 MB(proto_max_bulk_len参数决定)。
为什么不直接用C语言的字符串?
缺陷:
- 长度计算效率低 C语言的字符串以空字符
\0
结尾,计算长度需要遍历整个字符串,时间复杂度O(N) - 缓冲区溢出风险 C语言没有内置的机制检查数组边界,容易缓冲区溢出
- 内存管理不灵活 C语言字符串在需要扩展或缩短时,需要手动重新分配内存,并复制已有数据。
- 二进制安全性:C语言字符串以
\0
结尾,因此无法直接存储包含空字符的二进制数据 - 多次分配内存效率低 动态构建字符串时,频繁的内存分配和释放操作会导致性能问题
sds.h里面,有三个属性,int length、int free, 还有一个char buff 数组,SDS的结构体:
struct sdshdr {
int len; // 字符串的长度,4字节
int free; // 剩余可用的空间,4字节
char buf[]; // 存储字符串数据的数组
};
redis3.2以后,又增加了字段flag标识类型,一个字节,低三位标识字符串类型,高五位 存储字符串长度,长度2^5-1=31
以sdshdr5(存储长度小于32位的短字符串)为例,结构体如下
对于长度大于31B的字符串,Redis会把len和free单独存放
- len:用2B存储buf中已占用字节数。
- alloc:用2B存储buf中已分配字节数。不同于之前的free,这里记录的是为buf分配的总长度。
- flags:标识当前结构体的类型,低3位用作标志位,高5位预留。
- buf:柔性数组,真正存储字符串的数据空间。
SDS的优点:
- 长度计算效率高 SDS直接存储了字符串的长度,获取长度时只需读取这个字段,时间复杂度为O(1)
- 防止缓冲区溢出 SDS在结构体中存储已分配的空间大小(
alloc
字段),在写入数据时可以检查是否有足够的空间,从而防止缓冲区溢出 - 灵活的内存管理: 可以根据需要自动扩展或缩短内存。每次扩展时,会分配比实际需求更多的内存空间,小于1M是两倍,大于1M的话,只加1M,避免浪费
- 惰性删除 空间依然给你保留,想恢复原来的空间就不用再额外分配了,空间预分配
其他数据结构的优化
哈希表(Hash)
优化技术:渐进式 rehashing:
rehashing 过程不会一次性完成,而是分摊到后续的增删改查操作中,避免了 rehashing 带来的性能抖动
列表(List)
优化技术:双端链表
- 压缩列表 是一种内存紧凑型的数据结构,用于存储小规模的数据
- 快速列表(Quicklist):Redis 3.2引入的数据结构,是一个双向链表,链表中的每个节点是一个ziplist结构
集合(Set)
- 整数集合(IntSet):
- 对于只包含整数的集合,Redis 使用整数集合(IntSet)来存储。这种数据结构是一个有序的整数数组,支持快速的查找、插入和删除。
- 哈希表:
- 对于较大的集合或包含非整数的集合,Redis 使用哈希表来存储。
- 优点:
- 灵活性:能够处理各种数据类型的集合元素。
有序集合(Sorted Set)
跳表(SkipList):
- Redis 使用跳表(SkipList)来实现有序集合。跳表是一种概率平衡的多层链表,能够在 O(log N) 时间复杂度内完成插入、删除和查找操作
内存淘汰策略
内存使用达到最大限制,决定移除哪些kv,释放空间
三种volatile,两种allkeys
volatile有volatile LRU、TTL以及random
- LRU,它是从已经设置的这个过期的时间的数据集里面选最近最少使用的这个数据淘汰。
- TTL是从这个设置过期时间的数据集里挑选就是集将即将要过期的这个数据
- random这个就是随机去挑选这个数据集进行淘汰
allkeys
- 一种也是LRU,对所有键使用 LRU 算法进行淘汰,无论是否有TTL,我们用的这个
- 另一种是random,随机删除
当然还有一种是No Eviction,不淘汰任何数据,写入直接报错内存不足,一般没人用
主从复制
2.8之前只有完全重同步
redis 2.8以后部分重同步
RUN_ID为主服务器的运行ID,OFFSET为复制偏移量。
部分重同步的要求:
- RUN_ID必须相等。
- 复制偏移量必须包含在复制缓冲区中
然而生产的常见场景无法执行部分重同步:
- 从服务器重启(复制信息丢失)
- 主服务器故障,导致主从切换,runId变更
redis 4.0进行优化:
- 持久化主从复制信息,存储在RDB
- 存储上一个master的复制信息,replid2和second_replid_offset,检查replid是否一致,或者检查slave的数据偏移量是否超出缓存数据偏移量的范围来确定是否部分重同步。
具体来说:
- 如果
master_replid
既不等于server.replid
,也不等于server.replid2
,则从节点的复制ID不匹配,需要进行全量同步。 - 即使
master_replid
等于server.replid2
,但如果psync_offset
大于server.second_replid_offset
,则超出了第二个复制ID的有效范围,也需要进行全量同步。
数据分区
槽slot,一致性hash分区,hash分区天然随机性,符合我们的想法,但是有个缺点,如果节点数量发生变化,所有映射需要重新计算,数据需要大规模迁移,如何避免,一致性hash。
将整个hash空间组织成一个虚拟的圆环,上面有多个节点,数据取余后,确定在圆环上的位置,沿着顺时针找到第一个节点就是要映射的节点,某个节点变化,只影响相近的节点的数据,其余节点不变。
节点增加:新增的节点只会影响其在哈希环上顺时针方向的第一个节点之前的数据
节点减少:被移除的节点上的数据会重新分配到哈希环上顺时针方向的下一个节点
有个缺陷,节点数量不多的时候,单节点存储的数据较多,节点发生变化后,对相邻节点影响大,数据容易负载不均衡。
redis引入虚拟节点的概念:圆环上的节点不再是实际的节点,而是虚拟节点,虚拟节点与实际节点还有一层映射关系,这样可以进一步平衡数据负载,这就是16384个slot的来历。
主从切换
1.主观下线A节点发送ping消息给B节点,若B节点超时未回应,则A节点会将B节点标记为pfail状态,并在下一次发送ping消息时,告诉其他节点—B节点被自己(A节点)标记为了pfail状态。此时,B节点在A节点的视角下为主观下线。
2.客观下线C节点收到A节点发送的、携带着B节点为pfail状态的ping消息后,会做一次计算,判断A节点是否需要客观下线。在C的视角,同时满足以下条件,则C节点会向全部节点发起一次广播,同步A节点的状态为fail,同步成功后,A节点客观下线。
- B节点目前也被C节点标记为pfail状态。
- 集群中标记B节点为pfail状态的节点超过了总主节点数的一半
预留500ms,让主节点为fail的消息能充分扩散至整个集群。之后就可以向其余主节点正式发起failover的投票
增加random,避免多个从节点同时发起选举,造成选举风暴,提高某个从节点的选举成功率。
通过currentEpoch自己的选票和configEpoch主节点的配置纪元,防止网络分区下,多个节点信息冲突。不知道哪个节点的配置最新。