Redis学习总结

578 阅读18分钟

Redis基本数据类型

String

可以用来存储int(整数)、float(单精度浮点数)、String(字符串)

存储模型

以set hello world 为例

key

key是字符串,Redis自己实现了一个字符串类型:SDS,hello使用SDS进行存储。

SDS的优点:

  1. 不用担心内存溢出,如果需要可以进行扩容。
  2. 获取字符串长度的时间复杂度为O(1),因为定义了len属性。
  3. 通过空间预分配与惰性空间释放,防止多次重分配内存。
  4. 判断是否结束是len属性,可以包含‘\0’。

value

value使用的RedisObject对象进行存储(Redis中五种基本数据类型的value都是使用RedisObject进行存储)

底层数据结构

使用type key可以查询value对应的存储类型

  1. int:存储8个字节的长整型(long,2^63-1)
  2. embstr:SDS的格式之一,存储小于44个字节的字符串
  3. raw:SDS的格式之一,存储大于44个字节的字符串

embstr,raw的区别:

  1. embstr的RedisObject和SDS是连续的,只需要分配依次内存空间,
    而raw的RedisObject和SDS是不是连续的,所以需要分配两次内存空间。
  2. embstr是只读的,如果被修改则转为raw进行存储。

当长度小于阈值时,是否会自动转换编码:
不会,编码转换在数据写入时完成,且转换过程不可逆,只能由小内存编码转大内存编码(除非是重新set值)。

Hash

Hash存储键值对,最大值是2^32-1

Hash与String的区别:

  1. Hash将所有key相关的值聚集到一个key中,节省存储空间。
  2. 只使用一个key,减少key的冲突。
  3. 当需要批量获取时只需要一个命令,减少I/O消耗。

Hash的缺点:

  1. Field不能单独设置过期时间。
  2. 由于field都聚集到一起,也就无法分布到多个节点。

底层数据结构

ziplist(压缩列表)

连续内存组成的双向链表

什么时候使用ziplist?

  1. 键值对数量少于512个。
  2. 所有键值对的键和值的字符串长度都低于64bytes。 如果超过一个条件编码类型就会变为hashtable

hashtable(哈希表)

数组+链表结构

如上图所示,一个value中有两个哈希表,ht[0]中存储数据,但ht[1]中为null。
Redis默认使用的是ht[0],h[1]不会初始化和分配空间。h[1]是用来rehash扩容的

rehash扩容
  1. 为字符ht[1]分配内存空间。
  2. 将ht[0]上所有节点重新rehash到ht[1]上,重新计算hash值和索引,并放到对应位置。
  3. 当所有数据迁移完毕,清空ht[0],并设置ht[1]为ht[0],创建新的ht[1]。

总结下

  1. String的底层使用INT,embstr,row
  2. Hash底层使用了ziplist,hashtable

list

早期版本:数据较少时使用ziplist存储,达到临界值时转换成linklist存储
3.2版本以后,统一使用quicklist存储
主要用来存储有序数据。

quicklist

quicklist是一个双向链表+数组的结构。

  1. head:指向双向列表的表头;
  2. tail:指向双向列表的表尾;
  3. count:所有ziplist中存储了多少元素;
  4. len:双向列表的长度,即node的数量;

Set

存储不重复,无序数据
如果元素都是整数型用inset存储,个数超过512会用hashtable存储
如果不是整数型就用hashtable存储

应用场景:点赞,签到,打卡,商品标签

Zset

有序不重复集合。 每个元素都有个score属性,按照score进行从小到大排序,score相同时,按照key的ASCLL码排序。

数据结构对比

底层数据结构

默认使用ziplist
如果元素个数大于128个,或者任意member长度大于64字节就会转成skiplist+dict存储。

skiplist

随机选择一个元素成为level元素,带有指向对应level的下个节点

应用场景:百度热搜,微博热搜

Bitmap

是在字符串上面定义的位操作,一个字节由8个二进制组成。

Hyperloglogs

不太精准的基数统计方法,用来统计一个集合中不重复的元素个数。

Geo

存储位置信息,有对应的位置操作api

Streams

支持多播的可持久化的消息队列,用于实现发布订阅功能。

总结

事务

Redis提供了事务功能,可以把一组命令一起执行。

特点:

  1. 开启事务后,客户端发送的命令不是立刻执行,而是放到队列中。
  2. 按照进入队列的顺序执行。
  3. 不会受到其他客户端请求的影响。
  4. 事务不能嵌套,多个multi命令效果一样。

相关命令:

  1. multi(开始事务)
  2. exec(执行事务)
  3. discard(取消事务)
  4. watch(监视)/unwatch(取消监视):监视一个或者多个key,防止其被其他客户端修改,如果被修改,事务会被取消。

发生异常时的回滚:

  1. 在exec执行前发生异常:命令存在异常,事务不会执行,所有命令都不会被执行。
  2. 在exec执行后发生异常:运行时发生异常,事务不会回滚,已经执行的命令会生效。不能满足原子性的定义。

Lua脚本

轻量级脚本语言,批量执行命令,保证原子性

优点:

  1. 一次发送多个命令。
  2. Redis会将整个脚本作为一个整体执行,不会被其他客户端请求影响,保证原子性
  3. 对于复杂的组合命令,可以利用lua来实现命令的复用。

lua脚本缓存

script load "return 'hello world'"

>evalsha

脚本超时

脚本有个默认的超时时间,5秒

中止脚本

  1. script kill:脚本没有修改操作(set,del)
  2. shutdown nosave:脚本有修改操作(set,del)

redis为什么这么快

  1. 纯内存结构,时间复杂度O(1);
  2. 请求单线程;
  3. 同步非阻塞I/O:多路复用机制;

单线程

这里说的单线程是指的处理客户端请求是单线程的,在4.0版本之后,redis引入了其他线程去处理其他事情,如清理脏数据,无用连接,清理大key等。

优点:

  1. 没有创建、销毁线程带来的消耗;
  2. 避免上下文切换带来的CPU消耗
  3. 避免线程之前带来的竞争问题,例如加锁释放锁,死锁等。

注意:因为请求是单线程的,不要在生产环境运行长命令,比如save,keys *,flushall,flushdb否则会导致请求堵塞

多路复用

多路:多个TCP连接(Socket或者Channel)
复用:复用一个或者多个线程

问:select和epoll的区别?

答:

  1. select采用无差别轮询所有FD,且FD数量受限(32位1024个,64位2048个);
    epoll采用事件监听方式触发,FD数量无限制,在FD数量多时epoll效率高,少则select更优。
  2. select需从内核空间复制到用户空间,性能消耗比较大; epoll数据是放在内核空间和用户空间共用的内存中,效率较高。

redis内存回收机制

LRU:删除最近最少使用
LFU:删除最不常使用,按使用频率删除
Random:随机删除
volatile:针对设置过期时间的key
allkeys:针对所有key

Redis的LRU

Redis的每个对象都有个lru属性24位字节LRU_BITS,用来记录当前对象最后一次被访问的时间。当对象被创建时会被赋值,在被访问时也会更新对应的值。

这个lru记录的时间不是当前的系统时间,而是redis的server.lruclock全局变量(自己定时更新时间),主要是为了提升效率。

Redis的LFU

基于访问频率的淘汰机制
LRU_BITS用作LFU时,高16位用来记录访问时间,低8位用来记录访问频率。
同时会有一个定时器,当这个对象一段时间没有被访问就会减少。

redis持久化机制

RDB

RDB是Redis默认的持久化方案(当开启AOF时,优先使用AOF),当满足一定条件会把当前内存中的数据写入磁盘,生成一个快照文件dump.rdb。Redis会通过dup.rdb来恢复数据。

自动触发

  1. 配置的规则
  2. shutdown
  3. flushall

手动触发

  1. save:生成当前内存快照,会阻塞Redis,redis不能处理其他命令,如果数据较多,会造成较长时间阻塞。
  2. bgsave:fork一个子进程生产快照,不会记录fork之后的数据。阻塞发生在fork阶段,一半很短

优势

  1. RDB是一个非常紧凑的文件,非常适合用来备份和恢复。
  2. 生成RDB文件时,fork子进程进行处理,主进程不需要进行大量I/O操作,
  3. RDB在恢复数据的时速度比AOF快

劣势

  1. RDB没有办法做到实时持久化,且bgsave需要fork子进程,频繁执行成本过高。
  2. 如果redis挂掉,会丢失最新没有备份的数据

如果数据比较重要还是通过AOF进行备份。

AOF

默认不开启,将每个更改的命令都追加到文件中。Redis重启时,会把每个命令从前往后执行一次。

同步到磁盘的机制

重写机制

防止AOF文件越来越大,调用bgrewriteaof

  1. 当aof文件超过上一次AOF文件的百分之多少进行重写。
  2. 当aof文件到达最小文件大小。

优势

aof同步频率比rdb高的多

劣势

  1. aof文件比rdb文件大
  2. 在高并发下,rdb比aof性能更高

两种机制一起用,redis优先使用aof恢复数据,因为aof数据更完整。

Redis分布式

主从复制

主节点读写数据,从节点只能读,不能写

缺点

解决数据备份和一部分的性能问题,但是没有解决高可用问题(主节点挂了,对外不可用)。

Sentinel

启动奇数个sentinel监控redis节点,且互相监控。

选举master的基本流程:
在sentinel中基于raft协议选举一个leader,然后这个leader根据以下几个条件去选择一个节点成为master。

  1. 节点与哨兵断开连接过久就直接排除选举;
  2. 查看优先级,优先级高的中选;
  3. 优先级一致查看复制偏移量;
  4. 偏移量一致查看进程id,id最小中选。

master向其他节点发送slaveof no one 命令让他成为独立节点,然后发送slaveof x.x.xx.x成为master的从节点。

缺点

  1. 主从切换会丢失数据。
  2. 只能单点写,没有解决水平扩容问题。

分片

ShardingJedis

一致性哈希(哈希环)

把所有的哈希值空间组成一个圆环,整个空间顺时针组成,因为是环的,0-2^32-1重叠。
将数据放到比key的hash值大的第一个节点。

优点:

  1. 一致性哈希解决了动态增减节点数据需要重新分布的问题,他只会影响到下一个相邻的节点,对其他节点没有影响。
  2. 但是节点较少时,数据分布不均匀,但是在引入虚拟节点后这个问题也解决了。
  3. 不依赖其他中间件,分区逻辑可自定义。

具体实现:节点被放入红黑树中,当存取键值时,计算键的哈希值,然后从红黑树上找个比这个值大的第一个节点

代理

将分片的策略代码抽取出来,做成一个公共的服务,所有的客户端都连接到这个代理,由代理进行请求和转发。

Twemproxy

优点:稳定,可用性高 缺点:依赖其他组件,出现故障不能自动转移。

Redis Cluster

高可用,去中心化,客户端可以连接到任意节点。
节点之间两两交互,共享数据分片,节点状态等信息。
客户端不关心数据到底存在哪个节点,只需要关注整个集合整体。

数据分布

redis cluster没有采用哈希取模,也没有用一致性哈希,而是用虚拟槽来实现的。 具体实现:

  1. redis创建16384个虚拟槽,每个节点负责一定区间的slot。
  2. 对象进来的时候先对key用CRC16%16384取模,得到一个slot值,数据落到负责这个slot的redis节点上。

问:怎么让相关的数据落到同一节点上? 答:在key最加{hash tag} 。Redis在计算槽编号的时候只会获取{}之间的字符进行槽编号计算。

数据迁移

key和slot的关系不会变,当出现新的节点的时候,将属于这个节点slot对应的数据也要迁移到该节点上。

高可用与主从切换原理

当出现master挂点后:

  1. slave发现自己的master挂掉;
  2. 将自己记录的集群currentEpoch加1,并广播failover_request;
  3. 其他节点收到信息,只有master响应,判断合法性,返回failover_ack,对每个epoch只发送一个ack;
  4. 发起的slave收集ack;
  5. 超过半数则成为master;
  6. 广播Pong通知其他集群节点。

总结

  1. 无中心架构。
  2. 数据按照slot分布在多个节点,节点直接数据共享,可动态的调整数据的分布。
  3. 可扩展性,可线性的扩展到1000个节点,节点可动态的添加删除。
  4. 高可用,部分节点不可用,集群仍然可用。通过增加slave做standby数据副本,能够实现故障自动failover。
  5. 降低运维成本,提高系统的可扩展性和可用性。

Redis客户端

Jedsi

在springboot2.x版本之前默认使用的是Jedsi

缺点: 多线程使用一个连接的时候不安全。
解决版本: 连接池,为每个请求创建不同的连接。

连接池:JedisPool(普通连接池),ShardedJedisPool(分片连接池),JedisSentinelPool(哨兵连接池)

Cluster连接原理

使用Jedis连接Cluster时,我们只需要连接到任意一个或者多个redis group的实例地址,那我们怎么获取到应该操作的master实例呢?
为了避免get,set时发生重定向错误,我们需要把slot与Redis节点的关系保存起来,在本地计算key应该保存在哪个slot中,然后获取对应的redis节点。

如何保存slot与Redis连接池的关系?

  1. 程序启动初始化集群环境时,读取配置文件中的节点配置,无论是主是从,无论是多少,只拿一个,获取对应的redis实例。
  2. 获取该redis连接实例对应的虚拟槽信息。
  3. 根据虚拟槽信息获取所有槽点值
  4. 获取对应的主节点,构建hostAndPost对象。
  5. 根据hostAndPort拿到缓存中对应的JedisPool信息。如果没有则创建,并存入。
  6. 将slot值与JedisPool存入Map<String,JedisPool>(key是slot的下标,value是连接池)。

获取时,根据key计算得到对应的slot值,从map中获取到对应的jedisPool实例,根据jedisPool拿到对应的Jedis实例,完成存取工作。

Redis分布式锁实现

加锁:利用Hash(key锁名称,value(线程Id,过期时间,获取锁等待时间,重入次数)存入线程Id,过期时间,获取锁的等待时间。判断对应的线程Id的key是否存在,不存在则直接创建锁,存在则判断是否是一个线程Id,是则重入,不是则等待。

释放锁:判断key的线程Id是否与当前的相同,相同则重入次数减一,为0直接释放锁(del)。不是则不管。

加锁释放锁都需要用Lua脚本去实现,防止在高并发情景下的不安全问题。

Pipleline

客户端需要批量发送多个命令,将这些命令缓存起来,这些命令大小超过8M就会一起发送过去。服务端逐个执行并一起返回结果。

有些场景,例如批量写入数据,对于结果的实时性和成功性要求不高,就可以用Pipeline。

Lettuce

springboot 2.x默认客户端。无线程安全问题,基于Netty,支持所有功能。

Redission

提供了分布式锁的api,实现思路同上。且有看门狗功能(定时给key续命)。

数据一致性

Redis和mysql之间是没有事务功能的。
当缓存的数据需要被修改时,是先修改数据库还是修改缓存?

先更新数据库,再删除缓存

异常情况:更新数据库成功,删除缓存失败
解决办法:提供重试机制

  1. 删除缓存失败,把需要删除的key发送给消息队列,通过消费者去删除。 缺点:会造成代码入侵。
  2. 因为更新数据库时会往binglog中写入日志,所以我们通过一个服务监听binlog的变化(比如阿里的canal),然后在客户端完成删除key的操作。如果删除失败再发送到消息队列中。

先删除缓存,再更新数据库

异常情况:并发情况下出现ABA的问题,A删除缓存还没有更新数据库时,B就重新查询数据库将值写入缓存。 解决办法:延时双删,在写入数据后再删除一次key.

高并发问题

如何发现热点数据

客户端

统计key的次数

问题:

  1. 对客户端代码有侵入
  2. 不知道有多少key的存入,可能会造成内存泄漏
  3. 只能统计当前客户端的热点key

创建一个统计用的服务端,客户端统一发送key信息给服务端做统计。

服务端

Redis有个monitor命令,可以监控到所有Redis执行的命令。

问题:

  1. 高并发有性能问题,不适合长时间使用
  2. 只能监听一个redis节点的信息

机器层面

tcp协议抓包。

缓存雪崩

大量热点数据因为设置同样的过期时间同时过期(失效),而这时的并发又比较高,导致所有的请求落入到mysql中。

解决方案:

  1. 加互斥锁或者使用队列,针对同一个key只允许一个线程到数据库查询。
  2. 缓存定时预先更新,避免同时失效。
  3. 通过加随机数,避免同时失效。
  4. 不设置过期时间。

缓存穿透

频繁查询不存的数据。

  1. 将不存在的值也缓存到redis中,但也有个问题,如果是恶心的攻击,每次都是查询不同的值,如果都放到redis中存储,就会导致redis存储大量垃圾数据。
  2. 布隆过滤器。项目启动时,将数据存入到布隆过滤器中,查询redis时,先去布隆过滤器中判断是否存在,如果不存在,则不去查询redis和数据库。

布隆过滤器

一个0,1数组,加诺干个哈希函数。

相关面试题:如何在在海量数据中(10亿,无序,不重复,不定长)快速判断一个元素是否存在?

原理

存:假设有三个哈希函数,将这个元素的分别哈希三次%数组长度,分别得到三个位置,标记数组上这个三个位置为1。

判断一个元素e是否存在,假设有三个哈希函数,将这个e哈希三次%数组长度,分别得到三个位置,如果这三个位置的数组元素都是1,则判定这个e可能存在,否则一定不存在。

总结

  1. 如果布隆过滤器中判断元素不存在,则一定不存在。
  2. 如果布隆过滤器存在,则可能存在

不足:没有删除,如果一个元素在数据库中删除了,没有办法在布隆过滤器中删除。
解决办法:类似HashMap的链式地址法,给每个位置的下标增加一个计数器,如果命中了两次则为2,删除则-1,布谷鸟过滤器已经实现相关功能。