Redis

Redis简介

  1. Redis是一个速度非常快的非关系型/NoSQL数据库,它不使用表,也不会预定义或强制要求用户对Redis存储的不同数据进行关联;
  2. Redis为什么速度非常快(读写性能可以达到10万/秒):1 纯内存操作,数据存储在内存中;2 数据结构简单,对数据操作也简单,底层又做了优化;3 单线程:不存在多进程或者多线程导致的切换而消耗CPU资源,也不用去考虑各种锁的问题;

Redis为什么使用单线程

  1. 易于实现,开发和维护更简单;
  2. 内部使用了基于epoll的多路复用,使用单线程也能够并发地处理多客户端的请求;
  3. 基于内存,查找和操作时间复杂度都是O(1),CPU不是其性能瓶颈,瓶颈很可能是机器内存与网络带宽的大小;
  4. 不是全面单线程,持久化、异步删除、集群数据同步等是由额外线程来执行,防止同步代码占用主线程,导致阻塞;

Redis基于单线程,为什么还这么快?

  1. 基于内存操作,数据存储在内存中,其所有的运算都是内存级的;
  2. 数据结构简单,专门为Redis设计的数据结构的查找和操作的时间复杂度都是O(1);
  3. 单线程模型,避免了多进程、多线程切换消耗CPU资源,也不用考虑各种锁问题;
  4. 使用IO多路复用功能来监听多个socket连接,这样就可以使用一个线程来处理多个情况,从而减少线程切换带来的开销,同时也避免了IO阻塞操作,极大提高了Redis性能;

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

  1. 主要从”高性能“和”高并发“这两点来看;
  2. 高性能:如果用户是第一次访问数据库中的某些数据,过程将比较慢,因为是从硬盘上读取的;然后将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了;操作缓存就是直接操作内存,所以速度非常快;如果数据库中的对应数据改变,同步改变缓存中相应的数据即可;
  3. 高并发:直接操作缓存能够承受的请求数远远大于直接访问数据库;因此考虑把数据库中的部分数据复制到缓存中去,这样用户的一部分请求会直接到缓存而不用经过数据库;

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

  1. 缓存分本地缓存和分布式缓存。以Java为例,使用自带的Map或guava实现的是本地缓存,生命周期随JVM的销毁而结束,在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性;
  2. Redis称为分布式缓存,在多实例情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持Redis服务的高可用,整个程序架构比较复杂;

Redis和Map的详细区别

  1. Redis可以实现分布式的缓存,Map只能存在创建它的程序里;
  2. Redis的缓存可以持久化,Map是内存对象,程序一重启数据就没了;
  3. Redis有缓存过期机制,Map无此功能;
  4. Redis可以用几十G内存来做缓存,Map不行,一般JVM分几个G就很大了;

Redis读写逻辑

读逻辑
在这里插入图片描述
写逻辑
在这里插入图片描述
高并发一次请求的流程
在这里插入图片描述

  1. 当一个请求到达服务器时,只是把业务数据在Redis上进行读写,而没有对数据库进行任何的操作,这样就能大大提高读写的速度,从而满足高速响应的需求;
  2. 但是这些缓存的数据仍然需要持久化,也就是存入数据库之中,所以在一个请求操作完Redis的读/写之后,会去判断该高速读/写的业务是否结束,这个判断通常会在秒杀商品为0,红包金额为0时成立,如果不成立,则不会操作数据库;如果成立,则触发事件将Redis 的缓存的数据以批量的形式一次性写入数据库,从而完成持久化的工作。

Redis的线程模型

  1. Redis基于Reactor模式开发了自己的网络事件处理器:file event handler,它是单线程的;
  2. file event handler的结构包含4个部分:多个socket、IO多路复用程序、事件分派器、事件处理器(连接应答处理器、命令请求处理器、命令回复处理器);
  3. 1 I/O多路复用程序会同时监听多个socket,并将socket放入一个队列中排队;2 事件分派器从队列中取出socket01交给连接应答处理器;3 连接应答处理器会将socket01的AE_READABLE事件与命令请求处理器相关联;4 假设客户端执行set操作,这时命令请求处理器会从socket01读取key value,在内存中完成key value的设置;5 在内存中完成设置后,会将socket01的AE_WRITEABLE事件与命令回复处理器相关联;6 命令回复处理器向socket01写入本次操作的结果,然后解除关联;

Redis数据结构

  1. Redis存储键(key)和5种不同类型的值(value)之间的映射;
  2. 这5种类型分别是:string(字符串)、hash(哈希表)、list(列表)、set(集合)、zset(有序集合);

string

作为常规的key-value缓存应用,微博数、粉丝数等

hash

是一个string类型的field和value的映射表,适合用于存储对象
在这里插入图片描述

list

关注列表、粉丝列表等。

set

在微博中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能。

zset

内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,跳跃表按score从小到大保存所有集合元素。

应用场景:排行榜

Redis与Memcached的异同

两者都是非关系型内存键值数据库,主要有以下不同:

  1. 数据类型:Redis支持5种不同的数据类型,可以更灵活地解决问题;Memcached仅支持字符串类型;
  2. 数据持久化:Redis支持两种持久化策略:RDB(快照)和AOF(只追加文件);Memcached不支持持久化;
  3. 分布式:Redis支持分布式;Memcached不支持分布式;
  4. 内存管理机制:在Redis中,并不是所有数据都一直存储在内存中,可以将一些很久不用的数据交换到磁盘,而Memcached的数据会一直在内存中;
  5. 线程模型:Redis使用的是单线程的多路IO复用模型;Memcached使用的是多线程非阻塞IO复用模型;

Redis 是如何判断数据是否过期?

Redis 通过一个叫做过期字典redisDb来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。

查看剩余存活时间

TTL key #计算key的剩余生存时间,s
PTTL key #计算key的剩余生存时间,ms

返回值:

  • 当 key 不存在时,返回 -2 。
  • 当 key 存在但没有设置剩余生存时间时,返回 -1 。
  • 否则,以秒为单位,返回 key 的剩余生存时间。

Redis 设置过期时间、定期删除、惰性删除

  1. Redis对存储在 Redis数据库中的数据设置有⼀个过期时间;
  2. 假设设置了⼀批 key 只能存活1小时,那么1小时后,Redis将对这批key执行定期删除+惰性删除操作;
  3. 定期删除:Redis默认是每隔 100ms 就随机抽取⼀些设置了过期时间的key,检查其是否过期,如果过期就删除;
  4. 惰性删除 :数据到达过期时间,不做处理。等下次访问该数据时,如果未过期,返回数据 ;如果发现已过期,删除,返回不存在;
  5. 仅仅通过设置过期时间还是有问题的。设想:如果定期删除漏掉了很多过期 key,然后也没及时去查,即没走惰性删除,将造成大量过期key堆积在内存里,导致Redis内存块耗尽。这就需要Redis数据淘汰机制来解决这个问题了;

Redis数据淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据)

Redis提供了8种数据淘汰策略:

  1. volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰;
  2. volatile-lfu:从已设置过期时间的数据集中挑选最不经常使用的数据淘汰;
  3. volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰;
  4. volatile-random:从已设置过期时间的数据集中选择任意数据淘汰;
  5. allkeys-lru:当内存不足以容纳新写入数据时,移除最近最少使用的数据(最常用的淘汰策略);
  6. allkeys-lfu:当内存不足以容纳新写入数据时,移除最不经常使用的数据;
  7. allkeys-random:当内存不足以容纳新写入数据时,从数据集中随机选择数据淘汰;
  8. no-eviction:禁止驱逐数据,即当内存不足以容纳新写入数据时,新写入操作会报错;

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

为了系统故障或重启机器后恢复数据,我们需要持久化数据即将内存中的数据写入到硬盘里。Redis有两种持久化方式: RDB快照和AOF日志。

RDB持久化是通过定期生成数据快照来实现的。在指定的时间间隔内,Redis会将内存中的数据集快照保存到磁盘上,形成一个RDB文件,也可以通过执行BGSAVE命令来手动触发。优点是快速恢复,因为只需要将单个文件加载到内存中即可恢复数据;以及更小的存储空间,因为RDB文件是压缩的二进制文件。缺点是可能会丢失最后一次快照之后的所有数据;

AOF持久化是通过记录所有写操作到一个日志文件中来实现的。每当执行一个写命令时,这个命令都会被追加到AOF文件的末尾。由于AOF文件记录了所有的写操作,因此可以通过重放这些操作来恢复数据。优点包括更好的数据安全性,因为每个写操作都被记录了。缺点是如果AOF文件很大,那么恢复数据可能需要较长的时间;此外,AOF文件的大小会随着时间的推移而增大。

综上所述,RDB提供了定时备份的快照,适用于需要快速恢复的场景;而AOF提供了连续的写操作日志,适用于需要高数据一致性的场景。

  1. RDB在redis.conf配置文件中默认有如下配置:
save 900 1 #在900(15分钟)之后,如果⾄少有1个key发⽣变化,Redis就会⾃动触发BGSAVE命令创建快照。

save 300 10 #在300(5分钟)之后,如果⾄少有10个key发⽣变化,Redis就会⾃动触发BGSAVE命令创建快照。

save 60 10000 #在60(1分钟)之后,如果⾄少有10000个key发⽣变化,Redis就会⾃动触发BGSAVE命令创建快照。
  1. AOF在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度

appendfsync everysec #每秒钟同步⼀次,显式地将多个写命令同步到硬盘

appendfsync no #让操作系统决定何时进行同步

为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步⼀次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

Redis事务

Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  1. 批量操作在发送 EXEC 命令前被放入队列缓存;
  2. 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行;
  3. 在事务执行过程中,其他客户端提交的命令请求不会插入到事务执行命令序列中;

一个事务从开始到执行会经历以下三个阶段:

  1. 开始事务;
  2. 命令入队;
  3. 执行事务;

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。

事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

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

缓存雪崩

  • 概念:缓存同⼀时间大面积失效,导致后面的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。或者缓存服务器宕机了,造成了整个缓存的不可用。
  • 有哪些解决办法?
    1. 事前:1 尽量保证整个 Redis集群的高可用性(哨兵模式、集群模式);2 过期时间加随机数,尽量避免缓存同时失效的情况发生;
    2. 事中:限流&降级,避免数据库崩掉(假设限制每秒就2000个请求,一秒过来5000个请求,此时只有2000个请求会通过限流组件,进入数据库,剩余的3000个请求走降级,调用自己开发好的一个降级的组件,返回一些默认的值,比如友情提示,或空值);
    3. 事后:利用 Redis持久化机制尽快恢复缓存集群,一旦重启,自动从磁盘上加载数据恢复内存中的数据;

缓存穿透

  • 概念:大量请求的 key 不存在于缓存中,导致请求直接落到了数据库上,根本没有经过缓存这一层。举例:某黑客故意制造缓存中不存在的 key 发起大量请求,导致大量请求落到数据库,⼀般 3000 个并发请求就能打死大部分数据库了。
  • 有哪些解决办法?
    1. 做好参数校验:⼀些不合法的参数请求直接抛出异常信息返回给客户端,比如查询的数据库 id 不能小于0等;
    2. 缓存无效 key : 如果缓存和数据库都查不到某个 key 的数据就写这个key到 Redis中去并设置过期时间。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求key,会导致 Redis中缓存大量无效的 key,因此这种方案并不能从根本上解决此问题;
    3. 布隆过滤器:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程;

缓存击穿

缓存雪崩是大批热点数据突然失效,而缓存击穿是指某一个热点数据突然失效,导致大量请求直接访问数据库,解决方案是考虑这个热点数据不设过期时间。

如何解决 Redis 的并发竞争 Key 问题

Redis 的并发竞争 Key 的问题也就是多个系统同时对⼀个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同。

解决方案一:分布式锁+时间戳

(1)如果对这个key操作,不要求顺序

这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可。

(2)如果对这个key操作,要求顺序

假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC。

期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这种情况下我们在操作数据时需要保存一个时间戳。假设时间戳如下

系统A:key1 {valueA 3:00}

系统B:key1 {valueB 3:05}

系统C:key1 {valueC 3:10}

那么,假设系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。

解决方案二:消息队列

在并发量过大的情况下,可以通过消息中间件进行处理,并发读写串行化:把set操作放在队列中使其串行化,必须一个一个执行。

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

写请求来了,要更新数据库和缓存,一前一后更新,就可能导致缓存和数据库中的数据在一段时间内不一致。

一般来说,如果允许缓存可以偶尔跟数据库有不一致的情况,也就是说系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做读请求和写请求串行化的方案: 即读请求和写请求串到一个内存队列里去。串行化之后,会导致系统的吞吐量大幅度降低。

不管是先删除缓存再写数据库,还是先写数据库再删除缓存,都有可能出现数据不一致的情况:

  1. 先删再写单库情况:1 A删除缓存,还没有来得及写库;2 B来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据;
  2. 先删再写主从同步读写分离的情况:1 A删除缓存;2 A写主数据库;3 B来读取,发现缓存为空,就去从数据库中读取数据写入缓存,由于主从同步还没同步成功,因此为脏数据;
  3. 先写再删:1 A读数据,发现没有缓存,从数据库读然后写进缓存时卡顿了;2 B写数据库,删缓存;3 A恢复,老数据写进缓存;

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。

延时双删策略

具体步骤如下:

  1. 先删缓存;
  2. 再写数据库;
  3. 休眠1秒;
  4. 再删缓存。

这个1秒怎么确定的,具体该休眠多久呢?

需要评估项目的读数据业务逻辑的耗时+数据库主从同步的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

异步淘汰缓存

新增一个读取binlog异步淘汰缓存模块,读取binlog,然后进行异步淘汰。

  1. 读请求走Redis;
  2. 写请求走MySQL;
  3. 更新Redis数据:使用MySQL的binlog来更新到Redis;

Redis更新

数据操作主要分为两块:

  1. 一个是全量(将全部数据一次写入到Redis)
  2. 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delete变更数据。这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新,就无需再从业务线去操作缓存内容。

Redis的高可用

  1. 在使用最简单的单机版 Redis 时,遇到了 Redis 故障宕机后数据无法恢复的问题,因此引入了「数据持久化」,将内存中的数据保存到磁盘上,以便 Redis 重启后能快速恢复数据。在进行数据持久化时,面临如何更高效地将数据保存到磁盘的问题,Redis 提供了 RDB 和 AOF 两种方案,分别对应数据快照和实时命令记录。当对数据完整性要求不高时,可以选择 RDB 持久化方案;如果对数据完整性要求较高,可以选择 AOF 持久化方案。但是AOF 文件体积会随着时间增长变得越来越大,优化方案是使用 AOF rewrite(由于 AOF 文件中记录了每次写操作,但对于同一个 key 可能会发生多次修改,我们只保留最后一次修改的值) 的方式对其进行瘦身,减小文件体积;

  2. 虽然可以通过数据恢复的方式还原数据,但恢复数据仍需要花费时间,这意味着业务应用仍会受到影响。进一步优化,采用「多副本」的方案,让多个实例保持实时同步,当一个实例故障时,把其他实例提升上来继续提供服务,因此引入了「哨兵」集群,哨兵集群通过互相协商的方式,发现故障节点,并可以自动完成切换,从而大幅降低对业务应用的影响。稳定性:Redis 故障宕机,通过哨兵 + 副本,可以自动完成主从切换性能:读请求量增长,可以再部署多个 slave,实现读写分离,分担读压力;
    在这里插入图片描述

  3. 最后,将关注点放在如何支持更大的写流量上,因此引入了「分片集群」来解决这个问题,让多个 Redis 实例分担写压力。面对更大的流量,我们还可以添加新的实例进行横向扩展,进一步提高集群性能;每个节点各自存储一部分数据,所有节点数据之和才是全量数据。制定一个路由规则,对于不同的 key,把它路由到固定一个实例上进行读写。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

hellosc01

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

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

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

打赏作者

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

抵扣说明:

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

余额充值