Redis基础知识

一、Redis基础

1.Redis为什么快

  • 纯内存KV操作:减少了一些不必要的 I/O 操作。
  • 单线程操作:省去多线程时CPU上下文切换的时间,也不用去考虑各种锁的问题。(机器内存和网络带宽才是瓶颈)
  • 高效的数据结构:底层多种数据结构支持不同的数据类型,使得数据存储时间复杂度降到最低。
  • I/O 多路复用:多个客户端的I/O请求,当某个socket(客户端连接)的请求可读或者可写的时候,它会给一个通知,达到一个线程同时处理多个IO请求的目的。

注:Redis 单线程是说一个线程处理所有网络请求,接收到用户的请求后,全部推送到一个队列里,然后交给文件事件分派器。后面的依旧是多线程。

注2:I/O :网络 I/O,多路:多个 TCP 连接,复用:共用一个线程或进程。也就是多个客户端的I/O操作复用一个线程。

注3:IO多路复用是指内核一旦发现进程指定的一个或者多个文件描述符IO条件准备好以后就通知该进程。

注4:Redis6.0 之后引入了多线程,处理命令还是单线程,网络数据的读写这类耗时操作上是用多线程。

2.Redis与Memcached

2.1 概述:

Memcached :是高性能分布式内存缓存服务器,本质一个key-value 数据库,但不支持数据持久化,服务器关闭后全丢失。只支持key-value结构。

Redis:将大部分数据放在内存中,支持的类型有:字符串、hash、list、set、zset。

2.2 区别

两者都是内存数据库,数据都在内存。不过memcache还可用于缓存其他东西,例如图片、视频等等

1.Redis数据类型更丰富;

2.Redis支持持久化,挂掉后可以通过aof文件进行恢复,而memcache不行;

3.Redis还可以用来做消息队列、数据缓存等,而memcache主要用来做SQL语句缓存、用户临时数据缓存等;

4.Redis支持数据备份,即master-slave模式的主从数据备份;

5.Redis并不是所有数据all存在内存中,当物理内存用完时,可以将一些很久没用到的value 交换到磁盘。

3.数据结构

Redis 有8?种数据结构,主要由5种最基本的数据结构(String、List、Hash、Set、zset) 加 HyperLogLogs(基数/不重复元素 统计)、Bitmap (位存储)、Geospatial (地理位置)。

注:最新的可能不长这样,最新的有QuickList,结合了双端队列和压缩链表的优点,细节可以见 这里

粗略帮助记忆:

String:C的char*不高效,所以用了sds。

List:有序队列,所以用双端队列。

Hash:Hash和压缩链表。

Set:元素不重复,用hash结构(不重复)。

Zset:有序列表,所以用跳表。

3.1 String

动态字符串SDS(Simple Dynamic String)。String只用SDS数据结构。

简单说就是:1.用一个 len 字段记录当前字符串的长度,想要获取长度只需要获取 len 字段即可,而传统C语言需要遍历字符。2.每次空间预分配double空间 3.字符串缩短时不直接回收,记录到free里面,后续操作直接用free对应的空间。

注:Redis基于C语言开发,但是没用char*类型,引入了sds。因为c里面判断数组长度得遍历到 ‘\0’,sds可以直接知道长度。

  • 空间预分配对 SDS 修改或扩充时,会额外分配未使用的空间。len长度小于 1M,double。如果修改后长度大于 1M,那么将分配1M的使用空间。

  • 惰性空间释放SDS 缩短时,并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。

用途:平常的存储token等

3.2 List

List用双端列表和压缩列表两种数据结构。双端列表只有List用。

列表 List 更多是被当作队列或栈来使用。底层是用的双端队列:链表中每个节点有两个指针,prev 指向前节点,next 指向后节点。

1.头结点有len长度,能知道链表长度。2.头节点里有 head 和 tail 两个参数,分别指向头节点和尾节点,方便对两端操作。

用途:消息队列。

3.3 Hash

List、Hash、Zset都会压缩列表。

类似于双端链表,只不过双端列表需要存储前后指针等额外的数据。所以用压缩列表。

所有的操作通过指针与解码出来的偏移量进行的。并且压缩列表的内存是连续分配的,遍历的速度很快。

注:压缩列表可以理解成数组的升级版,只不过数组每个元素空间大小固定(取最大元素的长度作为每个元素空间大小,存储不同数据可能有空隙),压缩列表更紧凑。所以每个entity会存储上一元素和当前元素的大小(用于遍历的时候区分不同元素)。

  • 好处:极大的节省了内存空间
  • 缺点:不能存储太多的元素,否则遍历效率就会很低;其次,新增或者修改某个元素时,会重新分配内存,甚至可能会引起连锁更新(每个entity记录上一节点元素大小的字段是一样的,如果跨数量级了就会连锁更新)。

用途:存储对象。

3.4 Set

用hash数据结构,能够在 O(1) 时间复杂度内取出和插入关联的值。

用途:去重

3.5 Zset

用跳跃表和压缩列表两种数据结构。跳跃表只有Zset用。

跳跃表,其在链表的基础上增加了多级索引来提升查找效率。

简单的说就是多级链表,方便快速查找。查找时间复杂度 O(logN)。

用途:排行榜

4.分布式锁

利用setnx和getset命令。Getset 命令用于设置指定 key 的值,并返回 key 的旧值。

原来分布式锁实现会比较复杂:1.要考虑锁不释放,引入expire 2.setnx和expire非原子操作,引入自动检测是否过期,过期了则getset占用锁 3.需要考虑是否会删除别人的锁(超时挂起了被别的线程占用了,恢复后把锁删了)(解决:存储时增加标记,删除判断标记是否是自己的)

注:最新的setnx和expire官方提供了原子操作,所以不用考虑后续了。

5.过期数据删除策略

  • 惰性删除 :只会在取出 key 的时候才对数据进行过期检查。优点:对 CPU 友好 缺点:可能会造成太多过期 key 没有被删除。
  • 定期删除 : 每隔一段时间取一批过期key 执行删除操作。优点:对内存更友好。

注:Redis对应key的过期时间,维护在过期字典里(hash表),每个key对应一个过期时间,是一个long long类型的整数。

6.内存淘汰策略

Redis 提供 6 种数据淘汰策略:

  • volatile-lru(least recently used):从设置了过期时间的key中,选最近最少使用的数据淘汰
  • volatile-ttl:从设置了过期时间的key中,挑选将要过期的数据淘汰
  • volatile-random:从设置了过期时间的key中,任意选择数据淘汰
  • allkeys-lru(least recently used):在键空间中,移除最近最少使用的 key(这个是最常用的)
  • allkeys-random:从数据集中任意选择数据淘汰
  • no-eviction:禁止驱逐数据,报错。

简单说就是:当空间不足时,会报错,或者随机删key,或者从有过期时间的key里面随机删或者根据LRU删或者根据快要过期删

4.0 版本后增加以下两种:

  • volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  • allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

二、数据持久化方式

rdb

aof

三、缓存一致性

1.更新缓存 VS 淘汰缓存

  • 更新缓存:数据不但写入数据库,还会写入缓存

优点:缓存不会增加一次miss,命中率高

缺点:加锁及各种并发情况可能会复杂

  • 淘汰缓存:数据只会写入数据库,不会写入缓存,只会把数据淘汰掉

优点:简单

缺点:会存在缓存miss

注:淘汰缓存后也可以通过加锁来保证只有一次命中DB,所以尽量是淘汰缓存。如果更新缓存的复杂度低那么可能才会考虑。

 

  • 先删缓存,再更新数据库  —> 进阶版:延迟双删(更新完了再删一遍)
  • 先更新数据库,再删缓存 —> 进阶版:监听binlog变更的mq,做删除

 

其他的更新缓存的不推荐:

  • 比如先更新缓存,再同步更新DB;或者先更新缓存,再异步批量更新DB。
  • 或者先更新DB再更新缓存

2.3种常用的缓存读写策略

  • Cache Aside Pattern(旁路缓存模式):先更新DB,然后删除缓存
  • Read/Write Through Pattern(读写穿透):先更新缓存再同步更新DB(缓存没有直接更新DB)
  • Write Behind Pattern(异步缓存写入):只更新缓存,异步批量更新 DB。

注:上面的读都一样,缓存有读缓存,缓存没有读DB写缓存。

注2:后两种并不常用。第三种可能点赞,或者维护浏览量这种场景可能用,吞吐量比较高。

 

3.几种缓存问题

  • 缓存穿透:请求不存在的key,每次都会落在DB上。

解决方法:布隆过滤器(一个白名单,判断请求值在不在集合中);缓存空对象(避免用一个不存在的key一直请求一直打到DB)

注:布隆过滤器可能会误判,因为采用的是hash,可能会hash冲突,那么会有小概率误判(部分不在白名单的误判成在了)。但是不影响,因为布隆过滤器说没在,那么就肯定没在。

  • 缓存击穿:缓存失效的时候,所有请求打到了DB。

解决方法:加锁

  • 缓存雪崩:同一时间不同key的缓存同时失效,请求都打到了DB。

解决方法:打散过期时间。

 

发表评论

邮箱地址不会被公开。 必填项已用*标注