Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

数据库篇--Redis #10

Open
lukaliou123 opened this issue Feb 28, 2022 · 12 comments
Open

数据库篇--Redis #10

lukaliou123 opened this issue Feb 28, 2022 · 12 comments

Comments

@lukaliou123
Copy link
Owner

1. 简单介绍一下 Redis

简单来说Redis就是一个使用C语言开发的数据库,不过与传统数据库不同的是Redis 的数据是存在内存中的 ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。
另外,Redis 除了做缓存之外,Redis 也经常用来做分布式锁,甚至是消息队列。
Redis 提供了多种数据类型来支持不同的业务场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案

2. 缓存数据的处理流程是怎样的?

image

  1. 如果用户请求的数据在缓存中就直接返回
  2. 缓存中不存在的话就看数据库中是否存在
  3. 数据库中存在的话就更新缓存中的数据。
  4. 数据库中不存在的话就返回空数据。
@lukaliou123
Copy link
Owner Author

lukaliou123 commented Feb 28, 2022

3.为什么要用 Redis/为什么要用缓存?

image
高性能 :
对照上面 👆 我画的图。我们设想这样的场景:
假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢?那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
不过,要保持数据库和缓存中的数据的一致性。 如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!

高并发
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 redis 的情况,redis 集群的话会更高)。

QPS(Query Per Second):服务器每秒可以执行的查询次数;

所以,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高的系统整体的并发。

@lukaliou123
Copy link
Owner Author

lukaliou123 commented Feb 28, 2022

4.Redis 常见数据结构以及使用场景分析

https://try.redis.io/
1.string
string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis 并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(simple dynamic string,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串长度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的 SDS API 是安全的,不会造成缓冲区溢出。
image

2.list
list 即是 链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且可以灵活调整链表长度,但是链表的随机访问困难。许多高级编程语言都内置了链表的实现比如 Java 中的 LinkedList,但是 C 语言并没有实现链表,所以 Redis 实现了自己的链表数据结构。Redis 的 list 的实现为一个 双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
image

3.hash
hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表)。不过,Redis 的 hash 做了更多优化。另外,hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。

4.set
set 类似于 Java 中的 HashSet 。Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程。

5.sorted set(z set)
和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表。有点像是 Java 中 HashMap 和 TreeSet 的结合体。

zset,其底层实现是一种优化过的链表,跳表
#1 (comment)

@lukaliou123
Copy link
Owner Author

lukaliou123 commented Feb 28, 2022

5.Redis 内存淘汰机制了解么?

相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
6种淘汰策略
1.volatile-lru(least recently used):已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
2.volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
3.volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
4.allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
5.allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
6.no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

@lukaliou123
Copy link
Owner Author

lukaliou123 commented Feb 28, 2022

6.Redis 持久化机制(怎么保证 Redis 挂掉之后再重启数据可以进行恢复)

很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)
1.快照(snapshotting)持久化(RDB)
Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
缺点:数据安全性低,因为是隔一段时间进行一次,而且一次保存的少,适合需求不严谨的时候
2.AOF(append-only file)持久化
与快照持久化相比,AOF 持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:
image
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。
image
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。 会把建立数据库的语句也保存进去
缺点:AOF文件大一些,恢复慢,因为使用保存的命令重头开始建。

6.1.AOF文件的重写

Redis 通过将每一次写操作记录到 AOF 文件的方式来实现数据持久化。但是,如果这个 AOF 文件一直增长,不仅会占用大量磁盘空间,而且在 Redis 启动时,需要加载 AOF 文件进行数据恢复,过大的 AOF 文件将极大延长启动时间

因此,Redis 提供了一种 AOF 重写的机制。简单来说,AOF 重写就是生成一个新的 AOF 文件,这个新的 AOF 文件能够达到和原来 AOF 文件一样的效果,但是体积更小。

具体来说,AOF 重写过程是这样的:

1.Redis 创建一个子进程来进行 AOF 重写操作。

2.子进程将当前 Redis 数据库的状态(即内存中的数据)写入到一个新的 AOF 文件中。而不是像原来的 AOF 文件那样记录每一次写操作,子进程只记录达到当前数据库状态所需要的写操作。比如,如果一个 key 被修改了多次,那么新的 AOF 文件中只需要记录最后一次修改即可,这样可以大大减小 AOF 文件的体积

3.在 AOF 重写的过程中,Redis 主进程还会继续处理客户端的请求。如果有新的写操作,Redis 不会直接写入到新的 AOF 文件中,而是先记录在一个缓冲区里

4.当子进程完成 AOF 重写操作后,Redis 主进程会将缓冲区中的写操作追加到新的 AOF 文件中。然后,Redis 用新的 AOF 文件替换掉旧的 AOF 文件,完成 AOF 重写操作。

@lukaliou123
Copy link
Owner Author

lukaliou123 commented Feb 28, 2022

7.Redis 事务

Redis 可以通过 MULTI,EXEC,DISCARD 和 WATCH 等命令来实现事务(transaction)功能。
使用 MULTI命令后可以输入多个命令。Redis不会立即执行这些命令,而是将它们放到队列,当调用了EXEC命令将执行所有命令
image
Redis 是不支持 roll back 的,因而不满足原子性的(而且不满足持久性)
你可以将Redis中的事务就理解为 :Redis事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断

@lukaliou123
Copy link
Owner Author

lukaliou123 commented Feb 28, 2022

8.缓存穿透

缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。

8.2. 缓存穿透情况的处理流程是怎样的?

image

8.3.有哪些解决办法?

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

8.3.1. 缓存无效 key
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086。
这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。

8.3.2.布隆过滤器
布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。
具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。存在的话才会走下面的流程。
image
但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。(说了还要说原理,别说了)

布隆过滤器是什么
是由一个固定大小的二进制向量或者位图(bitmap)和一系列映射函数组成的
在初始状态时,对于长度为 m 的位数组,它的所有位都被置为0,如下图所示:
image

当有变量被加入集合时,通过 K 个映射函数将这个变量映射成位图中的 K 个点,把它们置为 1(假定有两个变量都通过 3 个映射函数)。
image

@lukaliou123
Copy link
Owner Author

9.缓存击穿

缓存击穿是指数据库原本有得数据,但是缓存中没有,一般是缓存突然失效了,这时候如果有大量用户请求该数据,缓存没有则会去数据库请求,会引发数据库压力增大,可能会瞬间打垮

9.1.解决办法
1. 如果是热点数据,那么可以考虑设置永远不过期
2. 使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库打死。当然这样会导致系统的性能变差。

@lukaliou123
Copy link
Owner Author

10.缓存雪崩

缓存在同一时间大面积的失效,后面的请求都直接落到了数据库上,造成数据库短时间内承受大量请求。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。

或者:
有一些被大量访问数据(热点缓存)在某一时刻大面积失效,导致对应的请求直接落到了数据库上。

10.1.解决办法:

  1. 原有的失效时间上加上一个随机值,比如1-5分钟随机。这样就避免了因为采用相同的过期时间导致的缓存雪崩。

  2. 提高数据库的容灾能力,可以使用分库分表,读写分离的策略。

3.使用熔断机制。当流量到达一定的阈值时,就直接返回“系统拥挤”之类的提示,防
止过多的请求打在数据库上。至少能保证一部分用户是可以正常使用,其他用户多刷新几次也能得到结果。

  1. 为了防止Redis宕机导致缓存雪崩的问题,可以搭建Redis集群,提高Redis的容灾性。

@lukaliou123
Copy link
Owner Author

lukaliou123 commented Mar 9, 2022

11.Redis集群

11.1.Redis 主从架构

单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发
image

Redis replication 的核心机制
1.redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;
2.一个 master node 是可以配置多个 slave node 的;
3.slave node 也可以连接其他的 slave node
4.slave node 做复制的时候,不会 block master node 的正常工作
5.slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了

redis 主从复制的核心原理
1.当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。

2.如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份 RDB 快照文件

3.同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,

4.接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据

5.slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据
image

过程原理
1.当从库和主库建立MS关系后,会向主数据库发送SYNC命令
2.主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
3.当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
4.从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
5.之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致

端口是6379

@lukaliou123
Copy link
Owner Author

lukaliou123 commented Apr 5, 2022

11.2.哨兵模式

image
哨兵的介绍
sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:
集群监控:负责监控 redis master 和 slave 进程是否正常工作。
消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果 master node 挂掉了,会自动转移到 slave node 上
配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。

1.故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题
2.即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了

哨兵的核心知识
1.哨兵至少需要 3 个实例,来保证自己的健壮性。
2.哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
3.对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。

补充:和zk的投票有点像呢

Redis 的哨兵模式和 ZooKeeper 都是为了实现分布式系统中的高可用性和故障转移,所以在一定程度上,它们是有相似之处的。

ZooKeeper 提供的是一种分布式协调服务,它能帮助分布式应用程序实现诸如数据同步、配置管理、命名服务、集群管理等功能,比如在分布式数据库和分布式消息队列中都有广泛应用。

而 Redis 的哨兵模式主要是针对 Redis 服务本身的一种高可用解决方案,它能够监控 Redis 主从服务器的运行状态,并在主服务器发生故障时实现故障自动转移

在工作方式上,ZooKeeper 使用的是一种基于多数投票的一致性算法,只要半数以上的节点同意,就可以进行服务的切换;而 Redis 的哨兵模式下,只要有一个哨兵节点发现主节点出现故障,并且得到大多数哨兵节点的确认,就会进行主节点的切换。

@lukaliou123
Copy link
Owner Author

12.Redis的常见用途

1.消息队列系统: Redis具有发布/订阅功能,可以用作消息队列系统。生产者可以发送消息,消费者可以订阅并消费消息。

2.会话缓存: Redis常被用作会话缓存(session cache),例如存储网站或应用用户的会话信息。

3.排行榜和计数器: 利用Redis的排序集合和原子操作,可以很容易地实现排行榜和计数器功能。

4.实时分析: Redis的高速访问和数据结构特性使其成为实时分析的好选择。例如,可以利用Redis进行访问计数、统计、日志记录等。

5.分布式锁: Redis也可以用来实现分布式锁,控制多个实例对共享资源的访问。

@lukaliou123
Copy link
Owner Author

lukaliou123 commented Jul 18, 2023

13.原生Redis分布式锁有什么问题,怎么解决

在Redis中,可以使用SETNX(set if not exist)命令来创建一个锁。SETNX的作用是只在键不存在时,才对键进行设置操作。SETNX命令可以用来实现锁定。但是,原生的Redis分布式锁有一些问题,主要有以下两个方面:

1.锁超时问题:在获取锁之后,如果业务处理的时间过长,超过了锁的有效期,那么锁会自动释放,这时其它的线程就有可能获取到锁,进而导致数据的不一致性

2.锁非原子性问题:在Redis中设置锁和设置锁过期时间是两个操作,如果在设置完锁后,还没有设置过期时间时,系统就宕机了,那么锁将会一直存在,其它线程无法获取到锁

为了解决这些问题,我们需要进行以下的优化

1.使用SET key value NX PX milliseconds命令:这个命令可以在设置锁的同时设置锁的过期时间,而且这个操作是原子性的。

2.使用Lua脚本释放锁:为了确保释放锁的原子性,我们可以使用Lua脚本来进行判断,如果当前锁是由这个线程加的,那么就释放,否则就不处理。

一个简单的Redis分布式锁的实现方式如下:
image
完整:String result = jedis.set(key, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
完整:Object result = jedis.eval(script, Collections.singletonList(key), Collections.singletonList(requestId));
在这里,我们使用了SET key value NX PX milliseconds命令来设置锁,而且这个命令可以保证操作的原子性。同时,我们使用了Lua脚本来释放锁,也保证了操作的原子性。

注意,以上的示例代码只适用于单个Redis实例的情况。如果是Redis集群的情况,需要使用Redlock算法来保证锁的安全性。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant