Redis

是一个基于内存的、可持久化的键值对(key-value型)存储系统,数据读写是内存中的。

1.是单线程吗?为什么还这么快?

是也不是,Redis单线程指的是 接受客户端请求->解析请求->进行数据读写操作->发送数据给客户端 这个过程是由一个线程来完成,这是我们常说Redis是单线程的原因,关闭文件、AOF刷盘、释放内存又有各自的线程,这是说Redis多线程的原因。

单线程Redis吞吐量可以达到10W/每秒,Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:

  • Redis的数据读写是在内存中完成,所有的数据都存储在内存中。内存的读写速度远快于磁盘,这极大地提升了数据的访问速度。
  • Redis内部使用了多种高效的数据结构,如String、list、hash、set、zset,支持快速的查找、插入和删除操作。
  • 采用单线程可以避免多线程之间的竞争,省去了多线程切换带来的时间和性能上的、锁竞争的开销,不会导致死锁问题。
  • Redis采用I/O多路复用机制处理大量客户端的Socket请求,一个线程处理多个I/O流。Redis只运行单线程的情况下,内核会一直监听这些socket上的连接请求和数据请求,一旦请求到达,就会交给Redis线程处理,就实现了一个Redis线程处理多个IO流的效果。

2.分布式锁 

分布式锁(多个进程(服务)之间的互斥访问):在分布式系统中,当多个进程或服务需要访问共享资源时,保证同一时间只有一个进程或服务可以访问该资源,以避免并发问题。

多个用户访问不同的服务时,访问同一个数据库,但是用户在不同的进程里,数据库在内存中无法共享,可以把令牌放到线程所共享的Redis中,每个进程执行时都去Redis中检查令牌是否存在,如果有则难道令牌去执行代码,如果没有则等待其他进程将其删除。令牌在无锁,不在有锁。

3. 缓存

3.1缓存击穿

如果缓存中的某个热点数据过期(被频繁访问的的数据,例如秒杀活动)了,此时大量的请求访问该热点数据就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿问题。

解决方案:
  • 置不给热点数据设过期时间,由后台异步更新缓存,或者在热点数据准备要过期前 提前通知后台线程更新缓存以及重新设置过期时间。
  • 分布式锁,保证同一时间只有一个业务线程请求缓存,未能获取分布式锁的,等待锁释放后读取缓存。但是这种的话有一个弊端,那就是获取分布式锁的请求,都会执行一遍查询数据库,并更新到缓存。理论上只有第一个加载数据库记录请求是有效的针对这个问题,可以通过双重判定锁的形式,在获取到分布式锁之后再次查询一次缓存是否存在。如果缓存中存在数据,就直接返回;如果不存在,才继续执行查询数据库的操作。这样就可以避免大量请求访问数库。双重判定锁有效提升了锁性能以及数据库访问。

 一个用户线程去Redis缓存查看代码数据,而Redis缓存中的数据已过期,就会去数据库中查找,在查询数据库之前,使用分布式锁来确保只有一个线程或进程去数据库查询,查到后将数据放到Redis中,由于数据已经被更新,所以能够直接从Redis中获取数据,无需再访问数据库。当并发数太多,每台用户都要完成以上步骤很繁杂,可以加入分布式锁,第一个用户将数据放到Redis中,所有的线程共享此Redis,以后的用户线程只需要再次查看Redis就可以了,无需再次访问数据库。(存在两次查看Reids,double check,第一次是第一个用户查看Redis缓存中是否有数据,若没有则存数据库中加载数据放到Redis,第二次查看是加入分布式锁后,以后的用户去Redis中拿令牌查看数据,无需访问数据库)确保缓存的一致性和减少数据库的压力。 

3.2缓存穿透

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

 缓存穿透的发生有两种情况:
  • 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
  • 黑客恶意攻击,故意大量访问某些读取不存在数据的业务。
解决方案 

1.空对象值缓存:当查询结果为空时,也将结果进行缓存,但是设置一个较短的过期时间。这样在接下来的一段时间内,如果再次请求相同的数据,就可以直接从缓存中获取,而不是再次访问数据库,可以一定程度上解决缓存穿透问题。

这种方式是比较简单的一种实现方案,会存在一些弊端。那就是当短时间内存在大量恶意请求,缓存系统会存在大量的内存占用。如果要解决这种海量恶意请求带来的内存占用问题,需要搭配一套风控系统,对用户请求缓存不存在数据进行统计,进而封禁用户。整体设计就较为复杂,不推荐使用。

2.使用锁:当请求发现缓存不存在时,可以使用锁机制来避免多个相同的请求同时访问数据库,只让一个请求去加载数据,其他请求等待。

这种方式可以解决数据库压力过大问题,如果会出现“误杀“现象,那就是如果缓存中不存在但是数据库存在这种情况,也会等待获取锁,用户等待时间过长,不推荐使用。

3.布隆过滤器:布隆过滤器是一种数据结构,可以用于判断一个元素是否存在于一个集合中。它可以在很大程度上减轻缓存穿透问题,因为它可以快速判断一个数据是否可能存在于缓存中。

这种方式较为推荐,可以将所有存量数据全部放入布隆过滤器,然后如果缓存中不存在数据,紧接着判断布隆过滤器是否存在,如果存在访问数据库请求数据,如果不存在直接返回错误响应即可。但是这种问题还是会有一些小概率问题,那就是如果使用一种小概率误判的缓存进行攻击,依然会对数据库造成比较大的压力。

4.组合方案:如果说缓存不存在,那么就通过布隆过滤器进行初步选,然后判断是否存在缓存空值,如果存在直接返回失败。

如果不存在缓存空值,使用锁机制避免多个相同请求同时访问数据库。最后,如果请求数据库为空,那么将为空的 Key 进行空对象值缓存。

请求布隆过滤器和缓存空值判断会向 Redis 发起两次网络 I0,如果想优化的话,可以使用管道或者 Lua 命令来提高性能。

3.数据结构

Redis提供了丰富的数据类型,常见的有五种

键值对中的Key就是字符串对象,而value可以是字符串对象,也可以是集合数据类型的对象

3.1String(字符串)

key-value结构,key是唯一标识,value是具体的值

基本命令

set key value 设置值;get key获取值;incr key递增1;

适用场景:

1.全局ID/分布式ID:在一个系统中,无论数据分布在哪个节点或数据库,生成的ID都是全局唯一的;功能与全局ID相同,但是是在分布式系统中。例如全局计数器。

例如分表,一张数据表中存储的数据量太大,会导致B+树的高度增加导致查询效率变低,可以采用水平分表的方式将一个表分成两个,两个新表数据都来自同一个表但是会有两份新的自增ID,导致不能通过ID来唯一标识,这时可以将表数据存储到Redis中,借助String类型的incr让ID key每次自增1,形成唯一标识的自增ID列表。

  1. 当需要插入新数据时,先向Redis发送INCR命令,获取一个新的全局唯一ID。
  2. 使用这个全局唯一ID作为新数据的主键或唯一标识。
  3. 将新数据插入到相应的分表中。

2.缓存:将数据库中的热点数据存储到Redis的String类型中,极大的提高系统的访问速度,减少数据库的访问压力;用户的会话信息(登录状态、购物车内容等)可以存储在Redis的中,以便请求共享。

3.常规计数

4.分布式锁

内部实现

底层的数据结构实现是SDS简单动态字符串和int

len,记录了字符串的长度(buf中已占用空间的长度),获取字符型串长度的时候只需返回这个成员变量的值就行。

free,buff中剩余可用空间的长度。通过剩余空间的长度可以知道空间占用情况,当超过负载值动态扩容。也可通过修改len与free实现缩容。

  • 扩容:当字符串长度小于1M时,扩容都是加倍修改后的空间,如果超过1M,扩容时一次只会多扩1M。
  • 缩容:不会释放,修改len与free,供下次使用。

buf[],数据空间。

好处:返回字符串长度的时间复杂度低O(1);二进制安全,用len属性记录字符串的长度可以包含‘\0’,数据写入是什么样,它被读取时就是什么样;自动扩容不会发生缓冲区溢出

3.2List(列表)

List 类型的底层数据结构是由双向链表或压缩列表实现的:

  • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
  • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为List 类型的底层数据结构;

但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由quicklist 实现了,替代了双向链表和压缩列表。

常见命令

ZipLIist
结构

是Redis为节约内存而开发的,是由连续内存块组成的顺序型数据结构,类似数组。

 表头:

  • zlbytes,记录整个压缩列表占用内存字节数;
  • zltail,记录压缩列表尾部节点距离起始地址有多少字节
  • zlen,记录压缩列表包含的节点数量

entry压缩列表节点:

prevlen,记录 前一个节点 的长度,目的是实现从后向前遍历;

prevlen是如何根据数据的大小和类型进行不同空间大小的分配?

如果前一个节点的长度小于254字节,那么prevlen属性需要用1字节的空间来保存这个长度值

如果前一个节点的长度大于254字节,那么prevlen属性需要用5字节的空间来保存这个长度

encoding,记录当前节点的实际数据的类型和长度 ,类型主要有两种:字符串和整数;

encoding是如何根据数据的大小和类型进行不同空间大小的分配?

如果当前节点是整数,则encoding会使用1字节的空间进行编码,也就是encoding长度为1字节。

如果当前节点是字符串,根据字符串的大小,encoding会使用1字节/2字节/5字节的空间进行编码。

data,记录当前节点的实际数据;

zlend标记压缩列表的结束点

优缺点
  • 优点:一种内存紧凑型的数据结构,占用一块连续的内存空间,可以利用cpu缓存,也可以针对不同长度的数据进行相应的编码,有效的节省内存开销。顺序存储,查询效率高。
  • 缺点:不能保存过多的元素,否则查询效率会降低;新增或修改某个元素,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新问题。
存在的问题

连锁更新(连续多次空间扩展)

压缩列表新增某个元素或删除某个元素时,如果空间不够,压缩列表占用的内存空间就需要重新分配(例如新插入一个254字节的,那prevlen应为5,如果原来是1,那么以后得每个entry都应+4)。而当新插入的元素较大时,可能会导致后续元素的(entry1)prevlen占用空间都发生变化,从而引起 连锁更新 问题(entry2的长度、entry3的长度...变大),导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。

quickList

Redis3.0之前,List对象的底层数据结构是ZipList。在Redis3.2,List对象的底层由quicklist数据结构实现。是双向链表+压缩链表的组合,因为quicklist是一个链表,而链表中的每个元素又是一个压缩列表。

解决连锁更新问题

通过控制每个链表节点(quickListNode)中的压缩列表的大小或者元素个数,来规避连锁更新的问题。(因为压缩列表元素越少或越小,连锁更新来的的影响就越小,从而提供了更好的访问性能)能缓解连锁更新问题,链表中某个节点变长不会影响后面的节点,但是遍历的效率低。

  • 提供配置项list-max-ziplist-size来配置ZipList的大小(数值大小、内存空间大小)避免每个ZipList中的entry过多。
  • 对节点的ZipList做压缩,通过配置项list-compress-depth来控制,这个参数控制的是首尾不压缩的节点个数。

quickList的结构 

 

 quickList节点的结构

 quickList的节点里包含了前一个节点和下一个节点的指针,这样每个quicklistNode形成了一个双向链表,链表节点的元素保存的是一个压缩列表。

 在向 quicklist 添加一个元素的时候,不会像普通的链表那样,直接新建一个链表节点。而是会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,如果不能容纳,才会新建一个新的quicklistNode 结构。

quicklist 会控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来规避潜在的连锁更新的风,险,但是这并没有完全解决连锁更新的问题。

3.3Set(集合)

Set 类型的底层数据结构是由哈希表或整数集合实现的:

  • 如果集合中的元素都是整数且元素个数小于 512(默认值,set.maxintset-entries配置)个,Redis 会使用整数集合作为 Set类型的底层数据结构;
  • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为Set 类型的底层数据结构。

适用场景

  • 需要存储不重复的元素集合,如用户ID、商品ID等。
  • 可以实现社交应用中的共同好友、关注列表等功能。
  • 支持集合间的交集、并集、差集等操作,适用于需要这些操作的场景。

常用命令

  • SADD:向集合添加一个或多个成员,如果成员已存在则忽略。
  • SMEMBERS:返回集合中的所有成员。
  • SREM:移除集合中一个或多个成员。

 

 整数集合

可以看到,保存元素的容器是一个 contents 数组,虽然 contents被声明为 int8_t类型的数组,但是实际上 contents 数组并不保存任何 int8 t类型的元素,contents 数组的真正类型取决于 intset 结
构体里的 encoding 属性的值。比如:

  • 如果 encoding 属性值为 INTSET ENC INT16,那么 contents 就是一个 int16_t类型的数组,数组中每一个元素的类型都是int16_t;
  • 如果 encoding 属性值为 INTSET ENC INT32,那么 contents 就是一个 int32_t类型的数组,数组中每一个元素的类型都是int32 t;
  • 如果 encoding 属性值为 INTSET ENC INT64,那么 contents 就是一个 int64 t类型的数组,数组中每一个元素的类型都是int64_t;

不同类型的 contents 数组,意味着数组的大小也会不同 。


3.4ZSet(有序集合)

Zset 类型的底层数据结构是由压缩列表或跳表实现的:

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于64 字节时, Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为Zset 类型的底层数据结构;

适用场景

  • 需要存储不重复的元素,并且元素之间需要保持顺序,如排行榜、用户积分榜等。
  • 支持范围查询和按分值范围的操作,非常适合处理需要排序的数据。

常用命令

  • ZADD:向有序集合添加一个或多个成员,或者更新已存在成员的分数。
  • ZREM:移除有序集合中的一个或多个成员。
  • ZSCORE:返回有序集合中指定成员的分数。

 内部实现

  跳表

链表在查找元素的时候,因为需要逐一查找,所以查询效率非常低,时间复杂度是O(N),于是就出现了跳表。跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,这样的好处是能快读定位数据。

跳表结构里包含了:

  • 跳表的头尾节点,便于在O(1)时间复杂度内访问跳表的头节点和尾节点;
  • 跳表的长度,便于在O(1)时间复杂度获取跳表节点的数量;
  • 跳表的最大层数,便于在O(1)时间复杂度获取跳表中层高最大的那个节点的层数量;

 每个跳表节点由对象的元素值、元素权重值、后向指针(指向前一个节点,目的是为了方便从跳表的尾结点开始访问节点,使倒序查找方便)、节点的level数组(保存每层上的前向指针和跨度(计算这个节点在跳表中的排位))

 


3.5Hash(哈希)

互斥锁(一个进程中多个线程之间的互斥访问)

Hash 类型的底层数据结构是由压缩列表或哈希表实现的:

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为Hash 类型的底层数据结构,
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为Hash 类型的底层数据结构。 

适用场景

  • 存储对象信息,如用户信息(用户名、密码、邮箱等),每个字段独立存储,便于管理和更新。
  • 适用于需要频繁读取/写入的数据结构,因为Hash类型在内存中的存储效率较高。

常用命令

  • HSET:设置哈希表field的值。
  • HGET:获取哈希表中指定field的值。
  • HDEL:删除哈希表中的一个或多个字段。

 内部实现

哈希表的结构如下

dictEntry 结构里不仅包含指向键和值的指针,还包含了指向下一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对链接起来,以此来解决哈希冲突的问题,这就是链式哈希。


 4.持久化

RDB

是Redis数据备份文件,默认是RDB快照,存储的是二进制数据,是异步且定时每隔一段时间将内存数据放入磁盘,当系统若宕机后内存中没有数据,重启后就能从磁盘中加载到内存,在内存做数据处理。Redis停机时会自动执行一次RDB快照。

redis -cli  连接RDB
bgsave 开启子线程执行RDB,避免主线程收到影响
saving started + 间隔时间

RDB方式bgsave(开启子线程将数据持久化到磁盘)的基本流程

  • fork主进程得到一个子进程,共享内存空间
  • 子进程读取内存数据并写入RDB文件
  • 用新的RDB文件替换旧的RDB文件

缺点:

  • RDB执行间隔时间长,两次RDB之间写入数据有丢失的风险
  • fork子进程、压缩、写出RDB文件都比较耗时 

 AOF

追加文件,记录比RDB文件大。Redis处理的每一个写命令都会记录在AOF文件,可以看作是命令日志文件。

默认关闭,需要开启,需要修改Redis.conf配置文件来开启AOF,AOF的三种写回策略:

  因为是记录命令,AOF文件会比RDB文件大得多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF执行重写功能,用最少得命令达到相同的效果。

相比较:

 两者结合使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小翩zhi

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值