文章目录
Redis为什么那么快?
(1)Redis是基于内存操作,避免了磁盘I/O的开销
(2)Redis是高效数据结构,对数据的操作也比较简单
(3)Redis是单线程模型,避免了多线程中上下文频繁切换的操作
(4)使用多路I/O复用模型,非阻塞I/O
为什么要使用Redis,Redis的使用场景?
使用Redis缓存的目的就是提升读写性能。而实际业务场景下,更多的是为了提升读性能,还可以带来更高的并发量。Redis 的读写性能比 Mysql 好的多,我们就可以把Mysql中的热点数据缓存到 Redis 中,提升读取性能,同时也减轻了 Mysql 的读取压力
①适合存储热点数据(热点商品、资讯、新闻)
②缓存
③任务队列
④消息队列
⑤分布式锁
Redis有哪些数据类型?
(1)String字符串类型
(2)列表list,常用来存储一个有序数据,可以看做是一个双向链表结构。既可以支持正向检索和也可以支持反向检索。
(3)set,集合中不能出现重复的数据。它的底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)
(4)sorted set,不允许重复的成员。每个元素都会关联一个double类型的分数(score)。redis正是通过分数来为集合中的成员进行从小到大排序。有序集合的成员是唯一的,但分数却可以重复。
(5)Hash,也叫散列,可以将对象中的每个字段独立存储
(6)Bitmaps相当于是一个以位为单位的数组,数组的每个单位只能存储0和1,是一种统计二值状态的数据类型
(7)HyperLogLog,统计一个集合中不重复的元素个数
(8) Redis流(Stream):Redis流是Redis版的MQ消息中间件+阻塞队列
数据库缓存双写不一致问题
(1)修改DB更新缓存场景:在高并发写请求场景下,若多个请求对数据库中同一个数据进行修改,修改后还需要更新缓存中相关的数据,那么就有可能会出现缓存与数据库中数据不一致的问题。

(2)修改DB删除缓存场景:在高并发读写情况下,若这些请求对数据库中同一个数据的操作既包含写也包含读,且修改后还要删除缓存中相关数据,那么就可能出现缓存与数据库不一致的情况。

如何保证Redis和MySQL的数据一致性
(1)先更新数据库, 再更新缓存
如果先更新数据库,再更新缓存,如果缓存更新失败,就会导致数据库和 Redis中的数据不一致。
(2)先删除缓存, 再更新数据库
如果是先删除缓存, 再更新数据库,理想情况是应用下次访问 Redis 的时候,发现 Redis 里面的
数据是空的, 就从数据库加载保存到 Redis 里面, 那么数据是一致的。 但是在极端情况下, 由于
删除 Redis 和更新数据库这两个操作并不是原子的,所以这个过程如果有其他线程来访问, 还是会
存在数据不一致问题。
(3)延时双删策略(推荐)
①先删除redis中的数据
②更新数据库
③再延时删除redis

例如:客户端1去修改接口的数据时,先删除已有的redis缓存;这时,客户端2查询这个接口,因为redis中没有数据,就会去查找MySQL数据库,将数据缓存到redis中。此时,客户端1更新MySQL数据后,再延时删除redis缓存(延时删除的目的是为查询后,保证构建缓存执行完毕后再去删除,以免删不掉)。当下次再有客户访问时,就又会重新查询数据库,从而保证数据的一致性。
缺点:由于它有延时操作,会造成服务器的阻塞。所以,不适合高并发的场景
(4)在极端情况下保证Redis和Mysql数据最终一致性(推荐)
第二次如果删除失败,使用mq异步重试机制。但是耦合性较高,修改、新增都要加程序。可以使用canal,canal利用MySQL的主从复制机制伪装成MySQL的一个从机,他会不断的去监听我们这个二进制日志文件,当我们的数据发生变化的时候,他会主动的给我们的mq发送一个消息。
Redis如何内存优化?
(1)尽可能的使用散列表,散列表使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。
(2)比如你的web系统中有一个用户对象,不要为这个用户的名称,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。
Redis中的管道有什么用?
一次请求/响应,服务器能处理新的请求,既使旧的请求还未被响应。这样就可以将多个命令发送到服务器,而
不用等待回复,最后在一个步骤中读取该答复,这就是管道。这是一种几十年来广泛使用的技术。例如许多POP3
协议已经实现支持这个功能,大大加快了从服务器下载新邮件的功能。
Redis的持久化
Redis持久化机制是什么?以及优缺点
Redis的持久化有RDB和AOF两中方式
(1)RDB:在指定的时间间隔内将内存中的数据集快照写入磁盘。保存备份时它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中
a.优点
①适合大规模的数据恢复
②按照业务定时备份
③对数据完整性和一致性要求不高
④RDB文件在内存中的加载速度要比AOF快得多
b.缺点
①rdb是在一定间隔时间做一次备份,如果redis意外down掉的话,就会丢失当前最近一次快照期间的数据
②rdb是全量同步,如果数据量太大就会导致I/O阻塞,严重影响服务器性能
③rdb依赖于主进程的fork(通过复制父进程的内存,创建一个完全相同的子进程。这个子进程伴随着父进程一起运行,但有自己独立的内存空间),在更大的数据集中,可能会导致服务请求的瞬时延迟
(2)AOF:以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
a.优点:更好的保护数据不丢失,可做紧急恢复
b.缺点:相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb;aof运行效率也要慢于rdb
RDB原理是什么?
原理是redis会单独创建(fork)一个与当前进程一模一样的子进程来进行持久化,这个子进程的数据和原进程一模一样,会先将数据写到一个临时文件中,持久化结束后,再用这个临时文件替换上次持久化好的文件,整个过程中,主进程不进行任何io操作,确保极高的性能
RDB文件在哪?
redis.conf配置文件的dbfilename指定了rdb文件的名字(默认dump.rdb),dir指定dbfilename文件存放目录。
哪些情况会触发RDB快照
(1)配置文件中默认的快照配置
(2)手动save/bgsave命令
(3)执行flushall/flushdb命令也会产生dump.rdb文件,但里面是空的,无意义
(4)执行shutdown且没有设置开启AOF持久化
(5)主从复制时,主节点自动触发
AOF原理是什么?
原理是将redis的操作记录(写操作)以追加的方式写入文件。文件默认名称是appendonly.aof,redis重启时重新执行aof文件中的写操作
高并发问题
缓存预热
缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
缓存雪崩
1.缓存雪崩的发生
(1)redis主机挂了,redis全盘崩溃,属于硬件运维责任
(2)redis中有大量key同时失效,属于软件责任
2.预防
(1)redis中的key设置为永不过期或者过期时间错开
(2)redis缓存集群实现高可用
(3)多缓存结合:使用本地缓存+redis缓存
(4)服务降级,如sentine限流或降级
(5)花钱买阿里云的云数据库redis版
缓存穿透
1.缓存穿透是什么(从头到尾、自始至终都没有)
请求去查询一条记录,先查redis无,后查mysql无,都查不到该条记录,每次查询都要访问数据库,就会导致数据库压力剧增,这种现象我们称为缓存穿透。
2.解决
(1)空对象或者缺省值(约定为零、负数或者defaultNull)缓存
(2)可以使用布隆过滤器解决缓存穿透的问题把已存在数据的key存在布隆过滤器中,相当于redis前面挡着一个布隆过滤器。当有新的请求时,先到布隆过滤器中查询是否存在:
①如果布隆过滤器中不存在该条数据则直接返回;
②如果布隆过滤器中已存在,才去查询缓存redis,如果redis里没查询到则再查询Mysql数据库

布隆过滤器BloomFilter
布隆过滤器是什么、能干嘛
布隆过滤器是由一个初值都为零的bit数组和多个无偏(无偏表示分布均匀)哈希函数构成,用来快速判断集合中是否存在某个元素。
①布隆过滤器能够高效的插入和查询,占用空间少,但返回的结果是不确定、不够完美。一个元素如果判断存在,元素不一定存在;但是判断结果为不存在,则一定不存在。
②布隆过滤器不能删除元素,由于涉及hashcode判断依据,删掉元素会导致误判率增加

原理
布隆过滤器是一种专门用来解决去重问题的高级数据结构。实质就是一个大型位数组和几个不同的无偏hash函数(无偏表示分布均匀)。由一个初值都为零的bit数组和多个个哈希函数构成,用来快速判断某个数据是否存在。但是跟 HyperLogLog 一样,它也一样有那么一点点不精确,也存在一定的误判概率
(1)添加key时:使用多个hash函数对key进行hash运算得到一个整数索引值,对位数组长度进行取模运算得到一个位置,每个hash函数都会得到一个不同的位置,将这几个位置都置1就完成了add操作。
(2)查询key时:只要有其中一位是零就表示这个key不存在,但如果都是1,则不一定存在对应的key。
布隆过滤器的误判
(1)布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位置1了,这样就无法判断究竟是哪个输入产生的,因此误判的根源在于相同的 bit 位被多次映射且置 1。
(2)这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。
如果我们直接删除这一位的话,会影响其他的元素
(3)特性:布隆过滤器可以添加元素,但是不能删除元素。因为删掉元素会导致误判率增加
hash冲突导致数据不精确
(1)哈希函数的概念是:将任意大小的输入数据转换成特定大小的输出数据的函数,转换后的数据称为哈希值或哈希编码,也叫散列值。如果两个散列值是不相同的(根据同一函数)那么这两个散列值的原始输入也是不相同的。这个特性是散列函数具有确定性的结果,具有这种性质的散列函数称为单向散列函数。散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同,这种情况称为“散列碰撞(collision)”。用 hash表存储大数据量时,空间效率还是很低,当只有一个 hash 函数时,还很容易发生哈希碰撞。所以我们一般用的多个hash 函数。

(2)布隆过滤器的误判是指多个输入经过哈希之后在相同的bit位 置1了,这样就无法判断究竟是哪个输入产生的,这种情况也造成了布隆过滤器的删除问题,因为布隆过滤器的每一个 bit 并不是独占的,很有可能多个元素共享了某一位。如果我们直接删除这一位的话,会影响其他的元素

布隆过滤器的使用场景
(1)可以使用布隆过滤器解决缓存穿透的问题
(2)黑名单校验,识别垃圾邮件
①发现存在黑名单中的,就执行特定操作。比如:识别垃圾邮件,只要是邮箱在黑名单中的邮件,就识别为垃圾邮件。
②假设黑名单的数量是数以亿计的,存放起来就是非常耗费存储空间的,布隆过滤器则是一个较好的解决方案。
③把所有黑名单都放在布隆过滤器中,在收到邮件时,判断邮件地址是否在布隆过滤器中即可
缓存击穿
(一开始有,后来没有了)
1.缓存击穿是什么:大量的请求同时查询一个key,此时这个key正好失效了,就会导致数据库压力剧增
2.发生:热点key突然失效
3.缓存击穿解决方案
(1)对于频繁访问的热点key,干脆就不设置过期时间
(2)采用双检加锁策略:如果多个线程同时去查询数据库的这条数据,那么我们可以在第一个查询数据的请求上使用一个 互斥锁来锁住它。其他的线程走到这一步拿不到锁就等着,等第一个线程查询到了数据,做了缓存。后面的线程进来发现已经有缓存了,就直接走缓存。

工作中哪里使用过redis
(1)创建任务编号
private int createCodeExec(){
String key = "COUNTER:LINE_TASK_CODE_SERIAL";
Long serial = redisTemplate.opsForValue().increment(key); //如果这个key在缓存中不存在则初始化值为0,并返回1作为增加后的值
Long counterExpire = redisTemplate.getExpire(key);
if(counterExpire<0){
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.HOUR_OF_DAY,1);
calendar.set(Calendar.MINUTE,0);
calendar.set(Calendar.SECOND,0);
calendar.set(Calendar.MILLISECOND,0);
redisTemplate.expireAt(key, calendar.getTime()); //设置key失效时间为当前时间的下一个小时整点的时间
}
// 获取当前时间,并按照 "MMddHH000" 的格式转换为字符串,然后将其解析为一个整数。
int num = Integer.parseInt(DateTime.now().toString("MMddHH000"));
// String.format("%1$-" + 9 + "s", num) 的作用是将 num 格式化为一个长度为9的字符串,并使用空格在右侧进行填充,以实现右对齐
// replace(' ', '0') 将空格替换为0
num = Integer.valueOf(String.format("%1$-" + 9 + "s", num).replace(' ', '0'));
return (int) (num+serial); // 将num和redis生成的value值进行相加
}
Redis的过期键删除策略
(1)惰性删除策略:只会在获取键时才对键进行过期检查,不会在删除其它无关的过期键花费过多的CPU时间。
①优点:对CPU时间非常友好
②缺点:对内存非常不友好
举个例子,如果数据库有很多的过期键,而这些过期键又恰好一直没有被访问到,那这些过期键就会一直占用着宝贵的内存资源,造成资源浪费。
(2)定期删除策略:由activeExpireCycle函数实现,每当Redis服务器的周期性操作serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。
①activeExpireCycle函数的大体流程为:
函数每次运行时,都从一定数量的数据库中随机取出一定数量的键进行检查,并删除其中的过期键,比如先从0号数据库开始检查,下次函数运行时,可能就是从1号数据库开始检查,直到15号数据库检查完毕,又重新从0号数据库开始检查,这样可以保证每个数据库都被检查到。
redis的集群有几种实现方式?
分布式锁的实现方法有很多,常见的有以下几种:
数据库锁:使用数据库中的行锁或表锁来实现分布式锁。
文件锁:使用文件来实现分布式锁。
Zookeeper锁:使用Zookeeper来实现分布式锁。
Redis锁:使用Redis来实现分布式锁。
消息队列锁:使用消息队列来实现分布式锁
什么是Redis哨兵模式
Redis Sentinel 是一个分布式系统, 你可以在一个架构中运行多个 Sentinel 进程(progress), 这些进程使用流言协议(gossip protocols)来接收关于主服务器是否下线的信息,并使用投票协议(agreement protocols)来决定是否执行自动故障迁移,以及选择哪个从服务器作为新的主服务器。
Redis Sentinel作用
(1)监控(Monitoring): Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
(2)提醒(Notification): 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
(3)自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时,Sentinel 会开始一次自动故障迁移操作,它会将失效主服务器的其中一个从服务器升级为新的主服务器,并让失效主服务器的其他从服务器改为复制新的主服务器;当客户端试图连接失效的主服务器时,集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。
Sentinel的工作方式
(1)每个 Sentinel 以每秒钟一次的频率向它所知的 Master,Slave 以及其他 Sentinel 实例发送一个 PING 命令。
(2)如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值,则这个实例会被 Sentinel 标记为主观下线。
(3)如果一个 Master 被标记为主观下线,则正在监视这个 Master 的所有 Sentinel 要以每秒一次的频率确认 Master 的确进入了主观下线状态。
(4)当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认 Master 的确进入了主观下线状态,则 Master 会被标记为客观下线。
(5)在一般情况下,每个 Sentinel 会以每 10 秒一次的频率向它已知的所有 Master,Slave 发送 INFO 命令。
(6)当 Master 被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
(7)若没有足够数量的 Sentinel 同意 Master 已经下线,Master 的客观下线状态就会被移除。
(8)若 Master 重新向 Sentinel 的 PING 命令返回有效回复,Master 的主观下线状态就会被移除。
Redis Sentinel优缺点
(1)优点
①监控主数据库和从数据库是否正常运行
②主数据库出现故障时,可以自动将从数据库转换为主数据库,实现自动切换
③如果 redis 服务出现问题,会发送通知
(2)缺点
①主数据库出现故障时,选举切换的时候容易出现瞬间断线现象
②不能自动扩容
Redis Sentinel原理
(1)哨兵模式是一种特殊的模式,它是 Redis 高可用的一种实现方案。哨兵也是一个独立的进程, 可以实现对 Redis 实例的监控、通知、自动故障转移。
(2)实际上,每个哨兵节点每秒通过 ping 去进行心跳监测(包括所有 redis 实例和 sentinel 同伴),并根据回复判断节点是否在线。
(3)如果某个 sentinel 线程发现主库没有在给定时间( down-after-milliseconds)内响应这个 PING,则这个 sentinel 线程认为主库是不可用的,这种情况叫 “主观失效”(即SDOWN);这种情况一般不会引起马上的故障自动转移,但是当多个 sentinel 线程确实发现主库是不可用并超过 sentinel.conf 里面的配置项 sentinel monitor mymaster {#ip} {#port} {#number} 中的 #number 时候(这里实际上采用了流言协议),一般其余 sentinel 线程会通过 RAFT 算法推举领导的 sentinel 线程负责主库的客观下线并同时负责故障自动转移,这种情况叫 “客观失效”(即 ODOWN)。
redis主从同步(主从复制)
为什么需要主从复制,主从复制的作用
(1)假设我们只部署了一台Redis服务器,某个时刻redis服务挂了,就会有大量的请求直接打到数据库,造成数据库cpu飙升,严重的可能导致数据库直接挂掉,这就是我们常说的单点故障。
(2)主从复制的作用
①解决单机故障,master节点出现故障的时候,从节点可以提供服务
②读写分离,master节点主要是写,slave节点主要是读
③负载均衡,特别是读多写少的情况下,通过多个从节点分担读的的负载,提高redis服务的并发量。
④高可用的基石。哨兵和集群底层都依赖的是主从复制
什么是redis主从复制
(1)主从复制,是将一台redis服务器的数据,复制到其他redis服务器,前者称为主节点(master),后者称为从节点(slave),数据的复制的单向的,只能由主节点到从节点,一个主节点可以有多个从节点,但一个从节点只能有一个主节点。
(2)主从模式下,redis采用读写分离模式
①读操作:主节点与从节点均可执行读操作,主要从节点读为主
②写操作:主节点可以执行写操作,然后把数据同步给各个从节点,保证数据一致性
主从复制的过程
(1)同步阶段:从节点与主节点建立连接后,进行初始化同步,主节点将所有数据发送给从节点。
(2)增量复制阶段:在同步完成后,主节点会将接收到的写入操作发送给从节点,从节点将接收到的写入操作重新执行,保持数据的一致性。
(3)持续复制阶段:从节点持续监听主节点的写入操作,并按照接收顺序执行,以保持与主节点数据的一致性。
数据同步方式
(1)全量复制:主节点将所有数据发送给从节点,从节点将接收到的数据保存在本地,完成全量复制。
(2)部分复制:主节点仅发送写操作给从节点,从节点根据接收到的写操作进行数据更新,即进行增量复制。
手写分布式锁
加synchronized或者Lock
(1)启动两个微服务,端口不同分别为7777和8888

经过jmeter压测会出现超卖的情况
(2)为什么加了synchronized或者Lock还是没有控制住?
①在单机环境下,可以使用synchronized或Lock来实现。 但是在分布式系统中,因为竞争的线程可能不在同一个节点上(同一个jvm中),所以需要一个让所有进程都能访问到的锁来实现(比如redis或者zookeeper来构建)
②不同进程jvm层面的锁就不管用了,那么可以利用第三方的一个组件,来获取锁,未获取到锁,则阻塞当前想要运行的线程
(3)解决方法是:上redis分布式锁setnx,Redis具有极高的性能,且其命令对分布式锁支持友好,借助SET命令即可实现加锁处理。
redis分布式锁
redisTemplate.opsForValue().setIfAbsent 方法底层调用的就是 Redis 的 SETNX 命令
通过递归重试的方式

测试Jmeter压测OK。递归是一种思想没错,但是容易导致StackOverflowError,不太推荐,进一步完善
用while替代if,自旋替代递归重试
(1)防止栈内存溢出(最重要)
①递归:每调用一次函数,JVM 栈就多一帧,深度大必挂。
②循环/自旋:只在堆上改局部变量,不会长栈帧。
(2)避免上下文切换开销(性能高)
在非阻塞锁或原子操作中:
①synchronized(重量级锁):拿不到锁时,线程从运行态变为阻塞态(OS 内核态切换),唤醒后还要切回来。代价极高(微秒级)。
②while 自旋锁(Lightweight Lock):拿不到锁时,原地转圈(跑几个空循环),线程始终是运行态。无内核态切换,纳秒级响应。

但这种方法也存在问题:部署了微服务的Java程序机器挂了,代码层面根本没有走到finally这块,没办法保证解锁(无过期时间该key一直存在),这个key没有被删除,需要加入一个过期时间限定key。
设置key+过期时间分开

问题:加锁和过期时间设置不在同一行,没有保证原子性
设置key+过期时间合并成一行具备原子性

问题:实际业务处理时间如果超过了默认设置key的过期时间??张冠李戴,删除了别人的锁
防止误删key,只能自己删除自己的,不许动别人的

问题:finally块的判断+del删除操作不是原子性的

启用lua脚本编写redis分布式锁判断+删除判断代码
(1)Lua脚本

①Redis调用Lua脚本通过eval命令保证代码执行的原子性,直接用return返回脚本执行后的结果值
②eval luascript numkeys [key [key…]] [arg [arg…]]
(2)代码

(3)问题

①可重入锁又名递归锁, 是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提,锁对象得是同一个对象),不会因为之前已经获取过还没释放而阻塞。
②如果是1个有 synchronized 修饰的递归调用方法,程序第2次进入被自己阻塞了岂不是天大的笑话,出现了作茧自缚。
③所以Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个优点是可一定程度避免死锁。
可重入锁

(1)可重入锁种类
①隐式锁(即synchronized关键字使用的锁):在一个synchronized修饰的方法或代码块的内部调用本类的其他synchronized修饰的方法或代码块时,是永远可以得到锁的
Synchronized的重入的实现机理:
每个锁对象拥有一个锁计数器和一个指向持有该锁的线程的指针。
当执行monitorenter(进去这把锁)时,如果目标锁对象的计数器为零,那么说明它没有被其他线程所持有,Java虚拟机会将该锁对象的持有线程设置为当前线程,并且将其计数器加1。
在目标锁对象的计数器不为零的情况下,如果锁对象的持有线程是当前线程,那么 Java 虚拟机可以将其计数器加1,否则需要等待,直至持有线程释放该锁。
当执行monitorexit时,Java虚拟机则需将锁对象的计数器减1。计数器为零代表锁已被释放。
本文围绕Redis展开,介绍其快速的原因、使用场景、数据类型等。探讨了数据库缓存双写不一致问题及解决办法,如延时双删策略。还阐述了Redis持久化机制、布隆过滤器原理,以及缓存预热、雪崩、穿透、击穿等问题的应对措施,同时介绍了集群实现、哨兵模式和主从复制等内容。

2047

被折叠的 条评论
为什么被折叠?



