Redis简单笔记

Redis简单笔记

系统基本结构

(存储模块)、(索引模块),(操作模块),访问框架(动态库访问、网络访问框架)

img

访问框架(I/O模型):网络请求时,单线程还是多线程处理?

索引:内存键值数据库一般采用哈希表作为索引,

存储模块(分配器,持久化)

img

SimpleKV和Redis的对比:

  • 【数据结构】上缺乏广泛的数据结构支持:比如支持范围查询的SkipList,和Stream等等数据结构
  • 【高可用】上缺乏,哨兵或者master-slaver模式的高可用设计
  • 【横向扩展】上缺乏集群和分片功能
  • 【在内存安全性】上,缺乏内存过载时候的key淘汰算法的支持
  • 【内存利用率】没有充分对数据结构优化提高内存利用率,例如使用压缩性的数据结构
  • 【功能扩展】需要具备后续功能的拓展
  • 【不具备事务性】无法保证多个操作的原子性
  • 【内存分配器】SimpleKV就是glibc,Redis的分配器选择更多。

数据结构与对象

数据类型

五种:String、hash、list、set、zset。

①. String(字符串):redis 最基本的数据类型,一个 key 对应一个 value,一个键最大能存储 512MB

②. Hash(哈希):是一个键值对集合,特别适合用于存储对象

③. List(列表):存放多个字符串值,可以重复,按照插入顺序进行排序,也可以添加一个元素到列表的头部和尾部。(底层实现:双向链表和压缩列表) 建议:因地制宜地使用 List 类型。例如,既然它的 POP/PUSH 效率很高,那么就将它主要用于 FIFO 队列场景,而不是作为一个可以随机读写的集合。

④. Sets(集合):存放多个值,不可以重复,没有顺序

⑤. ZSet(有序集合):存放多个值,不可以重复,有顺序。不同的是每个元素都会关联一个 double 类型的分数,redis 正是通过分数来为集合中的成员进行从小到大的排序。(跳表)

String的简单动态字符串(SDS)

//SDS数据结构
struct sds {
    //记录buf数组已使用字节的数量,等于SDS所保存字符串的长度
    int len;
    
    //记录buf数组未使用字节的数量
    int free;
    
    //字节数组,用于保存字符串
    char buf[];
}

特性:

  1. buf字节数组空间分配策略,减少修改字符串时带来的内存重分配次数,优化效率:

    空间预分配

    • 当字符串的长度小于 1MB时,每次扩容都是加倍现有的空间,即free==len。
    • 如果字符串长度超过 1MB时,每次扩容时只会扩展 1MB 的空间,即free=1MB。

    惰性空间释放(不会立即释放多余空间)

  2. 二进制安全,可用于存储任意格式二进制数据。因为C中string 没有长度标示,当一段地址里面存在多个结束符号,string是没有办法访问到第一个/0后面的内容的,而正好sds的结构弥补了这一缺点。

  3. 兼容部分C字符串函数,如buf字节数组后保存空字符的字节,就是用来适配C字符串的结构,以重用C中的部分字符串函数。

更底层源码参考:https://blog.csdn.net/qq_33361976/article/details/109014012

字典

Redis的字典使用哈希表作为底层实现、基本结构类似Java的hashMap。但链式哈希冲突没用红黑树,rehash采用渐进式rehash

为了使 rehash 操作更高效,Redis 默认使用了两个全局哈希表:哈希表 1 和哈希表 2。一开始,当你刚插入数据时,默认使用哈希表 1,此时的哈希表 2 并没有被分配空间。随着数据逐步增多,Redis 开始执行 rehash,这个过程分为三步:给哈希表 2 分配更大的空间,例如是当前哈希表 1 大小的两倍;把哈希表 1 中的数据重新映射并拷贝到哈希表 2 中;释放哈希表 1 的空间。

到此,我们就可以从哈希表 1 切换到哈希表 2,用增大的哈希表 2 保存更多数据,而原来的哈希表 1 留作下一次 rehash 扩容备用。

简单来说就是在第二步拷贝数据时,Redis 仍然正常处理客户端请求,每处理一个请求时,从哈希表 1 中的第一个索引位置开始,顺带着将这个索引位置上的所有 entries 拷贝到哈希表 2 中;等处理下一个请求时,再顺带拷贝哈希表 1 中的下一个索引位置的 entries。如图:

img

这样就巧妙地把一次性大量拷贝的开销,分摊到了多次处理请求的过程中,避免了耗时操作,保证了数据的快速访问。

底层数据结构

集合类型的底层数据结构主要有 5 种:整数数组、双向链表、哈希表、压缩列表和跳表。前三种比较常见,主要关注后面两种redis中的特有数据结构。

跳表(skiplist)

跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针(索引),从而达到快速访问节点的目的。

跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。

img

压缩列表

压缩列表(ziplist)是列表和哈希键的底层实现之一。当一个列表只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表的底层实现。

img

压缩列表实际上类似于一个数组,数组中的每一个元素都对应保存一个数据。和数组不同的是,压缩列表在表头有三个字段 zlbytes、zltail 和 zllen,分别表示列表占用内存字节数、列表尾的偏移量和列表中的 entry 个数;压缩列表在表尾还有一个 zlend,表示列表结束。

不同操作的复杂度

img

整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,那为什么 Redis 还会把它们作为底层数据结构呢?

1、主要在于内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少(少去了指针的空间占用)。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。

2、数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。

参考:https://time.geekbang.org/column/article/268253

单机数据库的实现

实现原理、RDB持久化和AOF持久化、redis文件事件和时间事件

多机数据库的实现

Sentinel、复制(replication)、集群(cluster)

独立功能的实现

redis 属于 nosql

redis内置16个数据库,下标0-15标识,默认选择第0个库

面向key编程

持久化机制

Redis 的持久化主要有两大机制,AOF(Append Only File)日志和 RDB 快照

AOF(Append Only File)持久化

AOF持久化是通过保存Redis服务器所执行的写命令来记录数据库状态的。

Redis 的AOF使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况。除此之外,AOF 还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操作。

写回策略:

AOF 日志有三种写回策略,分别是 Always、Everysec 和 No

img

日志文件太大了怎么办?

​ Aof日志文件过大的问题:1.操作系统对文件大小有限制,超过则无法继续写入;2.文件太大,写入的效率也会变低;3.文件太大,恢复数据也很耗时

答:AOF 重写机制

即对过大的AOF文件创建一个新的 AOF 重写文件,进行重写,以此来压缩AOF文件的大小。 具体的实现是:检查当前键值数据库中的键值对,记录键值对的最终状态,从而实现对 某个键值对 重复操作后产生的多条操作记录压缩成一条 的效果。进而实现压缩AOF文件的大小。(我愿称为AOF重开加压缩机制

一图胜千言:

img

AOF 重写会阻塞吗?

​ 和 AOF 日志由主线程写回不同,重写过程是由后台子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。

主线程 fork出后台bgrewiteaof子线程后,子线程的文件描述符指针内容是指向主线程的已有内容的。但是当主线程写入新内容时,操作系统这时候才会重新复制一份主线程的内容给子线程。这种机制在操作系统中叫做写复制,原因就是节省内存空间,如果两个进程都没有改变文件内容,也就不需要新开辟空间再存储一遍了。

答:不会,因为有重写过程是子进程在操作。两个日志文件

img

RDB持久化

快照机制,能直接将数据存入RDB文件中。

Redis服务器有两种保存和载入RDB文件的方法:SAVE和BGSAVE

SAVE命令由服务器进程执行保存工作,BGSAVE命令则由子进程执行保存工作,所以SAVE命令会阻塞服务器,而BGSAVE命令则不会。

img

优点

  • 相对于AOF来说,可以快速恢复数据库。也就是只需要把 RDB 文件直接读入内存,这就避免了 AOF 需要顺序、逐一重新执行操作命令带来的低效性能问题。
  • 性能最大化,fork 子进程来完成写操作,让主进程继续处理命令,所以是 IO 最大化。(使用单独子进程来进行持久化,主进程不会进行任何 IO 操 作,保证了 redis 的高性能)

缺点:数据安全性低。RDB 是间隔一段时间进行持久化,如果持久化之间 redis 发生故障,会发生数据丢失。所以这种方式更适合数据要求不严谨的时候

Redis用作缓存是如何工作的?

首先,我们要了解缓存的特征。类比计算机系统中的缓存,Redis的理念也差不多。主要有以下两个特征:

  • 在一个层次化的系统中,缓存一定是一个快速子系统,数据存在缓存中时,能避免每次从慢速子系统中存取数据。
  • 缓存系统的容量大小总是小于后端慢速系统的,我们不可能把所有数据都放在缓存系统中,所以需要实现缓存系统的数据淘汰规则。

Redis缓存工作简单模型图

img

使用 Redis 缓存时,我们基本有两个操作:

  • 应用读取数据时,需要先读取 Redis;
  • 发生缓存缺失时,需要从数据库读取数据,还需要更新缓存。

缓存类型

只读缓存

读写缓存

关于是选择只读缓存,还是读写缓存,主要看我们对写请求是否有加速的需求。

  • 如果需要对写请求进行加速,我们选择读写缓存;
  • 如果写请求很少,或者是只需要提升读请求的响应速度的话,我们选择只读缓存。

举个例子,在商品大促的场景中,商品的库存信息会一直被修改。如果每次修改都需到数据库中处理,就会拖慢整个应用,此时,我们通常会选择读写缓存的模式。而在短视频 App 的场景中/或者用户信息场景,虽然视频的属性有很多,但是,一般确定后,修改并不频繁,此时,在数据库中进行修改对缓存影响不大,所以只读缓存模式是一个合适的选择。

Redis在用作缓存时,使用只读缓存或读写缓存的哪种模式?

1、只读缓存模式:每次修改直接写入后端数据库,如果Redis缓存不命中,则什么都不用操作,如果Redis缓存命中,则删除缓存中的数据,待下次读取时从后端数据库中加载最新值到缓存中。

2、读写缓存模式+同步直写策略:由于Redis在淘汰数据时,直接在内部删除键值对,外部无法介入处理脏数据写回数据库,所以使用Redis作读写缓存时,只能采用同步直写策略,修改缓存的同时也要写入到后端数据库中,从而保证修改操作不被丢失。但这种方案在并发场景下会导致数据库和缓存的不一致,需要在特定业务场景下或者配合分布式锁使用。

当一个系统引入缓存时,需要面临最大的问题就是,如何保证缓存和后端数据库的一致性问题,最常见的3个解决方案分别是Cache Aside、Read/Write Throught和Write Back缓存更新策略。

1、Cache Aside策略:就是文章所讲的只读缓存模式。读操作命中缓存直接返回,否则从后端数据库加载到缓存再返回。写操作直接更新数据库,然后删除缓存。这种策略的优点是一切以后端数据库为准,可以保证缓存和数据库的一致性。缺点是写操作会让缓存失效,再次读取时需要从数据库中加载。这种策略是我们在开发软件时最常用的,在使用Memcached或Redis时一般都采用这种方案。

2、Read/Write Throught策略:应用层读写只需要操作缓存,不需要关心后端数据库。应用层在操作缓存时,缓存层会自动从数据库中加载或写回到数据库中,这种策略的优点是,对于应用层的使用非常友好,只需要操作缓存即可,缺点是需要缓存层支持和后端数据库的联动。

3、Write Back策略:类似于文章所讲的读写缓存模式+异步写回策略。写操作只写缓存,比较简单。而读操作如果命中缓存则直接返回,否则需要从数据库中加载到缓存中,在加载之前,如果缓存已满,则先把需要淘汰的缓存数据写回到后端数据库中,再把对应的数据放入到缓存中。这种策略的优点是,写操作飞快(只写缓存),缺点是如果数据还未来得及写入后端数据库,系统发生异常会导致缓存和数据库的不一致。这种策略经常使用在操作系统Page Cache中,或者应对大量写操作的数据库引擎中。

Redis缓存异常问题

数据不一致问题

缓存雪崩、击穿、穿透问题

缓存雪崩

第一种情况是:缓存中有大量数据同时过期,导致大量请求无法得到处理。具体来说,当数据保存在缓存中,并且设置了过期时间时,如果在某一个时刻,大量数据同时过期,此时,应用再访问这些数据的话,就会发生缓存缺失。紧接着,应用就会把请求发送给数据库,从数据库中读取数据。如果应用的并发请求量很大,那么数据库的压力也就很大,这会进一步影响到数据库的其他正常业务请求处理

解决方案

  1. 尽量避免给大量的数据设置相同的过期时间。或者用 EXPIRE 命令给每个数据设置过期时间时,给这些数据的过期时间增加一个较小的随机数(例如,随机增加 1~3 分钟),这样一来,不同数据的过期时间有所差别,但差别又不会太大,既避免了大量数据同时过期,同时也保证了这些数据基本在相近的时间失效,仍然能满足业务需求。
  2. 服务降级

第二种情况是:Redis 缓存实例发生故障宕机了,无法处理请求,这就会导致大量请求一下子积压到数据库层,从而发生缓存雪崩。

一般来说,一个 Redis 实例可以支持数万级别的请求处理吞吐量,而单个数据库可能只能支持数千级别的请求处理吞吐量,它们两个的处理能力可能相差了近十倍。由于缓存雪崩,Redis 缓存失效,所以,数据库就可能要承受近十倍的请求压力,从而因为压力过大而崩溃。

解决方案

  1. 在业务系统中实现服务熔断请求限流机制。为了防止引发连锁的数据库雪崩,甚至是整个系统的崩溃,我们可以暂停部分业务应用对缓存系统的接口访问

img

  1. 通过主从节点的方式构建 Redis 缓存高可靠集群(预防机制)。如果 Redis 缓存的主节点故障宕机了,从节点还可以切换成为主节点,继续提供缓存服务,避免了由于缓存实例宕机而导致的缓存雪崩问题。

缓存击穿

​ 缓存击穿是指,针对某个访问非常频繁的热点数据的请求,无法在缓存中进行处理,紧接着,访问该数据的大量请求,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库处理其他请求。缓存击穿的情况,经常发生在热点数据过期失效时。

解决方案:为了避免缓存击穿给数据库带来的激增压力,我们的解决方法也比较直接,对于访问特别频繁的热点数据,我们就不设置过期时间了。这样一来,对热点数据的访问请求,都可以在缓存中进行处理,而 Redis 数万级别的高吞吐量可以很好地应对大量的并发请求访问。

缓存穿透

缓存穿透是指要访问的数据既不在 Redis 缓存中,也不在数据库中,导致请求在访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。这样一来,缓存也就成了“摆设”,如果应用持续有大量请求访问数据,就会同时给缓存和数据库带来巨大压力,如果这偏偏还是热点数据相对于雪崩和击穿来说它的影响是长期的。

缓存穿透会发生在什么时候呢?一般来说,有两种情况。

  • 业务层误操作:缓存中的数据和数据库中的数据被误删除了,所以缓存和数据库中都没有数据;
  • 恶意攻击:专门访问数据库中没有的数据。

解决方案:

  1. 缓存空值或缺省值。一旦发生缓存穿透,我们可以针对查询的数据,在 Redis 中缓存一个空值或是和业务层协商确定的缺省值(例如,库存的缺省值可以设为 0)

  2. 使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力。

    关于布隆过滤器,简单理解就是一个初始都为0的bit数组和N个哈希函数(N<bit数组长度)。当需要标记某条数据时,使用哈希函数对bit数组长度取模得到下标并标记为1。当需要查询这条数据时,查询该数组的N个位置,如果有一个不为1则表示该数据不存在数据库。

    img

    ​ 正是基于布隆过滤器的快速检测特性,我们可以在把数据写入数据库时,使用布隆过滤器做个标记。当缓存缺失后,应用查询数据库时,可以通过查询布隆过滤器快速判断数据是否存在。如果不存在,就不用再去数据库中查询了。这样一来,即使发生缓存穿透了,大量请求只会查询 Redis 和布隆过滤器,而不会积压到数据库,也就不会影响数据库的正常运行。布隆过滤器可以使用 Redis 实现,本身就能承担较大的并发访问压力。

  3. 针对恶意攻击的情况,可以在请求入口的前端进行请求检测。

总结:跟缓存雪崩、缓存击穿这两类问题相比,缓存穿透的影响更大一些。从预防的角度来说,我们需要避免误删除数据库和缓存中的数据;从应对角度来说,我们可以在业务系统中使用缓存空值或缺省值、使用布隆过滤器,以及进行恶意请求检测等方法。

总结

img

尽量使用预防式方案:

  • 针对缓存雪崩,合理地设置数据过期时间,以及搭建高可靠缓存集群;
  • 针对缓存击穿,在缓存访问非常频繁的热点数据时,不要设置过期时间;
  • 针对缓存穿透,提前在入口前端实现恶意请求检测,或者规范数据库的数据删除操作,避免误删除。

基本命令

切换数据库:select 5 //切换到第五号数据库

image-20211029201255543

事务编程

redis在事务中的执行的所有命令放入队列里,事务提交后统一执行;故在在事务内部无法使用命令的执行结果,如查询操作,需要在事务外部方可使用。

// 编程式事务
@Test
public void testTransactional() {
    Object obj = redisTemplate.execute(new SessionCallback() {
        @Override
        public Object execute(RedisOperations operations) throws DataAccessException {
            String redisKey = "test:tx";

            operations.multi();

            operations.opsForSet().add(redisKey, "zhangsan");
            operations.opsForSet().add(redisKey, "lisi");
            operations.opsForSet().add(redisKey, "wangwu");

            System.out.println(operations.opsForSet().members(redisKey));

            return operations.exec();
        }
    });
    System.out.println(obj);
}

问题

Redis是单线程的,为什么又很快?

  • Redis 的大部分操作在内存上完成,再加上它采用了高效的数据结构,例如哈希表和跳表,这是它实现高性能的一个重要原因。
  • 单线程操作减少了多线程上下文切换的开销和竞争条件,不用去考虑多线程编程模式面临的共享资源的并发访问控制问题。
  • redis 采用非阻塞I/O多路复用技术,避免了accept() 和 send()/recv() 潜在的网络 I/O 操作阻塞点。

基于多路复用的Redis高性能IO模型

为了在请求到达时能通知到 Redis 线程,select/epoll 提供了基于事件的回调机制,即针对不同事件的发生,调用相应的处理函数。

关键字:操作系统的I/O多路复用技术、队列、内核处理与事件回调机制

img

使用 Redis 有哪些好处?

(1) 性能高,因为数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1)

(2) 支持丰富数据类型,支持 string,list,set,Zset,hash 等

(3) 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行

(4) 丰富的特性:可用于缓存,消息,按 key 设置过期时间,过期后将会自动删除。Redis 还支持 publish/subscribe(发布/订阅)

Redis 的持久化机制是什么?各自的优缺点?

Redis 提供两种持久化机制 RDB 和 AOF 机制:

数据完整性差异

内存占用差异

恢复速度差异

redis主从复制

Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存 buffer,待完成后将 rdb 文件全量同步到复制节点,复制节点接受完成后将 rdb 镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

image-20211029203619933

问题:主库挂了怎么办?从库不能自动接替主库咋办?

在这个模式下,如果从库发生故障了,客户端可以继续向主库或其他从库发送请求,进行相关的操作,但是如果主库发生故障了,那就直接会影响到从库的同步,因为从库没有相应的主库可以进行数据复制操作了。如果客户端发送的都是读操作请求,那还可以由从库继续提供服务,这在纯读的业务场景下还能被接受。但是,一旦有写操作请求了,按照主从库模式下的读写分离要求,需要由主库来完成写操作。此时,也没有实例可以来服务客户端的写操作请求了。

解决:哨兵机制

redis哨兵机制

​ 哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:监控选主(选择主库)通知

​ 我们先看监控。监控是指哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。

image-20211029203813504

Redis 一般用在那些场景

①. 缓存热数据使用,热数据就是在项目中经常会被查询,但不经常会被修改和删除的数据

②. 计数器,诸如统计点击数等应用

③. 队列

④. 位操作(大数据处理),比如统计 QQ 用户在线

⑤. 分布式锁和单线程机制

⑥. 最新列表

⑦. 排行榜,使用 zadd 添加有序集合

学习外链

2021年Redis面试题总结

极客直播课程

image-20211029201418209

image-20211029201543080

image-20211029201946567

image-20211029202220913

内存优化:如签到,统计等相关功能相对set来说内存占用更少

image-20211029202549720

posted @ 2022-03-01 08:32  Y鱼鱼鱼Y  阅读(85)  评论(0编辑  收藏  举报