Redis笔记

redis简介

Redis(Remote Dictionary Server) 是一个使用 C 语言编写的,开源的高性能非关系型(NoSQL)的键值对数据库。

与传统数据库不同的是 Redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向,每秒可以处理超过 10万次读写操作。

Redis 是K-V型的数据库,整个数据库都是用字典来存储的,对Redis数据库的任何增删改查操作,实际上就是对字典中的数据进行增删改查

1. 可以存储海量数据,且可以根据键以O(1) 的时间复杂度取出或插入关联值;

2. 键值对中键的类型可以是字符串,整型,浮点型等,且键是唯一的;

3. 键值对中的值类型可以是string, hash ,list, set, sorted set。

图(1):redis数据存储流程

我们为什么要用Redis?(或者说我们为什么要用缓存)

假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的,硬盘的寻址速度是 毫秒级的。从数据库中获取到数据后将数据存在Redis中,这样下一次再访问这些数据的时候就可以直接从Redis中获取了,Redis数据是存在内存中的,内存的寻址速度是纳秒级的,所以可以极大提升响应速度,同时缓解数据库压力 。

备注:时间单位换算

1s = 1000ms

1ms = 1000us

1us = 1000ns


图(2)Redis访问流程

为什么要用 Redis 而不用 map/guava 做缓存?

缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,造成内存浪费,且缓存不具有一致性。

使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。

Redis 与 Memcached 的区别与选型 。

图(3):Redis与memcached比较

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

1、完全基于内存( ns 级的访问),非常快速。

2、Redis 中的数据结构是采用类似于java中HashMap的数据结构 Dict,底层用数组加链表实现的哈希表,可以实现查找和操作O(1)时间复杂度;

3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;

Redis有哪些数据类型及应用场景

图(4):Redis数据类型及常见应用场景

Redis的应用场景

1. 缓存

将热点数据放到内存中,提升访问速度,缓解DB压力。

2. 计数器

可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。

3. 分布式ID生成

利用自增特性,一次请求一个大一点的步长如 incr 2000 ,缓存在本地使用,用完再请求。

4. 海量数据统计

位图(bitmap): 存储是否参过某次活动,是否已读谋篇文章,用户是否为会员, 日活统计。

图(5):海量数据统计

5. 会话缓存

可以使用 Redis 来统一存储多台应用服务器的会话信息。当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。

6. 分布式队列/阻塞队列

List 是一个双向链表,可以通过 lpush/rpush 和 rpop/lpop 写入和读取消息。可以通过使用brpop/blpop 来实现阻塞队列。

7. 分布式锁实现

在分布式场景下,无法使用基于进程的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁热点数据存储

最新评论,最新文章列表,使用list 存储,ltrim取出热点数据,删除老数据。

8. 社交类需求

Set 可以实现交集,从而实现共同好友等功能,Set通过求差集,可以进行好友推荐,文章推荐。

图(6):set集合求差集

9. 排行榜

ZSet 可以实现有序性操作,从而实现排行榜等功能。

10. 延迟队列

使用sorted_set,使用 【当前时间戳 + 需要延迟的时长】做score, 消息内容作为元素,调用zadd来生产消息,消费者使用zrangbyscore获取当前时间之前的数据做轮询处理。消费完再删除任务 rem key member

图(7):使用zset作为延迟队列

Redis持久化

Redis是基于内存的数据库,同时提供了持久化的能力,持久化就是把内存的数据写到磁盘中去,防止服务宕机了,内存数据丢失。

Redis有哪些持久化方式?各自的优缺点?

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

RDB(Redis DataBase):RDB保存某一个时间点之前的快照数据。

AOF(Append-Only File):指所有的命令行记录以 redis 命令请求协议的格式完全持久化存储保存为 aof 文件。

混合持久化(4.x):指进行AOF重写时子进程将当前时间点的数据快照保存为RDB文件格式,而后将父进程累积命令保存为AOF格式。

RDB 快照有两种触发方式:

1.为通过配置参数,如下:

通过一定的时间周期内看,命令执行的个数,超过阈值及执行快照生成

图(8):RDB快照设置

2.为通过手动执行bgsave,显示触发生成快照

图(9):bgsave存储过程

优点:

1.性能最大化,fork 子进程来完成写操作,让主进程继续处理命令。使用单独子进程来进行持久化,保证了 redis 的高性能。

2.当重启恢复数据的时候,数据量比较大时,Redis直接解析RDB二进制文件,生成对应的数据存储在内存中,比 AOF 的启动效率更高。

缺点:

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

2.RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,会消耗比较大的内存空间。

AOF持久化执行流程 :

通过 appendonly yes 开启。

图(9):AOF持久化流程

Redis使用单线程响应命令,如果每次写AOF文件命令都追加到磁盘,会极大影 响处理性能,所以Redis先写入aof缓冲区,根据用户配置的同步硬盘策略写入aof文件中,可以通过 appendfsync 参数配置同步策略:值得含义如下

图(10): appendfsync 参数配置同步策略

AOF 重写机制

随着命令得不断写入AOF,文件会越来越大,为了解决这个问题Redis引入了AOF重写机制压缩文件体积。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。AOF重写机制可以通过手动触发和自动触发。

手动触发: bgrewriteaof命令

自动触发:auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机。

auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认为64MB。

auto-aof-rewrite-percentage:代表当前AOF文件空间(aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。

自动触发时机

图(11):AOF自动重写设置   

aof_current_size和aof_base_size可以在info Persistence统计信息中查看

优点:数据安全,aof 持久化可以配置 appendfsync 属性,有 always,每进行一次命令操作就记录到 aof 文件中一次。

缺点:数据集大的时候,比 rdb 启动效率低。

混合持久化:

可以通过设置 aof-use-rdb-preamble yes 开启。

加载时,首先会识别AOF文件是否以REDIS字符串开头,如果是,就按RDB格式加载,加载完RDB后继续按AOF格式加载剩余部分。

混合式持久化方案兼顾了RDB的速度,和aof的安全性。

Redis 是如何处理过期数据的?

过期淘汰

对于已经过期的数据,Redis 将使用两种策略来删除这些过期键,它们分别是惰性删除定期删除。

惰性删除是指 Redis 服务器不主动删除过期的键值,而是当访问键值时,再检查当前的键值是否过期,如果过期则执行删除并返回 null 给客户端;如果没过期则正常返回值信息给客户端。

它的优点是简单,不需要对过期数据做额外的处理,只是在每次访问时才检查键值是否过期。缺点是删除过期键不及时,造成了一定的空间浪费。

定期删除是指 Redis 服务器每隔一段时间会检查一下数据库,看看是否有过期键可以被清除。

默认情况下 Redis 定期检查的频率是每秒扫描 10 次,用于定期清除过期键。当然此值还可以通过配置文件进行设置,在 redis.conf 中修改配置“hz”即可,默认的值为“hz 10”。定期删除的扫描并不是遍历所有的键值对,这样的话比较费时且太消耗系统资源。Redis 服务器采用的是随机抽取形式,每次从过期字典中,取出 20 个键进行过期检测,过期字典中存储的是所有设置了过期时间的键值对。如果这批随机检查的数据中有 25% 的比例过期,那么会再抽取 20 个随机键值进行检测和删除,并且会循环执行这个流程,直到抽取的这批数据中过期键值小于 25%,此次检测才算完成。Redis 服务器为了保证过期删除策略不会导致线程卡死,会给过期扫描增加了最大执行时间为 25ms,及每次扫描不会超过25ms。

当内存不够用时 Redis 又是如何处理的?

Redis 内存淘汰策略

当 Redis 的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略

当 Redis 内存不够用时,Redis 服务器会根据服务器设置的淘汰策略,删除一些不常用的数据,以保证 Redis 服务器的顺利运行。

在 4.0 版本之前 Redis 的内存淘汰策略有以下 6 种。

noeviction:不淘汰任何数据,当内存不足时,执行缓存新增操作会报错,它是Redis 默认内存淘汰策略。

allkeys-lru:淘汰整个键值中最久未使用的键值。

allkeys-random:随机淘汰任意键值。

volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值。

volatile-random:随机淘汰设置了过期时间的任意键值。

volatile-ttl:优先淘汰更早过期的键值。

而在 Redis 4.0 版本中又新增了 2 种淘汰策略:

volatile-lfu,淘汰所有设置了过期时间的键值中最少使用的键值;

allkeys-lfu,淘汰整个键值中最少使用的键值。

内存淘汰策略可以通过配置文件来修改,redis.conf 对应的配置项是“maxmemory-policy noeviction”,只需要把它修改成我们需要设置的类型即可。

如何保证缓存与数据库双写时的数据一致性?

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,比如更新数据库后,写缓存失败, 那么你如何解决一致性问题?

一般来说,要完全保证数据库和缓存的一致性,需要将请求同步串行化,这样往往性能上是不可接受的,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案。串行化之后,就会导致系统的吞吐量会大幅度的降低。还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再更新缓存。缓存更新如果失败后,可以进行重试,如果重试失败,则将失败的 key 写入消息队列中,待缓存访问恢复后,将这些 key 从缓存删除。这些 key 在再次被查询时,重新从 DB 加载,从而保证数据的一致性。也可以设置较短的过期时间,缩短数据不一致的时间。

场景一:

问题的根源其实是读数据与写数据请求同时并发时,数据库与缓存数据已更新,但访问的数据还是老数据,出现了脏读数据,我们假设读请求流程没有问题,那就分析写请求流程的优化。

1. 先更新缓存,再更新数据库

这个方案肯定不行。原因是更新缓存成功,更新数据库出现异常了,导致缓存数据与数据库数据完全不一致。

2. 先更新数据库,再更新缓存

这个方案同样不行,原理跟第一个一样,数据库更新成功了,缓存更新失败,同样会出现数据不一致问题。

那两种方案都不行,还有什么更好的解决方案呢?我们遇到写请求时,可用先删除缓存数据,再更新数据库,这样不管数据库更新失败还是缓存删除失败,缓存与数据库始终一致这种方案一般可满足上万人并发操作了,因为删除缓存到更新数据库的时间可以用毫秒计算,正常的并发影响不大。但如果是达到上亿级访问,在这时间段内,会出现读请求在写请求更新数据库之前执行,导致数据库与缓存不一致。

备注:多线程情况下此方案也会产生脏数据,可以采取延时双删策略:(1)先淘汰缓存;(2)再写数据库(这两步和原来一样);(3)休眠1秒,再次淘汰缓存。其中1秒的时间设置按照具体业务来设置,即写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。

具体详情可参考此篇博客:https://blog.csdn.net/hukaijun/article/details/81010475

场景二:

上亿级并发访问,导致缓存与数据不一致。

比方:淘宝双11活动,抢购商品,商品数量为100,当前状态是数据库和缓存都是100,这时上亿账户抢购该商品,商品数量要减少。一个消费者A抢购成功,这时应该删除缓存,更新商品数量,在更新商品数量之前,又有一个消费者B来查看该商品数量,由于缓存清空,到数据库查询,该消费者查看到的是商品数量为100,并更新缓存为100,其实商品数量已经被消费者B抢购成功之后,数据库中商品数量更新为99了,缓存与数据库数据不一致。

方案:读写分离。

读请求只访问缓存,写请求只修改数据库和缓存。

写请求修改数据库和缓存是事务性动作,如果更新数据库成功,更新缓存失败,则回滚数据库,保证缓存与数据库数据强一致。这样实现了读写分离,不仅提高了读的响应速度,由写请求负责缓存与数据库一致,只有写请求成功才会影响到缓存的内容,时效性大大增强。

详情请查看此篇博客:https://www.cnblogs.com/williamjie/p/11287317.html

Redis 主从复制的原理是怎样的?

为什么需要主从复制功能呢?有两个作用:

1)读写分离,单台服务器能支撑的QPS是有上限的,我们可以部署一台主服务器、多台从服务器,主服务器只处理写请求,从服务器通过复制功能同步主服务器数据,只处理读请求,以此提升Redis服务能力;

2)数据容灾,任何服务器都有宕机的可能,我们同样可以通过主从复制功能提升Redis服务的可靠性;由于从服务器与主服务器数据保持同步,一旦主服务器宕机,可以立即将请求切换到从服务器,从而避免Redis服务中断。

复制原理:

1. 从节点向主节点发送 sync 命令, 请求同步数据。

2. 主节点收到sync命令,开始执行bgsave持久化数据到一个 rdb文件,并且在持久化期间会将所有新执行的写入命令都保存到一个缓冲区。

3.当持久化数据执行完毕后,主节点将该RDB文件发送给从服务器,从服务器接收该文件,并加载到内存中。

4.主节点将缓冲区中的指令发送给从节点。

5.每当主节点接收到写命令时,都会将该指令按照Redis协议发送给从节点,从节点接收并处理主节点发过来的命令。

图(12): Redis主从复制流程图

上述流程以及可以完成主从复制的基本功能,但是由于bgsave是一个重量级的 操作,如果复制过程中发现了网络问题,从节点重写连到主节点时,又执行了sync 请求,如果这个重连的时间很短的化,主节点数据没有发生很大的变化,这时是没必要重新生成快照的,所以 Redis2.8 以后提出了新的主从复制解决方案,从服务器会记录已经从主节点接收到的数据量(复制偏移量),Redis主节点会维护一个复制缓冲区,记录自己已执行且待发送给从服务器的命令请求,同时还需要记录缓冲区第一个字节的复制偏移量,从节点同步请求改为了 psync,当从节点连接到主节点是会 发送 psync 同时带上已经接收到的复制偏移量,如果该复制偏移量在主节点的复制缓冲区区间内,则不需要执行持久化操作,主节点直接发送复制缓冲区中的指令即可。这就是部分重同步。

你知道有哪些Redis分区实现方案?

1. 客户端分区就是在客户端就已经决定数据会被存储到哪个redis节点或者从哪个redis节点读取。

a. 范围切分

b. hash切分:一致性哈希/普通哈希

2. 代理分区意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。代理根据分区规则决定请求哪些Redis实例,然后根据Redis的响应结果返回给客户端。redis的代理分区实现如Twemproxy。

3. 查询路由(Query routing) 的意思是客户端随机地请求任意一个redis实例,然后由Redis将请求转发给正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,但并不是直接将请求从一个redis节点转发到另一个redis节点,而是在客户端的帮助下直接redirected到正确的redis节点。

为什么要做Redis分片?redis 集群模式的工作原理能说一下么?

图(13):Redis集群分片

redis集群是一个由多个主从节点群组成的分布式服务器群,它具有复制、高可用和分片特 性。Redis集群不需要sentinel哨兵也能完成节点移除和故障转移的功能。Redis集群模式没有中心节点,可水平扩展,据官方文档称可以线性扩展到上万个节点(官方推荐不超过1000个节点)。

高并发:支持大并发,通过横向拓展实现,通过 --cluster add-node 添加新机器到集群执行 reshard 命令,重新分配slot ,将相对均摊了 slot 的分布,缓冲了 其他机器的并发压力,从而应对百万,甚至上千万的并发。

复制:每个小集群都是一个主从复制的架构,从而保证了主节点挂掉的时候,不至于丢失全部数据,当选举产生新的master节点后,继续对外进行服务,在主备切换过程中,部分key会有影响,但是其他分片上的key不会有任何影响,从而保证了高可用的场景。

分片:每个不同的主从架构小集群,数据是不一致的,客户端通过哈希函数,将数据路由到不同的数据节点,从而实现了数据的分片。这样技术内存不够用的时候,只需要添加新的集群节点进来,重新分配一下slot 就可以了。

方案说明:

1. 通过哈希的方式,将数据分片,每个节点均分存储一定哈希槽(哈希值)区间的数据,默认分配了16384 个槽位

2. 每份数据分片会存储在多个互为主从的多节点上。

3. 数据写入,先写主节点,再同步到从节点。

4. 同一分片多个节点间的数据不保持一致性。

5. 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点。

在集群模式下,redis 的 key 是如何寻址的?

每个服务器都会维护一个 slot表,对应了每个 slot 真实的物理节点,当服务器收到命令时,会对 key进行 crc16 进行哈希,得到哈希值后,对 16384 进行取模,取模的值就是key对应的slot,如果这个slot 是由当前服务器处理,则直接继续执行命令,如果不是由当前节点处理,则返回该slot 对应的服务器节点地址,由客户端重写请求对应的地址。

Redis分片有什么缺点?

1.涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。

2.同时操作多个key,则不能使用Redis事务。

    2.1)分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集。

    2.2)当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。

Redis是单线程的,如何提高多核CPU的利用率?

可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?

使用keys指令可以扫出指定模式的key列表。由于redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

什么是缓存失效?如何解决 ?

在写缓存时,我们一般会根据业务的访问特点,给每种业务数据设置一个过期时间,让缓存数据在这个固定的过期时间后被淘汰。一般情况下,因为缓存数据是逐步写入的,所以也是逐步过期被淘汰的。但在某些场景,如缓冲预热时,一大批数据会被系统主动或被动从 DB 批量加载,然后写入缓存。这些数据写入缓存时,由于使用相同的过期时间,在经历这个过期时间之后,这批数据就会一起到期,从而被缓存淘汰。此时,对这批数据的所有请求,都会出现缓存失效,从而都穿透到 DB,DB 由于查询量太大,就很容易压力大增,请求变慢。

解决方案

对于批量 key 缓存失效的问题,原因既然是预置的固定过期时间,那在设计缓存的过期时间时,可以使用:过期时间=固定时间+随机时间。即相同业务数据写缓存时,在基础过期时间之上,再加一个随机的过期时间,让数据在未来一段时间内慢慢过期,避免瞬时全部过期,对 DB 造成过大压力。

什么是缓存穿透,如何解决?

大量的请求访问一个不存在的Key, 由于缓冲数据不存在,则会穿透到数据库,这个时候,大量请求同时访问数据库,容易造成数据库奔溃,从而使系统对外不可用,这就是缓冲穿透。缓存穿透存在的原因,就是因为我们在系统设计时,更多考虑的是正常访问路径,对特殊访问路径、异常访问路径考虑相对欠缺。如果由大量的请求被认为构造,则对系统的伤害时致命性的。

解决方案

1. 查询这些不存在的数据时,第一次查 DB,虽然没查到结果返回 NULL,仍然记录这个 key 到缓存,只是这个 key 对应的 value 是一个特殊设置的值,如empty,且设置一个过期时间。但是当key 过多的时候,也会操作内存的浪费。

2. 构建一个 BloomFilter (布隆过滤器)缓存过滤器,记录全量数据,这样访问数据时,可以直接通过 BloomFilter 判断这个 key 是否存在,如果不存在直接返回即可,根本无需查缓存和 DB。

什么是缓存雪崩? 

在流量洪峰到达时,大量的正常请求导致了,缓存服务器宕机,所有请求到达db,导致了db服务不可用,就时缓冲雪崩。

解决方案:

1. 对缓存进行实时监控,当请求访问的慢速比超过阈值,及时报警,通过自动故障转移服务降级停止部分非核心接口访问

2. 对大热key,缓存在本地,缓解redis压力。

分布式锁

1.过期时间设置问题。

2.保证原子操作问题。

3.客户端误删问题。

4.释放锁的原子操作问题。

5.主备架构,故障转移数据同步问题。

RedLock

1.获取当前时间。

2.按顺序依次向N个Redis节点获取锁,为确保某个Redis节点失败不影响算法继续进行,获取锁还需要设置一个超时时间,Redis获取锁失败,立即尝试下一个节点。

3.计算加锁过程的耗时时间, 当前时间减第一步获取锁的时间如果小于锁的有效时间,且客户端从大多数Redis节点都加锁成功,则认为加锁成功,否则认为加锁失败。

4.如果最终获取锁的操作成功,锁的有效时间应该重新计算,锁的有效时间=设置的有效时间-加锁消耗的时间。

5.如果加锁失败了,则客户端应该释放所有节点对应得锁。

Redis线程模型原理?

1.非阻塞IO

2.事件轮询(多路复用)

你可能感兴趣的:(Redis笔记)