redis
redis使用性能优异的原因
redis 是基于内存的采用的是单进程单线程模型的kv数据库,由C语言编写的,官方提供的数据是可以达到10W+的qps的(每秒查询次数)
- 完全基于内存,绝大部分的请求是纯粹的内存操作,非常快速,它的数据存在内存中 类似hashmap 的优势 让查找和操作的时间复杂度都是O(1)
- 数据结构简单,对数据操作简单。
- 采用单线程 避免了不必要的上文切换和竞争条件,也不存在多进程或者多线程切换导致的cpu小号,不用考虑各种锁的问题,不存在加锁释放锁的操作,没有因为可能出现死锁而导致的性能消耗
- 使用多路I/O 复用模型 非阻塞IO
- 使用底层模型不同,他们之间底层实现方式以及客户端之间通信的应用协议不一样 redis 直接自己构建了VM机制,一般的系统调用系统函数的话 会浪费一点的时间去移动和请求。
怎么解决单机瓶颈
使用集群的部署方式 redis cluster 并且是主从同步读写分离,类似mysql 的主从同步 。redis cluster 支撑N个redis master node 每个master node 可以挂载多个slave node
这样整个redis 就可以横向扩容了,如果要支持更大数据量的缓存 就横向扩容更多的master 结点 每个master结点就可以存放更多的数据了。
redis 怎么进行持久化的
- RDB : RDB持久化机制 是 对redis 中的数据执行周期性的持久化。
- AOF : AOF 机制对每个写入命令作为日志,以append-only的模式写入一个日志文件中,因为这个模式只追加的方式,没有任何磁盘寻址的开销。
两种方式都可以把redis内存中的数据持久化到磁盘上,然后再将这些数据备份到别的地方去,RDB更适合做冷备份,AOF更适合做热备份。
tip: 两种机制 全部开启的时候,redis 在重启的时候 会默认使用AOF去重新构建数据,因为AOF的数据是比RDB更完整的。
RDB 和AOF的优缺点
RDB
优点:
他会生成很多数据文件,每个数据文件分别都代表了某一时刻的redis里面的数据,这种方式,很适合做冷备份。
RDB对redis 的性能影响非常小,是因为在同步数据的时候他只是fork了一个子进程去做持久化的 而且他在数据恢复的时候比AOF来的快
缺点:
RDB都是快照文件,都是默认五分钟或者更久的时间才会生成一次,这意味着可能会造成数据丢失的问题,AOF则最多丢一秒的数据,数据完整性上高下立判
如果RDB在生成快照的时候 如果文件很大,客户端可能会暂定几毫秒甚至几秒。
AOF
优点:
AOF是一秒一次通过一个后台的线程fsync 操作,如果发送中断 最多丢失一秒的数据
AOF 在日志文件操作的是以append-only的方式去写的。他只是追加的方式写数据,减少了磁盘寻址的开销,文件也不容易破损
AOF的日志是通过一个叫非常可读的方式记录的,这样的特性适合做灾难性数据误删除的紧急恢复了,比如公司的实习生通过flushall清空了所有的数据,只要这个时候后台重写还没发生,你马上拷贝一份AOF日志文件,把最后一条flushall命令删了就完事了。
缺点:
一样的数据 AOF文件比RDB还要大
AOF开启后,Redis支持写的QPS会比RDB支持写的要低
怎么选择持久化方案
独用RDB你会丢失很多数据,你单独用AOF,你数据恢复没RDB来的快,真出什么时候第一时间用RDB恢复,然后AOF做数据补全,真香!冷备热备一起上。
redis 保证集群高可用的方式
哨兵集群sentinel
哨兵必须用三个实例保证自己的健壮性 哨兵+主从不能保证数据不丢失,但是可以保证集群的高可用。
哨兵组件的主要功能:
- 集群监控 : 负责监控redis master 和slave 进程是否正常工作
- 消息通知 : 如果某个redis 实例有故障 ,那么哨兵负责发送消息作为报警通知给管理员
- 故障转移 如果master node 挂掉了 会自动转移到slave node上
- 配置中心 如果故障转移发生了 通知clint 客户端新的master 地址
主从之间的数据怎么同步
你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。
过期策略
Redis的过期策略,是有定期删除+惰性删除两种。
定期好理解,默认100ms就随机抽一些设置了过期时间的key,去检查是否过期,过期了就删了
redis 内存淘汰机制
官网上给到的内存淘汰机制是以下几个:
-
noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)
-
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
-
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
-
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
-
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
-
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了。
多个系统同时操作redis 带来的数据问题(并发)
可以基于zookeeper来实现分布式锁。每个系统通过 Zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 Key,别人都不允许读和写。
你要写入缓存的数据,都是从 MySQL 里查出来的,都得写入 MySQL 中,写入 MySQL 中的时候必须保存一个时间戳,从 MySQL 查出来的时候,时间戳也查出来。
每次要写之前,先判断一下当前这个 Value 的时间戳是否比缓存里的 Value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。
双写一致性问题
使用缓存+ 数据库读写的模式 cap
-
读的时候 先读缓存,缓存没有在读数据库,然后取出数据放入缓存,同时返回响应
-
更新的时候 先更新数据库 在删除缓存
为什么是删除缓存,不是更新
如果这个表只是频繁的修改 而不是读取,那么就会浪费掉内存。
是一种懒加载的思想,使用到了 我在去添加缓存,
redis 和memcached 区别
redis 支持更复杂的数据结构:
redis 原生支持集群模式:
redis只是用单核 而memcached 可以使用多核,所以平均每个核redis在存储小数据比memcached性能更高。但是在100k以上的数据memcached则优于redis
redis 的线程模型
redis 内部使用文件事件处理器 ,这个文件事件处理器是单线程的。还采用io多路复用机制同时监听多个socket 根据socket上的时间来选择对应的事件处理器进行处理。
文件事件处理器的结构:
- 多个socket
- io多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器,命令请求处理器,命令回复处理器)
多个 Socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 Socket,会将 Socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
String (sds)
设置和获取键值对
当key存在的时候 set 会覆盖上一次设置的值
set key value
get key
删除键值对
exists key # 查询键是否存在
del key # 删除这个key
批量设置键值对
mset key1 value1 key2 value2
mget key1 key2
过期和 SET 命令扩展
setex key time value # 设置key value 的过期时间time 秒为单位
PSETEX key milliseconds value # 和上面的一样 毫秒为单位
set key value
expire key 5 # 在5秒钟后过期
setnx key value # 如果key 存在设置成功 如果key不存在设置失败
计数
可以使用incr 命令来进行原子性的自增操作
set cnt 100
incr cnt # cnt = 101
incrby cnt 50 # cnt = 101+50
DECR key # value -1
DECRBY key a # value -a
有返回值的set
set key value
getset key value1 #返回原值value 并且设置新值
"value"
其他
getrange key start end # 返回key中字符串的子字符
getbit key offset # 对key的字符串值 获取偏移量的位
DECRBY key decrement #如果key是一个字符串 则追加到value后面
使用场景
- 缓存功能 :String 是字符串最常用的数据结构,可以利用redis 作为缓存 配合其他数据库作为存储层,利用redis支持高并发的特点 大大加快系统的读写速度,降低后端数据库的压力
- 计数器 : 使用redis 作为系统的实时计数器,可以加速实现技术和查询的功能
- 共享用户的session:用户重新刷新一次界面 可能需要访问一下数据进行重新登录,或者访问页面缓存cookie 但是可以利用redis将用户session 集中管理,在这种模式只需要宝珠redis 的高可用,每次用户session的更新和获取都可以快速完成。
list (linkedlist)
基本操作
rpush key a
rpush key b
lpush key c
lrange key 0 -1 # -1 表示倒数第一个元素, 这里表示从第一个元素到最后一个元素,即所有
c->a->b
lpop key # 从左边弹出一个元素
rpop key # 从右边弹出一个元素
blpop key timeout #移除列表的第一个元素 如果没有元素就阻塞列表直到超时等待 或者发现可以弹出的元素
brpop key timeout # 弹出最后一个元素
brpoplpush # 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
lindex key index # 索引获取元素
llen key # 列表长度
lrange key start stop # 获取列表指定范围的元素
lrem key count value # 移除指定元素 count个
lset key index value # 通过索引设置元素的值
ltrim key start stop # 选取start-stop 区间的值 其他的删掉
应用场景
-
消息队列:redis 的链表结构 可以轻松实现阻塞队列,可以使用左进右出的命令完成队列的设计。
-
文章列表或者数据分页展示的应用。
比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,而且当文章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,可以完美解决分页查询功能。大大提高查询效率
hash
基本操作
hset key feild value # hset 键 字段 值
hget key feild # hget 键 字段 返回一个值
hgetall key # 获取所有的key value key和value 间隔给出
hmset key ddd "123" zzz "123" # 批量操作
hdel key feild # 删除key的一个字段 或者多个
hexists key field # 查看key中 的这个字段是否存在
HINCRBY key field increment # 给指定字段的整数+上increment
HINCRBYFLOAT key field increment # 给指定字段的浮点数整数+上increment
hkeys key #获取所有字段
hlen key # 获取字段的数量
hmget key feild #获取所有给定字段的值
HVALS key # 获取key 中的所有value
使用场景
将一些可以结构化的数据给缓存
set
sadd key value # set中增加一个元素 可以增加多个
smembers key # 输出key的所有元素 无序的
spop # 随机弹出一个
scard key #获取长度
SDIFF key1 [key2] # 差集
SDIFFSTORE destination key1 [key2] # 结果存在destination
SINTER key1 [key2] # 并集
SISMEMBER key member #判断 member 元素是否是集合 key 的成员
SMEMBERS key #返回集合中的所有成员
SMOVE source destination member #将 member 元素从 source 集合移动到 destination 集合
SRANDMEMBER key [count] # 返回集合中的一个或者多个成员
SUNION key1 [key2] #返回所有给定集合的并集
SSCAN key cursor [MATCH pattern] [COUNT count] #迭代集合中的元素
应用场景
- 查看各种集合的交集 并集
sorted set
zadd key sorce value
ZCARD key #获取有序集合的成员数
ZCOUNT key min max #计算在有序集合中指定区间分数的成员数
zrange key 0 -1 # 遍历所有 排序输出 升序
ZREVRANGE key 0 -1 # 降序输出
zcard key # 查询有几个元素
ZSCORE books "java concurrency" # 获取指定value的score
zrank key value #获取排名
zrangebyscore k 0 9 # 按score区间0-9 进行遍历
ZRANGEBYSCORE books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。
ZREM books "java concurrency" # 删除 value
应用场景
- 排行榜
- 用sorted set 来做带权重的队列,根据优先级取任务。
key
del key # 删除一个key
dump key # 序列化一个key 返回序列化的值
exists key # 查询key 是否存在
expire key seconds # 给key 设置过期时间 秒为单位
expireat key timestamp # 给key设置过期时间 时间搓
pexpirekey # 毫秒为单位
keys pattem # 查询符合条件的key
move key db # 将key 移动到db中
persist key # 移除key的过期时间
pttl key # 返回剩余的过期时间 毫秒单位
randomkey # 随机返回一个key
rename key newkey #修改key 的名称
renamenx key newkey #当newkey 不存在的时候 将key改名为newkey
scan cursor # 迭代数据库的数据库键
type key # 返回key的类型
HyperLogLog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的
PFADD key element #添加指定元素到 HyperLogLog 中。
PFCOUNT key [key ...] #返回给定 HyperLogLog 的基数估算值。
PFMERGE destkey sourcekey #将多个 HyperLogLog 合并为一个 HyperLogLog
发布订阅模式
Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
Redis 客户端可以订阅任意数量的频道。
下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y6Dcxlxk-1597733462767)(C:\Users\Administrator\Desktop\面试\redis图\订阅者模式1.png)]
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4HeljzHP-1597733462772)(C:\Users\Administrator\Desktop\面试\redis图\订阅者模式2.png)]
PSUBSCRIBE pattern [pattern ...] 订阅一个或多个符合给定模式的频道。
SUBSCRIBE channel 订阅给定的一个或多个频道的信息。
PUBSUB subcommand 查看订阅与发布系统状态。
PUBLISH channel message 信息发送到指定的频道。
PUNSUBSCRIBE [pattern #退订所有给定模式的频道。
UNSUBSCRIBE [channel [channel ...]] 指退订给定的频道。
事务
Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
MULTI #标记一个事务块的开始。
DISCARD # 取消事务,放弃执行事务块内的所有命令。
EXEC # 执行所有事务块内的命令。
UNWATCH # 取消 WATCH 命令对所有 key 的监视。
WATCH key [key ...] # 监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
服务器
| 序号 | 命令及描述 |
|---|---|
| 1 | BGREWRITEAOF 异步执行一个 AOF(AppendOnly File) 文件重写操作 |
| 2 | BGSAVE 在后台异步保存当前数据库的数据到磁盘 |
| 3 | [CLIENT KILL ip:port] [ID client-id] 关闭客户端连接 |
| 4 | CLIENT LIST 获取连接到服务器的客户端连接列表 |
| 5 | CLIENT GETNAME 获取连接的名称 |
| 6 | CLIENT PAUSE timeout 在指定时间内终止运行来自客户端的命令 |
| 7 | CLIENT SETNAME connection-name 设置当前连接的名称 |
| 8 | CLUSTER SLOTS 获取集群节点的映射数组 |
| 9 | COMMAND 获取 Redis 命令详情数组 |
| 10 | COMMAND COUNT 获取 Redis 命令总数 |
| 11 | COMMAND GETKEYS 获取给定命令的所有键 |
| 12 | TIME 返回当前服务器时间 |
| 13 | [COMMAND INFO command-name command-name …] 获取指定 Redis 命令描述的数组 |
| 14 | CONFIG GET parameter 获取指定配置参数的值 |
| 15 | CONFIG REWRITE 对启动 Redis 服务器时所指定的 redis.conf 配置文件进行改写 |
| 16 | CONFIG SET parameter value 修改 redis 配置参数,无需重启 |
| 17 | CONFIG RESETSTAT 重置 INFO 命令中的某些统计数据 |
| 18 | DBSIZE 返回当前数据库的 key 的数量 |
| 19 | DEBUG OBJECT key 获取 key 的调试信息 |
| 20 | DEBUG SEGFAULT 让 Redis 服务崩溃 |
| 21 | FLUSHALL 删除所有数据库的所有key |
| 22 | FLUSHDB 删除当前数据库的所有key |
| 23 | [INFO section] 获取 Redis 服务器的各种信息和统计数值 |
| 24 | LASTSAVE 返回最近一次 Redis 成功将数据保存到磁盘上的时间,以 UNIX 时间戳格式表示 |
| 25 | MONITOR 实时打印出 Redis 服务器接收到的命令,调试用 |
| 26 | ROLE 返回主从实例所属的角色 |
| 27 | SAVE 同步保存数据到硬盘 |
| 28 | [SHUTDOWN NOSAVE] [SAVE] 异步保存数据到硬盘,并关闭服务器 |
| 29 | SLAVEOF host port 将当前服务器转变为指定服务器的从属服务器(slave server) |
| 30 | [SLOWLOG subcommand argument] 管理 redis 的慢日志 |
| 31 | SYNC 用于复制功能(replication)的内部命令 |
缓存雪崩
问题描述
如果所有首页的Key失效时间都是12小时,中午12点刷新的,我零点有个秒杀活动大量用户涌入,假设当时每秒 6000 个请求,本来缓存在可以扛住每秒 5000 个请求,但是缓存当时所有的Key都失效了。此时 1 秒 6000 个请求全部落数据库,数据库必然扛不住,它会报一下警,真实情况可能DBA都没反应过来就直接挂了。此时,如果没用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。这就是我理解的缓存雪崩。
大量的key 在同一时间失效了,造成所有的请求都打在了数据库上 ,直接让数据库扛不住了。
解决策略
-
批量往redis 存数据的时候,将每个key的失效时间都加上一个随机数,这样保证了不会同一时间大量的key失效
setRedis(Key,value,time + Math.random() * 10000);
-
如果redis是集群管理的 ,可以将热点数据均匀分布在不同的redis库中,
-
设置热点数据永不过期,有更新操作就更新缓存。
缓存穿透和击穿
问题描述
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,而缓存中又没有这个key ,这样的攻击会导致数据库压力过大。
解决策略
- 可以在接口处增加校验:用户鉴权校验,参数校验。
- 如果从缓存取不到数据 在数据库也没有渠道数据 这个可以将对应的key的value设置会null 缓存7有效时间 可以设置为30s 。
- 可以使用nginx 配置 将每秒访问次数超过阈值的直接拉黑。
- 使用布隆过滤器。原理是 利用高效的数据结构和算法快速判断出你这个key是否在数据库存在,不存在你return 就好了,存在在查db 刷新kv 在return
问题描述
缓存击穿 和缓存雪崩有点像,缓存雪崩是大量的key失效,而缓存击穿只是一个非常热点的key,这个key在不停的扛着大并发。当这个key失效的瞬间,持续的大并发就穿破了缓存直接请求了数据库
解决方案
设置热点数据永远不过期,或者加上互斥锁
布隆过滤器
Bloom Filter原理
当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1,检索时 我们只要看这些点是不是1 就(大约)知道集合中有没有他了。如果这些点任何一个是0 则被检元素一定不在,如果都是1 则可能存在。
Boolm Filter 和单哈希函数Bit-Map 不同的地方是 Bollm Filter 使用了k个哈希函数 每个字符串 跟K个bit对应 从而降低了冲突的概率。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AfVUDI5r-1597733462858)(C:\Users\Administrator\Desktop\面试\redis图\Bloom Filter.jpg)]
一般我们用于防止缓存击穿。
bloom filter 缺点
Bloom Filter 之所以可以做到时间和空间上的效率高,因为牺牲了判断的准确性、删除的便利性
- 存在误判 可能要抄到的元素并没有在容器中,但是hash之后得到的k个位置上的值都是1,如果bloom filter 存的是黑名单,那么可以通过建立一个白名单来存储可能误判的元素。
- 删除困难 一个放入容器的元素映射到bit数组的k个位置上是1 ,删除的时候不能简单的直接为0 可能会影响其他元素的判断 可以采用 counting Bloom Filter
bloom Filter的实现
布隆过滤器有许多实现与优化,Guava中就提供了一种Bloom Filter的实现。
在使用bloom filter时,绕不过的两点是预估数据量n以及期望的误判率fpp,
在实现bloom filter时,绕不过的两点就是hash函数的选取以及bit数组的大小。
对于一个确定的场景,我们预估要存的数据量为n,期望的误判率为fpp,然后需要计算我们需要的Bit数组的大小m,以及hash函数的个数k,并选择hash函数
使用
-
引入包
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>23.0</version> </dependency>2.测试
public class TestBloomFilter { private static int total = 1000000; private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total); // private static BloomFilter<Integer> bf = BloomFilter.create(Funnels.integerFunnel(), total, 0.001); public static void main(String[] args) { // 初始化1000000条数据到过滤器中 for (int i = 0; i < total; i++) { bf.put(i); } // 匹配已在过滤器中的值,是否有匹配不上的 for (int i = 0; i < total; i++) { if (!bf.mightContain(i)) { System.out.println("有坏人逃脱了~~~"); } } // 匹配不在过滤器中的10000个值,有多少匹配出来 int count = 0; for (int i = total; i < total + 10000; i++) { if (bf.mightContain(i)) { count++; } } System.out.println("误伤的数量:" + count); } }
BloomFilter一共四个create方法,不过最终都是走向第四个。看一下每个参数的含义:
funnel:数据类型(一般是调用Funnels工具类中的)
expectedInsertions:期望插入的值的个数
fpp 错误率(默认值为0.03)
strategy 哈希算法(我也不懂啥意思)Bloom Filter的应用
错误率越大,空间和时间越小,错误率越小,空间和时间越大
应用场景:
- cerberus在收集监控数据的时候, 有的系统的监控项量会很大, 需要检查一个监控项的名字是否已经被记录到db过了, 如果没有的话就需要写入db.
- 爬虫过滤已抓到的url就不再抓,可用bloom filter过滤
- 垃圾邮件过滤。如果用哈希表,每存储一亿个 email地址,就需要 1.6GB的内存(用哈希表实现的具体办法是将每一个 email地址对应成一个八字节的信息指纹,然后将这些信息指纹存入哈希表,由于哈希表的存储效率一般只有 50%,因此一个 email地址需要占用十六个字节。一亿个地址大约要 1.6GB,即十六亿字节的内存)。因此存贮几十亿个邮件地址可能需要上百 GB的内存。而Bloom Filter只需要哈希表 1/8到 1/4 的大小就能解决同样的问题。
SDS(简单动态数组)
结构
struct sdshdr{
// 记录已经使用的字节数量
int len;
// 记录未使用的字节数量
int free;
// 字节数组 用来保存字符串
char buf[];
}
优点
- 常数复杂度的获取字符串的长度
- 使用 len字段记录字符串的长度 不需要每次遍历
- 杜绝缓冲区的溢出
- 使用空间分配策略杜绝移除的情况
- 如果需要对SDS进行修改,API会先检查sds的空间是否满足修改的要求,如果不满足 就自动将sds的空间扩展至执行修改所需要的大小,才会执行实际的修改。
- 减少修改字符串长度所需要的内存重分配次数
- 使用一个空间预分配的策略
- 当需要对sds进行修改 并且需要对SDS进行空间扩展的时候,程序不仅会为sds分配修改所需要的空间 还会为sds分配额外的未使用空间。
- 如果小于1M 那么 free的值就和len的值一样
- 如果大于1m 那么分配的空间就是 1m
- 惰性空间释放
- 当sds需要缩短sds保存的字符串的时候,程序不是立即回收内存重新分配来回收缩短后的字符串,而是使用free属性将这些数量记录起来。
- 使用一个空间预分配的策略
- 二进制安全
- 所有的SDS API 都会已二进制的方式来处理SDS存放在buf数组里面的数据。程序不会对其他的数据做任何限制。
- 兼容部分C字符串函数
list(链表)
结构
typedef struct listNode{
//前置结点
struct listNode *prev;
//后置结点
struct listNode *next;
//结点的值
void *value;
}listNode;
struct list{
// 表头结点
listNode *head;
//表尾结点
listNode *tail;
// 链表包含的结点数量
unsigned long len;
// 节点值复制
void *(*dup)(void * ptr);
// 节点值释放
void *(*free)(void * ptr);
// 节点值对比
void *(*match)(void * ptr,void *key);
};
优点
- 链表的结点由一个listNode结构来表示,每个结点都有一个指向前置结点的指针,链表是一个双向链表
- 每个链表使用一个list结构来表示,这个结构带有表头节点指针,表尾指针以及长度等信息。
- 链表的表头节点的前置结点和表尾的后置结点是都指向null 的所以 链表是一个无环链表
- 可以卫链表设置不同的类型特定函数,redis 的链表可以保存各种不同类型的值。
dict (字典)
结构
typedef struct dictht{
//哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
//哈希表大小掩码 用于计算索引的值
// 总是等于size-1
unsigned long sizemask;
//哈希表已有结点个数
unsigned long used;
}dictht;
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}v;
// 指向下个哈希表结点 形成链表
struct dictEntry *next;
}dictEntry;
typede struct dict{
//类型特定函数
dictType *type;
//私有数据
void *privdata;
//哈希表
dictht ht[2];
//rehash 索引
//当没有rehash 的时候为-1
int rehashidx;
}dict;
rehash(扩展和收缩)
随着操作的不断执行,hash表的保存的键值对都会逐渐的增多或者减少,为了让哈希表的负载因子维持在一个合理的范围。程序会对哈希表的大小进行一个扩展或者收缩的操作。
redis的哈希表执行的rehash 的步骤:
- 为字典的ht[1]哈希表分配空间,
- 如果执行的是扩展操作的时候 那么ht[1]的大小是第一个大于ht[0].used * 2的 2^n ;
- 如果执行的是收缩的操作 那么ht[1]的大小为第一个大于等ht[0].used的2^n;
- 将保存在ht[0]的所以键值对rehash到ht[1]上面
- 当ht[0]包含的所有键值对都迁移到ht[1]之后 释放ht[0],将ht[1]设置为ht[0]并且在ht[1]新创建一个空白的哈希表
哈希表的扩展与收缩
当以下的条件任意一个都满足 程序会自动开始对哈希表的执行扩展操作
1. 当服务器目前没有执行bgsave 或者 bgrewriteaof命令 并且哈希表的负载因子大于等于1
2. 服务器目前正在执行bgsave 或者bgrewriteaof命令 并且哈希表的负载因子大于5
3. 负载因子等于 哈希表已经保存的结点数量/哈希表大小
当 哈希表的负载因子小于0.1的时候 程序自动为哈希表进行一个收缩操作。
渐进式rehash
步骤:
1. 为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表
2. 在字典中维护一个索引计数变量rehashidx,并且将他的值设置为0,表示rehash工作正式开始
3. 在rehash期间 每次对字典执行添加,删除,查找或者更新操作的时候程序出了执行指定的操作外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对都rehash到ht[1]上 ,当rehash工作完成 将索引+1
4. 随着字典操作的不断进行 最终ht[0]的所有键值对都会被rehash到ht[1]上这时 rehashidx属性的值设置为-1 表示rehash操作完成。
5. 在渐进式rehash期间 字典的删除 查找 更新操作 都会在两个哈希表上进行,而新增这时在ht[1]里面保存
优点
- redis中的字典使用哈希表作为底层实现,每个字典带有两个哈希表,一个平时使用,一个rehash的时候使用
- redis使用murmurHash2算法来计算键的哈希值
- 哈希表使用链地址法来解决哈希冲突,头插
- 对哈希表的扩展或者收缩操作时,程序需要将现有的哈希表包含的所有键值对都rehash到新的哈希表里面,这个rehash的过程不是一次性完成的 而是渐进式的。
zskiplist(跳跃表)
结构
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
}level[];
// 后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *robj;
}zskiplistNode;
typdef struct zskiplist{
//头结点和尾节点
struct zskiplistNode *header ,*tail;
// 表中结点的数量
unsigned long length;
//层数
int level;
}
用处
- 跳跃表是有序集合的底层实现之一
- 每个跳跃表结点的层高都是1-32之间的随机数
- 在同一个跳跃表中,多个结点可以包含同样的分数,但是每个结点的成员对象必须是唯一的
- 跳跃表的结点按照分值大小排序,分值相同按照成员对象的大小排序
intset (整数集合)
结构
typedef struct intset{
uint32_t encoding;
uint32_t length;
//保存元素的数组
int8_t contents[]
}intset;
升级
- 根据新元素的类型 扩展整数集合底层数组的空间大小,并且为新元素分配空间
- 将底层数组现有元素都转换成与新元素相同的类型,并且还是保持有序
- 添加新元素
好处
1. 提升灵活性
2. 节约内存
不支持降级
ziplist 压缩列表
结构
重点
- 压缩列表是为了节约内存而开发的顺序数据结构
- 作为列表键和哈希键的底层实现之一
- 可以包含多个结点,每个结点可以保存一个字节数组或者整数值
- 添加新的结点到压缩列表,或者删除可能会引起连锁更新,但是几率不高。
多机数据库的实现
复制
从服务器发起slaveof 主服务器 那么主服务器的数据会同步到从服务器以只读的方式
同步
当客户端向从服务器发送slaveof命令,要求服务器复制主服务器时,从服务器首先需要执行一个同步操作,就是说需要将服务器的数据库状态更新到主服务器所处的数据库状态
从服务器对主服务器实现同步操作 需要通过像主服务器发送一个sync命令来完成
步骤:
1. 从服务器像主服务器发送sync命令
2. 收到sync命令的主服务器执行bgsave命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令
3. 当主服务器的BGSAVE命令执行完毕时,主服务器会将bgsave命令生成的RDB文件发给从服务器,从服务器接收并且载入这个RDB文件,将自己的数据库状态更新到主服务器执行BGsave命令时的数据库状态
4. 主服务器将记录在缓冲区里面的所有谢明令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新到主服务器数据库当前所处的状态。
新版复制
为了解决旧版复制在处理断线重新复制的情况低效问题,使用PSYNC命令代替了SYNC命令。
PSYNC具有完整重同步和部分重同步两种模式:
1. 其中完整重同用于处理初次复制情况,完整重同步和发送SYNC命令步骤一样
2. 部分重同步则用于处理断线后重复制情况,当从服务器在断线后重新连接主服务器时,如果条件允许,主服务器可以将主从服务器断开连接期间执行的写命令发送给从服务器,从服务器只要接受并执行这些写命令,就可以更新一致了。
心跳检测 在命令传播阶段 从服务器会每秒一次的像主服务器发送命令
分布式锁
本质上是利用redis的setnx命令来实现 setnx key value 如果key不存在就代表设置成功 返回1 如果失败 就返回0 此时我们可以把key 当成锁 value可以用作验证用有锁的线程。
加锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kKVFSOEX-1597733462860)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200814213652768.png)]
解锁
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X6SS571u-1597733462862)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\image-20200814215121011.png)]

1066

被折叠的 条评论
为什么被折叠?



