文章目录
- 1.什么是Redis?它的优缺点是什么?
- 2.redis是单线程的,为什么还那么快?
- 3.redis支持的数据类型及使用场景
- 4.redis的key的过期策略
- 5.redis的内存淘汰策略
- 6.一个字符串类型的值能存储最大容量是多少?
- 7、缓存穿透、缓存击穿、缓存雪崩解决方案
- 8、redis的持久化机制,及优缺点?
- 9、redis的事务
- 10、redis的主从复制
- 11、哨兵模式
- 12、如何保证redis和数据库的数据一致性
- 13、redis实现分布式锁
- 14、redis文件事件模型
- 15、redission的看门狗续约原理
1.什么是Redis?它的优缺点是什么?
Redis是一个用c语言写的、开源的、基于内存的、高性能的Key-Value 非关系型数据库。
优点:
- 提供丰富的数据类型
- 读写性能优异
- 支持数据持久化
- 支持主从复制
- redis的所有命令都是原子性的
缺点:
- 数据库容量受限与物理内存
- redis是单线程的,单台服务器无法利用多核服务器的CPU
2.redis是单线程的,为什么还那么快?
- 基于内存
- 数据结构简单
- 单线程,避免线程上下文切换,以及竞争资源所带来的开销
- 使用多路IO复用,非阻塞IO
3.redis支持的数据类型及使用场景
| 类型 | 使用场景 | 底层实现 |
|---|---|---|
| String | 缓存 计数器 共享session | |
| Hash | 存储对象、存储购物车物品数量 | |
| List | lpush + lpop = 栈 lpush + rpop = 队列 lpush +ltrim = 有限集合 lpush + brpop = 消息队列 | |
| Set | 标签 适用于数据需要进行交集、并集、差集的场景 | |
| sorted set | 有序集合、排行榜 | |
| bitmap | 布隆过滤器 | |
| geoHash | 坐标,借助sorted set实现,通过zset的score排序来获得坐标附近的其他元素,然后通过score还原成坐标 | |
| HyperLogLog | 统计不重复数据,用于大数据基数统计 | |
| Streams | 内存版的kafka |
跳表 = 多级索引 + 有序链表 的数据结构,用随机策略建立上层索引,增删改查均 O(logN),范围查询友好、实现比平衡树简单、内存可控,Redis 用它实现 ZSet。
C 字符串以\0结尾,取长慢、不能存二进制、容易溢出、扩容频繁;所以 Redis 封装 SDS,靠 len 记录长度,实现 O (1) 取长、二进制安全、安全扩容、减少内存分配。
4.redis的key的过期策略
- 定期删除:redis每个隔一定的时间间隔,扫描内存的key,如果存在过期的key,则删除。(默认)
- 惰性删除:当某个key设置了过期时间,然后时间到了,redis不会立即删除,当你下次使用该key时,redis会检查该key是否过期,过期则删除,不会返回任何数据。(默认)
- 手动删除:主动调用删除命令。
- 永不删除
5.redis的内存淘汰策略
即当redis内存不足时,就会触发淘汰策略。
淘汰策略:
- no-eviction:当内存使用达到阈值时,返回报错,不会移除key。(默认)
- allkeys-lru:在redis的全部键中,优先移除最近最少使用的key。
- volatile-lru:在redis设置了过期时间的键中,优先移除最近最少使用的key,lru即以最近一次访问时间作为参考依据。
- allkeys-random:在redis的全部键中,随机移除某个key。
- volatile-random:在redis设置了过期时间的键中,随机移除某个key。
- volatile-ttl:在redis设置了过期时间的键中,优先移除存活时间短,即快过期的key。
redis4.0增加了2种:
- volatile-lfu:(最不经常使用)从设置了过期时间的key中,移除最近一段时间访问次数最少的key
- allkeys-lfu:从全部key中,移除最近一段时间访问次数最少的key
6.一个字符串类型的值能存储最大容量是多少?
512M
7、缓存穿透、缓存击穿、缓存雪崩解决方案
缓存穿透:查询一个一定不存在的数据,每次都要去数据库中查询,查询不到数据也就不会缓存,当并发数多时,数据库承受了巨大的压力,可能崩掉。
解决方案:
- 查询不到数据,返回一个空对象存放在缓存中,并设置过期时间;
- 使用布隆过滤器,将所有可能存在的数据的key哈希到一个巨大的bitmap中,一个一定不存在的数据key会被这个bitmap过滤掉,从而避免了查询数据库。
缓存击穿:对于设置了过期时间的key,缓存在某个时间点的key过期了,这时有大量的请求来请求这个key的数据,这些大量的请求相当于击穿了缓存,请求到数据库上,而这些瞬间大量的请求可能会让数据库崩溃。
解决方案:
- 使用互斥锁,当缓存失效时,不立即去请求数据库,先使用如redis的setnx去设置一个互斥锁,当操作成功返回时再进行加载数据库的操作并回设缓存,否则重试get缓存的方法。
- 设置key永不过期,物理永不过期,但逻辑过期(后台异步线程去刷新)
缓存雪崩:设置缓存时,多个key采用了相同的过期时间,当key过期时,如同雪崩一般,多个key的请求全部打到了数据库,瞬间会把数据库压垮。或者redis挂了。
解决方法:
- 将多个key的过期时间分散开来。
- 缓存预热
- 多级缓存
- 熔断降级
- 搭建高可用集群
8、redis的持久化机制,及优缺点?
redis提供两种持久化机制,RDB(默认)和AOF机制。
RDB机制(Redis DataBase):
RDB是redis默认的持久化方式,在指定的时间间隔内将内存中的数据快照到磁盘(保存为二进制的数据),保存名为:dump.rdb文件,恢复时直接将快照文件读取到内存中。
RDB的原理:
说到原理,有两个命令触发执行持久化,分别是save和bgsave命令,这两个命令的持久化方式略有不同:
save命令:同步,阻塞的,持久化的时候会阻塞redis服务,由于redis是单线程的,所以相当于redis短暂停止服务,也就不会处理客户端的任何请求,这个短暂的时间就是持久化的时间。
bgsave命令:异步,非阻塞的,原理就是fork + 写时复制,它可以一边持久化一边对外提供服务,互不影响,如上图所示,其流程描述如下:
当主进程要进行RDB持久化时,会Fork一个子进程,fork出来的子进程共享那一时刻主进程的内存空间的数据,他们互不影响,然后子进程将内存中的数据写入磁盘的一个临时文件,待持久化过程结束了,再将这个临时文件替换原来旧的RDB文件;在子进程持久化的期间,如果有客户端请求redis服务,主进程会处理客户端的请求,但是主进程和子进程共享内存,然后互不影响,那么主进程是如何做到的呢?答案就是copyonwrite(写时复制)。
当fork一个子进程时,内核会将主进程中的所有内存页的权限设置为read-only,然后子进程指向主进程的内存空间,相当于共享了主进程的内存,这时有客户端写请求到主进程中,由于内存的权限被设置为只读的,所以会触发页异常中断,把触发异常的页复制一份,然后写请求是对复制出来的那份数据的修改,与主进程中内存的数据是分开的,独立的,所以是互不影响的。
持久化触发机制:
- 配置文件中save满足条件时,会自动触发 --自动
- 使用flushall命令时会触发 --自动
- 退出redis时会触发 --自动
- 使用bgsave命令时触发 --手动
优点:
- 适合大规模的数据恢复
- 对数据的完整性要求不高
- 由于rdb文件是二进制文件,比较小,且只有一个,恢复会比较快
缺点:
- 持久化会有时间间隔,如果redis出现意外,会导致最后一次持久化的数据丢失
- fork的子进程会占用一定的内存空间
AOF机制(Append Only File)
以日志的形式记录每个写操作,只追加,不改写文件,redis的启动之初会读取该文件重新构建数据,即将写命令重新执行一遍。默认不开启,当aof和rdb同时存在时,数据恢复优先使用aof进行回复。
aof持久化原理图:
aof重写原理图:
redis的持久化在配置文件中配置:
- appendfsync always:每修改同步,每一次发生数据变更都会持久化到磁盘上,性能较差,但数据完整性较好。
- appendfsync everysec: 每秒同步,每秒内记录操作,异步操作,如果一秒内宕机,有数据丢失。
- appendfsync no:不同步,不是说不持久化数据,而是交给操作系统来进行持久化。
重写规则:
AOF 是redis的一种持久化方式,用来记录所有的写操作,但是随着时间增加,aof文件会越来越大,所以需要进行重写,将内存中的数据重新以命令的方式写入aof文件。
在重写的过程中,由于redis还会有新的写入,为了避免数据丢失,会开辟一块内存用于存放重写期间产生的写入操作,等到重写完毕后会将这块内存中的操作再追加到aof文件中。(双写,重写期间有写操作,直接追加旧aof文件,然后缓存一份新命令,重写结束,将缓存的写命令追加到新的aof文件)
优点:
- 可以配置每次修改都同步,数据的完整性好
- 可以配置每秒同步一次,会丢失最后1秒的数据
- 可以配置从不同步,效率最高
缺点:
- 相对于rdb数据文件来说,aof的文件大小比rdb的大,且恢复的速率慢
对比:
混合持久化:(RDB + AOF)
开启混合持久化时,单个AOF文件,分为两段:
- RDB 头——只写一次(重写时重新生成);
- AOF 尾——持续追加(写命令实时落地)。
重写时才从新生成RDB头,写命令继续追加。
9、redis的事务
redis的事务是一组命令的集合,命令在事务中没有被执行,只有发起执行命令才会被执行。redis单条命令保证原子性,事务不保证原子性,且没有回滚。
注意:
- 当发生编译型异常时(如代码有问题、命令有问题),事务中的所有命令都不会被执行;
- 当发生运行时异常时(如 1/0,对一个字符串进行加1操作),这个错误的命令不会被执行,但是事务中的其他正常命令会被执行;
Redis中的事务可以使用MULTI、EXEC、DISCARD和WATCH等命令来实现。以下是这些命令的简要说明:
MULTI:开始一个事务,将所有后续的命令排入队列,但不立即执行它们。
EXEC:执行事务中排队的所有命令。如果在执行过程中出现错误,Redis会继续执行其余的命令,而不会回滚之前的操作。
DISCARD:取消事务,清空事务队列,放弃之前排队的命令。
WATCH:监视一个或多个键,如果这些键被其他客户端修改,事务将被中止。
在使用事务时,您可以将多个命令排队,然后使用EXEC来执行它们。事务中的所有命令要么全部执行,要么全部放弃,这确保了原子性。如果您使用WATCH命令来监视键,可以在事务执行前检查键是否被修改,以实现乐观锁的目的。
请注意,Redis的事务不支持回滚操作,而且在事务中发生的错误不会影响其他命令的执行。因此,在设计使用Redis事务的应用程序时,需要小心处理错误处理和回滚逻辑。
Redis 事务的“原子性”是弱原子性,含义只有一句话:
EXEC 阶段所有命令要么顺序执行完,要么遇到语法错误时全部不执行;
但运行时错误(如 INCR 非数字)不会回滚,后续命令继续跑,也不会隔离其他客户端。
因此它不能保证“独占”或“回滚”,与 Lua 脚本的强原子性(独占+无并发,不会出现中间状态)不是同一级别。
Lua脚本执行时如果遇到运行时错误会终止,前面执行的写操作不会回滚,需要自行写回退逻辑。
- MULTI/EXEC 遇到运行时错误继续执行后续命令,部分成功;
- Lua 遇到运行时错误整个脚本失败,全部不生效(或回滚)。
10、redis的主从复制
11、哨兵模式
12、如何保证redis和数据库的数据一致性
先说下以下几种情况:
- 先更新缓存,再更新数据库
- 更新缓存成功,更新数据库失败会导致缓存脏数据。不可取。
- 先更新数据库,再更新缓存
- 在高并发下存在问题,A线程更新数据库,此时A线程还没更新缓存,线程B更新数据库并且更新缓存成功,然后线程A继续更新缓存,这时数据库中的值为线程B更新的值,缓存中的值为线程A更新的值,这时缓存和数据库不一致。
- 先删除缓存,再更新数据库
- 在高并发下,线程A删除了缓存的值,然后线程B进行了数据读取,此时缓存没命中,就去查询数据库,此时数据库的值还是旧的值,然后线程B会将这个旧值写入缓存,接着线程A更新数据库,这时数据库的值为线程A更新的值,缓存中的值为线程B更新的旧的数据库中的值。导致redis和数据库中的数据不一致。
- 解决:在线程A更新数据库之后,延迟一段时间,然后删除缓存中的值,这叫延迟双删,这里的延迟时间要大于业务的一次读操作的时间。
- 先更新数据库,再删除缓存
- 在高并发下,线程A进行查询,缓存没命中,接着去查询数据库,然后线程B更新了数据库,并且更新缓存,这时线程A将查询的数据库的值写入缓存,此时缓存中的值为线程A更新的值,数据库的值为线程B更新的值,导致redis中和数据库的数据不一致。
- 解决:
延迟双删,线程B在删除缓存之后,延迟一段时间再进行删除缓存。如果删除缓存失败,将key放入队列,然后循环删除,直到删除成功。
其实以上的方式都不能保证百分百的缓存与数据库的双写一致性,最简单粗暴的方法是在操作数据库和缓存时加一把锁,分布式锁。
不过这样会导致效率低下,面对读多写少的场景,可以使用读写锁来进行优化。
面对读多写多的场景可以使用中间件来优化。
不过一般情况下可以允许缓存和数据库的短暂不一致,我们在更新缓存时可以设置key的过期时间,一般互联网公司对于库存和数据库中的值也是采用此方式,展示商品的库存的值与数据库的值一般不一样,等到提交订单时才使用数据库中的库存值进行校验。
延迟双删存在问题
13、redis实现分布式锁
https://blog.csdn.net/Linging_24/article/details/118947248
14、redis文件事件模型
IO多路复用
Redis内部使用文件事件处理器File Event Handler,这个文件事件处理器是单线程的所以Redis才叫做单线程的模型。它采用I/O多路复用机制同时监听多个Socket,将socket跟文件事件处理器关联,将产生事件的Socket压入到内存队列中,事件分派器根据Socket上的事件类型来选择对应的事件处理器来进行处理。文件事件处理器包含5个部分:
- 多个Socket
- I/O多路复用程序
- Scocket队列
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
采用Reactor线程模型(单Reactor单线程模型),redis6.0以前:socket读写,命令解析,执行,事件分派 都是单线程(主线程);
redis6.0以后:socket读写可变为IO子线程多线程执行,命令解析、执行、I/O多路复用的epoll_wait阻塞、事件分派 都还是主线程(单线程)执行。
文件事件处理器
Redis为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求:
- 为了对连接服务器的各个客户端进行应答,服务器要为监听套接字关联应答处理器。
- 为了接受客户端传来的命令请求,服务器要为客户端套接字关联命令请求处理器。
- 为了向客户端返回命令的执行结果,服务器要为客户端套接字关联命令回复处理器
- 当主服务器和从服务器进行复制操作时,朱从服务器都需要关联特别为复制功能编写的复制处理器
连接应答处理器
连接应答处理器用于对连接服务器监听套接字的客户端进行应答,具体实现为sys/socket.h/accpet函数的包装。
当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的AE_READABLE事件进行关联,当有客户端用sys/socketh/connect函数连接服务器监听套接字的时候,套接字就会产生AE_READABLE事件,引发连接应答处理器执行。
命令请求处理器
命令处理器负责从套接字中读入客户端发送的命令请求内容,具体实现为unistd.h/read函数的包装。
当一个客户端通过连接应答处理器成功连接到服务器之后,服务器就会将客户端套接字的AE_READABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求时,套接字就会产生AE_READABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作。
在客户端连接服务器的整个过程,服务器都会一直为客户端套接字的AE_READABLE事件关联命令请求处理器。
命令回复处理器
命令回复处理器负责将服务器执行命令后得到的回复命令通过套接字返回给客户端。
当服务器有命令回复需要传送给客户端时,服务器会将客户端套接字的AE_WRITEABLE事件和命令处理器关联起来,当客户端准备好接受服务器传回的命令回复时,就会产生AE_WRITABLE事件,引发命令回复处理器执行,并执行相应的套接字写入操作。
当命令回复处理器发送完毕后,服务器就会解除命令回复处理器与套接字得AE_WRITABLE事件之间的关联。
一次完整的客户端与服务器连接事件
当一个 Redis 服务器正在运作,那么这个服务器的监听套接字得 AE_READABLE事件应该正处于监听状态下,而事件所对应的处理器为连接应答处理器。
当 Redis 客户端向服务器发起连接,那么监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行。处理器会对客户端的连接应答请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_RAEDABLE事件与命令请求处理器进行关联,使得客户端可以主动向服务器发送命令请求。
之后,假设客户端向主服务器发送一个命令请求,那么客户端套接字将产生一个 AE_READABLE 事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后传给相关程序去执行。
执行命令将产生相应的命令回复,为了将这些命令回复传送给客户端,服务器会将客户端套接字的 AE_WRITABLE 事件与命令回复处理器进行关联。当客户端尝试读取命令回复的时候,客户端套接字将产生 AE_WRITABLE 事件,触发命令回复处理器执行,当命令回复处理器将命令回复全部写入套接字之后,服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联。
15、redission的看门狗续约原理
netty的时间轮+lua脚本对锁进行续约
https://blog.csdn.net/weixin_39719732/article/details/110726067
引用:
https://www.cainiaoxueyuan.com/sjk/45552.html
https://zhuanlan.zhihu.com/p/659819761