Redis
redis
redis的几种数据结构以及使用场景(能用来实现哪些功能)
Redis中一共有5种数据结构:String/hash/list/set/zset
基本数据类型:
1、String:String是最常用的一种数据类型,String类型的值可以是字符串、数字或者二进制,但值最大不能超过512MB。使用场景:常规key-value缓存应用。
2、Hash:Hash 是一个键值对集合。hash 特别适合用于存储对象。
3、Set:Set是一个无序去重的集合。Set 提供了交集、并集等一系列直接操作集合的方法,对于求共同好友、共同关注等功能实现特别方便。
4、List:List是一个有序可重复的集合,底层是依赖双向链表实现的。
5、SortedSet:有序Set。内部维护了一个score的参数来实现。适用于排行榜和带权重的消息队列等场景。
特殊的数据类型:
1、Bitmap:位图,可以认为是一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在Bitmap中叫做偏移量。Bitmap的长度与集合中元素个数无关,
而是与基数的上限有关。
假如要计算上限为1亿的基数,则需要12.5M字节的bitmap。就算集合中只有10个元素也需要12.5M。
2、Hyperloglog。HyperLogLog 是用来做基数统计的算法,其优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
场景:独立访客统计。HyperLogLog 的统计规则是基于概率完成的,所以它给出的统计结果是有一定误差的。
3、Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如定位、附近的人等。
HyperLogLog、Bitmap的底层都是 String 数据类型,Geospatial 的底层是 Sorted Set 数据类型。
String: set key value, get key,
介绍:key/value; 二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或 者序列化的对象。一个键最大能存储512MB。
常用命令:set,get,strlen,exists,decr,incr,setex等等。
应用场景:一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等。
Hash: hset key field value
介绍:hash类似于JDK1.8前的HashMap,内部实现也差不多(数组+链表)。不过,Redis的hash做了更多优化。另外,hash是一个string类型的
field和value的映射表,特别适合用于存储对象,后续操作的时候,可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以hash数据结构来存储用户信息,
商品信息等等。
常用命令:hset,hmset,hexists,hget,hgetall,hkeys,hvals等。
应用场景: 系统中对象数据的存储。
1.以cookieId作为key,设置30分钟为缓存过期时间,能很好的模拟出类似session的效果
2.电商的购物车,以用户id为key,商品id为field,商品数量为value
1)添加商品hset cart:1001 10088 1
2)增加数量hincrby cart:1001 10088 1
3)商品总数hlen cart:1001
4)删除商品hdel cart:1001 10088
5)获取购物车所有商品hgetall cart:1001
缺点是:过期策略不能用在某个field上,只能用在key上
list
介绍:list 即是链表。链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且可以灵活调整链表长度,但是链表的随机访问困难。
许多高级编程语言都内置了链表的实现比如Java中的LinkedList,但是C语言并没有实现链表,所以Redis实现了自己的链表数据结构。
Redis 的 list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
常用命令: rpush,lpop,lpush,rpop,lrange,llen等。
应用场景: 发布与订阅或者说消息队列、慢查询。
1.关注列表
1).微信或者微博的消息流
LPUSH msg:{诸葛老师-ID} 10018
2)备胎说车发微博,消息ID为10086
LPUSH msg:{诸葛老师-ID} 10086
3)查看最新微博消息
LRANGE msg:{诸葛老师-ID} 0 5
LIST可以很好的完成排队,先进先出的原则。
2.队列
Set 用于去重
介绍 :set 类似于 Java 中的HashSet 。Redis 中的set类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,
又不希望出现重复数据时,set是一个很好的选择,并且 set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
可以基于 set轻易实现交集、并集、差集的操作。
比如:你可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。
这个过程也就是求交集的过程。
常用命令:sadd,spop,smembers,sismember,scard,sinterstore,sunion等。
应用场景: 需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景
1.微信抽奖小程序,sadd key {userId},SRANDMEMBER key [count]/pop [count]
2.点赞,用户关注模型(共同关注列表)
3.统计独立IP
Zset
介绍:和 set 相比,sorted set增加了一个权重参数score(double 类型的分数),使得集合中的元素能够按score的大小进行有序排列,还可以通过score
的范围来获取元素的列表。有点像是Java中HashMap和TreeSet的结合体。
常用命令:zadd,zcard,zscore,zrange,zrevrange,zrem等。
应用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息
(可以理解为按消息维度的消息排行榜)等信息。
可以做排行榜应用,取TOP N操作。
1.微博热搜的排行榜
2.带权重的消息队列
Redis 如何做内存优化?
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,
所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的 web 系统中有一个用
户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的 key,而是应该把这个用户的
所有信息存储到一张散列表里面.
Redis 怎么实现消息队列?
使用一个列表,让生产者将任务使用LPUSH命令放进列表,消费者不断用RPOP从列表取出任务。
BRPOP和RPOP命令相似,唯一的区别就是当列表没有元素时BRPOP命令会一直阻塞连接,直到有新元素加入。
优先级队列
如果多个键都有元素,则按照从左到右的顺序取元素。
发布/订阅模式
PSUBSCRIBE channel?* 按照规则订阅。
PUNSUBSCRIBE channel?* 退订通过PSUBSCRIBE命令按照某种规则订阅的频道。其中订阅规则要进行严格的字符串匹配,PUNSUBSCRIBE*无法退订channel?*规则。
缺点:在消费者下线的情况下,生产的消息会丢失。
延时队列
使用sortedset,拿时间戳作为score,消息内容作为key,调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
Redis 除了做缓存,还能做什么?
分布式锁: 通过Redis来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于Redisson来实现分布式锁。
限流:一般是通过 Redis + Lua 脚本的方式来实现限流。相关阅读:《我司用了 6 年的 Redis 分布式限流器,可以说是非常厉害了!》。
消息队列:Redis 自带的 list 数据结构可以作为一个简单的队列使用。0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,
有主题和消费组的概念,支持消息持久化以及 ACK 机制。
复杂业务场景:通过 Redis 以及 Redis 扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景,比如通过bitmap
统计活跃用户、通过 sorted set 维护排行榜。
…
Redis 为什么是单线程的而不采用多线程方案?
这主要是基于一种客观原因来考虑的。因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。
既然单线程容易实现,而且CPU不会成为瓶颈, 那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)
为什么使用 Redis,有什么好处?
1.速度快,因为数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
2.支持丰富数据类型,支持 string,list,set,sorted set,hash
3.支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行
4.丰富的特性:可用于缓存,消息,按 key设置过期时间,过期后将会自动删除
(其他优点:单线程的话就能避免多线程的频繁上下文切换问题每秒可以处理超过 10 万次读写操作,是已知性能最快的 Key-Value DB。
Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此
Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
单个value 的最大限制是 1G)
单线程的Redis为什么这么快?
主要是有三个原因:
- C语言实现,效率高
2、Redis的全部操作都是纯内存的操作;
3、Redis采用单线程,有效避免了频繁的上下文切换;
4,采用了非阻塞I/O多路复用机制。
5丰富的数据结构(全称采用hash结构,读取速度非常快,对数据存储进行了一些优化,比如亚 索表,跳表等)
Redis 为什么设计成单线程的?
多线程处理会涉及到锁,并且多线程处理会涉及到线程切换而消耗CPU。采用单线程,避免了不必要的上下文切换和竞争条件。其次CPU不是Redis的瓶颈,Redis的
瓶颈最有可能是机器内存或者网络带宽
了解Redis的线程模型吗?可以大致说说吗?
redis是一个单线程程序,也就说同一时刻它只能处理一个客户端请求;
redis是通过IO多路复用(select,epoll, kqueue,依据不同的平台,采取不同的实现)来处理多个客户端请求的
打开看过Redis的源码就会发现:Redis内部使用文件事件处理器file event handler,这个文件事件处理器是单线程的,所以Redis才叫做单线程的模型。
它采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器进行处理。
文件事件处理器的结构包含4个部分:
多个socket
IO多路复用程序
文件事件分派器
事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
使用I/O多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答
(accept)、读取(read)、写入(write)、关闭(close)等操作时,与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好
的事件处理器来处理这些事件。多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,
会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
一句话总结就是:“I/O 多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。”
redis主要采用定期删除和惰性删除这两种方式,一定能保证删除数据吗?如果不能,Redis会有什么应对措施?
首先介绍一下定期删除和惰性删除的工作流程:
定期删除:Redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。
为什么要随机呢?你想一想假如Redis存了几十万个key ,每隔100ms就遍历所有的设置过期时间的key的话,就会给CPU带来很大的负载!
惰性删除:定期删除可能会导致很多过期key到了时间并没有被删除掉,所以就有了惰性删除。它是指某个键值过期后,此键值不会马上被删除,
而是等到下次被使用的时候,才会被检查到过期,此时才能得到删除,惰性删除的缺点很明显是浪费内存。 除非你的系统去查一下那个key,才会被Redis给删除掉。
这就是所谓的惰性删除!
并不能保证一定删除,Redis有一个Redis 内存淘汰机制来确保数据一定会被删除。
如果定期删除没删除key,然后你也没及时去请求key,也就是说惰性删除也没生效。这样,Redis 内存会越来越高。那么就应该采用内存淘汰机制。
在Redis.conf中有一行配置:maxmemory-policy该配置就是配内存淘汰策略的,默认配置是noeviction,主要有以下六种方案(Redis4.0前):
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
注意这里的 6 种机制,volatile 和 allkeys 规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,
后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。
使用策略规则:
(1)如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lru
(2)如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用 allkeys-random
redis 过期键的删除策略?
(1)定时删除:在设置键的过期时间的同时,创建一个定时器timer. 让定时器在键的过期时间来临时,立即执行对键的删除操作。
(2)惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
(3)定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。
LRU 是什么?如何实现?(具体实现要清楚,或者手写)
最近最少使用策略 LRU(Least Recently Used)是一种缓存淘汰算法,是一种缓存淘汰机制。
使用双向链表实现的队列,队列的最大容量为缓存的大小。在使用过程中,把最近使用的页面移动到队列头,最近没有使用的页面将被放在队列尾的位置
使用一个哈希表,把页号作为键,把缓存在队列中的节点的地址作为值,只需要把这个页对应的节点移动到队列的前面,如果需要的页面在内存中,
此时需要把这个页面加载到内存中,简单的说,就是将一个新节点添加到队列前面,并在哈希表中更新相应的节点地址,如果队列是满的,
那么就从队尾移除一个节点,并将新节点添加到队列的前面。
假如MySQL有1000万数据,采用Redis作为中间缓存,取其中的10万,如何保证Redis中的数据都是热点数据?
可以使用Redis的数据淘汰策略,Redis 内存数据集大小上升到一定大小的时候,就会施行这种策略。
具体说来,主要有6种内存淘汰策略
缓存更新策略最佳实践就是组合使用:
- 一般来说我们都需要配置超过最大缓存后的更新策略(例如:LRU/LFU/FIFO算法剔除)以及最大内存,这样可以保证系统可以继续运行
(例如redis可能存在OOM问题)(极端情况下除外,数据一致性要求极高)。
- 一般来说我们需要把超时剔除和主动更新组合使用,那样即使主动更新出了问题,也能保证过期时间后,缓存就被清除了(不至于永远都是脏数据)
超时剔除:
就是我们通常做的缓存数据过期时间设置,例如redis和memcache都提供了expire这样的API,来设置K-V的过期时间。
主动更新:
业务对于数据的一致性要求很高,需要在真实数据更新后,立即更新缓存数据。
具体做法:例如可以利用消息系统或者其他方式(比如数据库触发器,或者其他数据源的listener机制来完成)通知缓存更新。
Redis是如何判断数据是否过期的呢?
Redis通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个
long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
过期字典是存储在 redisDb 这个结构里的:
typedef struct redisDb {
…
dict *dict; //数据库键空间,保存着数据库中所有键值对
dict *expires // 过期字典,保存着键的过期时间
…
} redisDb;
缓存穿透,缓存雪崩,缓存击穿,缓存预热,缓存更新,缓存降级的概念以及解决方案
缓存雪崩
缓存雪崩指的是缓存同一时间大面积的失效,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决办法
事前:尽量保证整个 Redis 集群的高可用性,发现机器宕机尽快补上,选择合适的内存淘汰策略。
事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉, 通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程
查询数据和写缓存,其他线程等待。
事后:利用 Redis 持久化机制保存的数据尽快恢复缓存
解决方法:在原有的失效时间基础上增加一个随机值,使得过期时间分散一些。
缓存穿透
缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中,将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。
造成缓存穿透的基本原因有两个:
第一, 自身业务代码或者数据出现问题。
第二, 一些恶意攻击、 爬虫等造成大量空命中。
解决办法
1、布隆过滤器
对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;
布隆过滤器的应用之大数据去重:
5TB的硬盘上放满了数据,请写一个算法将这些数据进行排重。如果这些数据是一些32bit大小的数据该如何解决?如果是64bit的呢?
通过布隆过滤器,在添加元素之前首先计算元素的hash,然后查询blommer,如果能够匹配则说明可能重复不予添加,否则添加进去,但是这种情况需要考虑
到容错率,因为可能存在哈西碰撞,需要问清楚到低容错率是多少。缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,如果从DB查不到
数据则不写入缓存,这将导致这个不存在的数据每次请求都要到DB去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了。
缓存空值,不会查数据库
采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,查询不存在的数据会被这个bitmap拦截掉,从而避免了对DB的查询压力。
布隆可以看成数据库的缩略版,用来判定是否存在值。启动的时候过滤器是要全表扫描的,数据库数据发生变化的时候会更新布隆过滤器。
布隆过滤器的原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。查询时,将元素通过散列函数映射之后
会得到k个点,如果这些点有任何一个0,则被检元素一定不在,直接返回;如果都是1,则查询元素很可能存在,就会去查询redis和数据库。
2、缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;如果一个查询返回
的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
但是这种方法会存在两个问题:
1、如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
2、即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。
缓存击穿
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。
解决方案:
1.设置热点数据永久缓存
2.设置互斥锁,单线程重建缓存
比如常见的电商项目中,某些货物成为“爆款”了,可以对一些主打商品的缓存直接设置为永不过期。即便某些商品自己发酵成了爆款,也是直接设为永不过期就好了。
mutex key互斥锁基本上是用不上的,有个词叫做大道至简。
缓存预热
缓存预热是指系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户会直接查询
事先被预热的缓存数据!
解决思路
1、直接写个缓存刷新页面,上线时手工操作下;
2、数据量不大,可以在项目启动的时候自动进行加载;
3、定时刷新缓存;
缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,
常见的策略有两种: 定时删除和惰性删除
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,
大家可以根据自己的应用场景来权衡。
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据
一些关键数据进行自动降级,也可以配置开关实现人工降级。降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的 最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见
的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
redis持久化方式(RDB和AOF)
持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。
Redis支持两种方式的持久化,一种是RDB的方式,一种是AOF的方式。前者会根据指定的规则定时将内存中的数据存储在硬盘上,而后者在每次执行完命令后将
命令记录下来。一般将两者结合使用。
RDB方式
RDB 是Redis默认的持久化方案。RDB持久化时会将内存中的数据写入到磁盘中,在指定目录下生成一个dump.rdb文件。Redis重启会加载dump.rdb文件恢复数据。
RDB持久化的过程(执行SAVE命令除外):
创建一个子进程;
父进程继续接收并处理客户端的请求,而子进程开始将内存中的数据写进硬盘的临时文件;
当子进程写完所有数据后会用该临时文件替换旧的RDB文件。
Redis启动时会读取RDB快照文件,将数据从硬盘载入内存。通过RDB方式的持久化,一旦Redis异常退出,就会丢失最近一次持久化以后更改的数据。
触发RDB快照:
手动触发:
用户执行SAVE或BGSAVE命令。SAVE命令执行快照的过程会阻塞所有来自客户端的请求,应避免在生产环境使用这个命令。BGSAVE命令可以在后台异步进行快照
操作,快照的同时服务器还可以继续响应客户端的请求,因此需要手动执行快照时推荐使用BGSAVE命令;
被动触发:
根据配置规则进行自动快照,如SAVE 300 10,300秒内至少有10个键被修改则进行快照。
如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点。
默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则自动执行bgsave。
执行debug reload命令重新加载Redis时,也会自动触发save操作。
优点:Redis加载RDB恢复数据远远快于AOF的方式。
缺点:
RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本过高。
存在老版本Redis服务和新版本RDB格式兼容性问题。RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式的RDB版本,存在老版本Redis服务无法
兼容新版RDB格式的问题。
AOF方式
AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了
数据持久化的实时性,目前已经是Redis持久化的主流方式。
默认情况下Redis没有开启AOF方式的持久化,可以通过appendonly参数启用appendonly yes。开启AOF方式持久化后每执行一条写命令,Redis就会将该命令
写进aof_buf缓冲区,AOF缓冲区根据对应的策略向硬盘做同步操作。默认情况下系统每30秒会执行一次同步操作。为了防止缓冲区数据丢失,可以在Redis写入AOF
文件后主动要求系统将缓冲区数据同步到硬盘上。可以通过appendfsync参数设置同步的时机。
重写机制:随着命令不断写入AOF,文件会越来越大,为了解决这个问题,Redis引入AOF重写机制压缩文件体积。AOF文件重写是把Redis进程内的数据转化为写
命令同步到新AOF文件的过程。
优点:
AOF可以更好的保护数据不丢失,一般AOF会每秒去执行一次fsync操作,如果redis进程挂掉,最多丢失1秒的数据。
AOF以appen-only的模式写入,所以没有任何磁盘寻址的开销,写入性能非常高。
缺点:
对于同一份文件AOF文件比RDB数据快照要大。
不适合写多读少场景。
数据恢复比较慢。
持久化有两种,那应该怎么选择呢?
-
不要仅仅使用 RDB ,因为那样会导致你丢失很多数据。
-
也不要仅仅使用 AOF ,因为那样有两个问题,第一,你通过 AOF 做冷备没有 RDB 做冷备的恢
复速度更快; 第二, RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备
份和恢复机制的 bug 。
- Redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,
用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF
文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
- 如果同时使用 RDB 和 AOF 两种持久化机制,那么在 Redis 重启的时候,会使用 AOF 来重新
构建数据,因为 AOF 中的数据更加完整。
如何保证 Redis 中的数据不丢失?
单机单节点模式
使用 AOF 和 RDB 结合的方式, RDB 做镜像全量持久化,AOF 做增量持久化。
因为 RDB 会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要 AOF 来配合使用。
Redis 集群模式
master 节点持久化
如果采用了主从架构,那么建议必须开启master node的持久化!不建议用slave node作为master node的数据热备,因为那样的话,
如果你关掉master的持久化,可能在master宕机重启的时候数据是空的,然后可能一经过复制,salve node数据也丢了,
master就会将空的数据集同步到slave上去,所有slav 的数据全部清空。
Redis 断点续传
从redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
主备切换的过程,可能会导致数据丢失
解决异步复制和脑裂导致的数据丢失
redis.conf 中
min-slaves-to-write 1
min-slaves-max-lag 10
要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒
如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master就不会再接收任何请求了
上面两个配置可以减少异步复制和脑裂导致的数据丢失。
redis高可用架构 三种架构(主从复制,哨兵模式,cluster集群,怎么选举,watchdog)https://mp.weixin.qq.com/s/3B3A83Kmlt0nuzN-Tvyzlg
集群的原理是什么?
Redis Sentinel(哨兵)着眼于高可用,Sentinel(哨兵)可以监听集群中的服务器,并在master主服务器宕机时 自动从slave服务器中选举出新的服务器
成为master服务器,继续提供服务。
Redis Cluster(集群)着眼于扩展性,在单个Redis内存不足时,使用Cluster进行分片存储。
哨兵模式
主从模式下,主服务器宕机之后就没有办法进行写操作了,必须手动重新配置,非常麻烦。
哨兵模式可以通过监控redis主从,当主宕机后选举出新的主从而保障服务。
sentinel哨兵是特殊的redis服务,不提供读写服务,主要用来监控redis实例节点。 哨兵架构下client端第一次从哨兵找出redis的主节点,后续就直接访问
redis的主节点,不会每次都通过sentinel代理访问redis的主节点,当redis的主节点发生变化,哨兵会第一时间感知到,并且将新的redis主节点通知给
client端(这里面redis的client端一般都实现了订阅功能,订阅sentinel发布的节点变动消息)
哨兵leader的选举流程:
当一个master服务器被某sentinel视为主观下线状态,当超过半数的sentinel发现master下线时,认为是客观下线,就会选举出一个leader进行故障转移,
该sentinel会与其他sentinel协商选出sentinel的leader进行故障转移工作。每个发现master服务器进入客观下线的sentinel都可以要求其他sentinel
选自己为sentinel的leader,选举是先到先得。同时每个sentinel每次选举都会自增配置纪元(选举周期),每个纪元中只会选择一个sentinel作为leader。
如果所有超过一半的sentinel选举某sentinel作为leader。之后该sentinel进行故障转移操作,从存活的slave中选举出新的master,这个选举过程跟集群
的master选举很类似。哨兵集群只有一个哨兵节点,redis的主从也能正常运行以及选举master,如果master挂了,那唯一的那个哨兵节点就是哨兵leader了,
可以正常选举新master。不过为了高可用一般都推荐至少部署三个哨兵节点。为什么推荐奇数个哨兵节点原理跟集群奇数个master节点类似
Redis集群
首先启动redis,此时redis都是游离状态,使用‐‐cluster create ‐‐cluster‐replicas 节点数量创建集群,使用cluster nodes 可以验证集群信息
Cluster 默认会对 key 值使用 crc16 算法进行 hash 得到一个整数值,然后用这个整数值对 16384 进行取模来得到具体槽位。HASH_SLOT = CRC16(key) mod 16384 ,
当客户端向一个错误的节点发出了指令,该节点会发现指令的 key 所在的槽位并不归自己管理,这时它会向客户端发送一个特殊的跳转指令携带目标操作的节点地址,
告诉客户端去连这个节点去获取数据。redis cluster节点间采取gossip协议进行通信维护集群的元数据有两种方式:集中式和gossip,这两种方式的区别参看文档
集群的选举流程:
当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave,从而存在多个slave竞争
成为master节点的过程, 其过程如下:
1.slave发现自己的master变为FAIL
2.将自己记录的集群currentEpoch加1,并广播FAILOVER_AUTH_REQUEST 信息
3.其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack
4.尝试failover的slave收集master返回的FAILOVER_AUTH_ACK
5.slave收到超过半数master的ack后变成新Master(这里解释了集群为什么至少需要三个主节点,如果只有两个,当其中一个挂了,只剩一个主节点是不能选举成功的)
6.广播Pong消息通知其他集群节点。
Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?
因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。
奇数个master节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都
能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。
是否使用过 Redis Cluster 集群,集群的原理是什么?
使用过 Redis 集群,它的原理是:
所有的节点相互连接,集群消息通信通过集群总线通信,集群总线端口大小为客户端服务端口 + 10000(固定值)
节点与节点之间通过二进制协议进行通信,客户端和集群节点之间通信和通常一样,通过文本协议进行
集群节点不会代理查询
数据按照 Slot 存储分布在多个 Redis 实例上
集群节点挂掉会自动故障转移
可以相对平滑扩/缩容节点
Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,redis 先对
key 使用 crc16 算法算出一个结果,然后把结果对 16384 求余数,这样每个 key 都会对应一个编号
在 0~16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。
Redis Cluster 集群方案什么情况下会导致整个集群不可用?
Redis 没有使用哈希一致性算法,而是使用哈希槽。Redis 中的哈希槽一共有 16384 个,计算给定
密钥的哈希槽,我们只需要对密钥的 CRC16 取摸 16384。假设集群中有 A、B、C 三个集群节点,
不存在复制模式下,每个集群的节点包含的哈希槽如下:
节点 A 包含从 0 到 5500 的哈希槽;
节点 B 包含从 5501 到 11000 的哈希槽;
节点 C 包含从 11001 到 16383 的哈希槽;
这时,如果节点 B 出现故障,整个集群就会出现缺少 5501 到 11000 的哈希槽范围而不可用。
Redis 集群架构模式有哪几种?
Redis 集群架构是支持单节点单机模式的,也支持一主多从的主从结构,还支持带有哨兵的集群部署模式。
说说 Redis 哈希槽的概念?
Redis 集群并没有使用一致性hash,而是引入了哈希槽的概念。Redis集群有16384(2^14)个哈
希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。
Redis常见使用方式有哪些?
Redis的几种常见使用方式包括:
单机版
Redis主从
Redis Sentinel(哨兵)
Redis Cluster
使用场景:
单机版:很少使用。存在的问题:1、内存容量有限 2、处理能力有限 3、无法高可用。
主从模式:master节点挂掉后,需要手动指定新的 master,可用性不高,基本不用。
哨兵模式:master节点挂掉后,哨兵进程会主动选举新的master,可用性高,但是每个节点存储的数据是一样的,浪费内存空间。数据量不是很多,集群规模
不是很大,需要自动容错容灾的时候使用。
Redis cluster:主要是针对海量数据+高并发+高可用的场景,如果是海量数据,如果你的数据量很大,那么建议就用Redis cluster,所有主节点的容量
总和就是Redis cluster可缓存的数据容量。
Redis 主从同步是怎么实现的?
全量同步
master 服务器会开启一个后台进程用于将redis中的数据生成一个rdb文件,与此同时,服务器会缓存所有接收到的来自客户端的写命令(包含增、删、改),
当后台保存进程处理完毕后,会将该rdb文件传递给slave服务器,而slave服务器会将rdb文件保存在磁盘并通过读取该文件将数据加载到内存,在此之后master
服务器会将在此期间缓存的命令通过redis传输协议发送给slave服务器,然后slave服务器将这些命令依次作用于自己本地的数据集上最终达到数据的一致性。
增量同步
从redis 2.8版本以前,并不支持部分同步,当主从服务器之间的连接断掉之后,master服务器和slave服务器之间都是进行全量数据同步。从redis 2.8开始,
即使主从连接中途断掉,也不需要进行全量同步,因为从这个版本开始融入了部分同步的概念。部分同步的实现依赖于在master服务器内存中给每个slave服务器维
护了一份同步日志和同步标识,每个slave服务器在跟master服务器进行同步时都会携带自己的同步标识和上次同步的最后位置。当主从连接断掉之后slave服务器
隔断时间(默认1s)主动尝试和master服务器进行连接,如果从服务器携带的偏移量标识还在master服务器上的同步备份日志中,那么就从slave发送的偏移量
开始继续上次的同步操作,如果slave发送的偏移量已经不在master的同步备份日志中(可能由于主从之间断掉的时间比较长或者在断掉的短暂时间内master
服务器接收到大量的写操作),则必须进行一次全量更新。在部分同步过程中,master会将本地记录的同步备份日志中记录的指令依次发送给slave服务器从而
达到数据一致。
Redis 主从同步策略
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave在任何时候都可以发起全量同步。
redis策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
Redis 的同步机制了解么?
Redis 可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续
修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成
后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节
点进行重放就完成了同步过程
Redis分布式锁
Synchronized是JVM级别的锁,这里的锁是用于同一进程里面,因为多个线程共同访问某个共享资源,而进行的同步措施,他的前提条件是同一进程内,内存共享;
如果在分布式环境下,是没有办法保证封锁的,此时需要使用分布式锁
版本1:使用redis完成分布式锁
Redis为单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,Redis中可以使用SETNX命令实现分布式锁。
将key的值设为value,如果成功则进行下一步的操作,失败则返回。
为了防止代码执行中可能存在的无法释放锁的情况,需要在finally中使用del key命令释放锁,
为了防止死锁,需要给锁设置一个最大有效时间,如果超过,则Redis来帮我们释放锁。
Redis分布式锁实现原理:
set px nx
守护线程,进行 renew
Redis分布式锁实现:先拿setnx 来争抢锁,抢到之后,再用expire(过期)给锁加一个过期时间防止锁忘记了释放。
如果在setnx之后执行expire,之前进程意外crash或者要重启维护了,那会怎么样:
set指令有非常复杂的参数,这个应该是可以同时把setnx和expire合成一条指令来用的!
版本2:使用Redission完成分布式锁
如果有一些代码执行的之间已经超过了这个锁的有效时间,那么redis就会帮我们释放锁,但是这个时候我们并不希望锁被释放,因为该线程执行的操作还没有结束。
可以使用redission对锁进行续命,redission加锁之后会生成另一个线程,这个线程叫watchdog,watchdog默认每间隔10s检查redis主线程是否执行完毕,
如果没有执行完毕则将锁过期时间重置。最后在finally中的获取锁和释放锁是如何保证原子性的?
Redission的原子性依靠lua脚本来实现
如何解决Redis的并发竞争Key问题
所谓Redis的并发竞争Key的问题也就是多个系统同时对一个key进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
但是顺序我们无法控制。推荐一种方案:分布式锁(zookeeper和Redis都可以实现分布式锁)。
(如果不存在Redis的并发竞争Key问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,
生成一个唯一的瞬时有序节点。判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。当释放锁的时候,只需将这个瞬时节点删除即可。
同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的 子节点释放锁。
(使用分布式锁,例如zk,同时加入数据的时间戳,同一时刻,只有抢到锁的客户端才能写入,同时,比较当前数据的时间戳和缓存中数据的时间戳)
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
ZK 和 Redis 的区别,各自有什么优缺点?
先说 Redis:
Redis 只保证最终一致性,副本间的数据复制是异步进行(Set 是写,Get 是读,Reids
集群一般是读写分离架构,存在主从同步延迟情况),主从切换之后可能有部分数据没有
复制过去可能会丢失锁情况,故强一致性要求的业务不推荐使用 Reids,推荐使用zk。
Redis 集群各方法的响应时间均为最低。随着并发量和业务数量的提升其响应时间会有明
显上升(公有集群影响因素偏大),但是极限 qps 可以达到最大且基本无异常
再说 ZK:
使用 ZooKeeper 集群,锁原理是使用 ZooKeeper 的临时节点,临时节点的生命周期在
Client 与集群的 Session 结束时结束。因此如果某个 Client 节点存在网络问题,与
ZooKeeper 集群断开连接,Session 超时同样会导致锁被错误的释放(导致被其他线程
错误地持有),因此 ZooKeeper 也无法保证完全一致。
ZK 具有较好的稳定性;响应时间抖动很小,没有出现异常。但是随着并发量和业务数量
的提升其响应时间和 qps 会明显下降
高并发分布式锁(了解)
如果希望redis的性能尽可能的高,且还需要实现分布式锁,怎么操作?
可以参考ConcurrentHashMap的分段锁将原数据进行拆分,1000拆分成十个,0-100,101-200…然后每一段的数据都放到一个redis主从中去,如果收到请求,
通过hash将请求平均分配给集群中的主从就能够实现数据的分散。
MySQL与Redis 如何保证双写一致性(程序员田螺)
总结:
1.缓存延时双删
2.删除缓存重试机制
3.读取biglog异步删除缓存
(延迟双删:采用先更新数据库,再删除缓存的方式,实现延迟双删,对第二次删除不成功的情况使用mq或者本地记录删除失败数据定时任务执行,
构建重试机制,重试删除三次不成功,记录到数据库,发短信给人工处理)
如何保证数据库和缓存双写一致性? (苏三说技术)https://juejin.cn/post/7081138393551208455
数据为什么会出现不一致的情况? 这样的问题主要是在并发读写访问的时候,缓存和数据相互交叉执行。
- 常见方案
通常情况下,我们使用缓存的主要目的是为了提升查询的性能。 大多数情况下,我们是这样使用缓存的:
用户请求过来之后,先查缓存有没有数据,如果有则直接返回。
如果缓存没数据,再继续查数据库。
如果数据库有数据,则将查询出来的数据,放入缓存中,然后返回该数据。
如果数据库也没数据,则直接返回空。
这是缓存非常常见的用法。一眼看上去,好像没有啥问题。
但你忽略了一个非常重要的细节:如果数据库中的某条数据,放入缓存之后,又立马被更新了,那么该如何更新缓存呢?
不更新缓存行不行?
答:当然不行,如果不更新缓存,在很长的一段时间内(决定于缓存的过期时间),用户请求从缓存中获取到的都可能是旧值,而非数据库的最新值。
这不是有数据不一致的问题?那么,我们该如何更新缓存呢?
目前有以下4种方案:
先写缓存,再写数据库
先写数据库,再写缓存
先删缓存,再写数据库
先写数据库,再删缓存
接下来,我们详细说说这4种方案。
经过分析我们知道如果直接更新缓存的问题很多。
那么,为何我们不能换一种思路:不去直接更新缓存,而改为删除缓存呢?
为什么是删除缓存而不是更新缓存呢?
线程安全问题。同时有请求A和请求B进行更新操作,那么会出现
(1)线程A更新了缓存
(2)线程B更新了缓存
(3)线程B更新了数据库
(4)线程A更新了数据库,由于网络等原因,请求B先更新数据库,这就导致缓存和数据库不一致的问题。
如果业务需求写数据库场景比较多,而读数据场景比较少,采用这种方案就会导致,数据压根还没读到,缓存就被频繁的更新,浪费性能。
如果你写入数据库的值,并不是直接写入缓存的,而是要经过一系列复杂的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是浪费性能的。
删除缓存方案,同样有两种:
先删缓存,再写数据库
先写数据库,再删缓存
我们一起先看看:先删缓存,再写数据库的情况。
先删除缓存,再更新DB,同样也有问题。假如A先删除了缓存,但还没更新DB,这时B过来请求数据,发现缓存没有,去请求DB拿到旧数据,然后再写到缓存,
等A更新完了DB之后就会出现缓存和DB数据不一致的情况了,导致缓存中的数据一直是老数据。
解决方法1:采用延时双删策略。
更新完数据库之后,延时一段时间,再次删除缓存,确保可以删除读请求造成的缓存脏数据。评估项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在
读数据业务逻辑的耗时基础上,加几百ms即可。可以将第二次删除作为异步的。自己起一个线程,异步删除。这样,写的请求就不用sleep一段时间了,增大吞吐量。
(还有一种可能,如果执行更新数据库,准备执行删除缓存时,服务挂了,执行删除失败怎么办?这就坑了!!不过可以通过订阅数据库的biglog来删除。)
解决方法2:使用队列。
当这个key不存在时,将其放入队列,串行执行,必须等到更新数据库完毕才能读取数据。
推荐使用先写数据库,再删缓存的方案,虽说不能100%避免数据不一致问题,但出现该问题的概率,相对于其他方案来说是最小的。
其实先写数据库,再删缓存的方案,跟缓存双删的方案一样,有一个共同的风险点,即:如果缓存删除失败了,也会导致缓存和数据库的数据不一致。
那么,删除缓存失败怎么办呢?
答:需要加重试机制。
在接口中如果更新了数据库成功了,但更新缓存失败了,可以立刻重试3次。如果其中有任何一次成功,则直接返回成功。如果3次都失败了,则写入数据库,
准备后续再处理。当然,如果你在接口中直接同步重试,该接口并发量比较高的时候,可能有点影响接口性能。
这时,就需要改成异步重试了。
异步重试方式有很多种,比如:
1.每次都单独起一个线程,该线程专门做重试的工作。但如果在高并发的场景下,可能会创建太多的线程,导致系统OOM问题,不太建议使用。
2.将重试的任务交给线程池处理,但如果服务器重启,部分数据可能会丢失。
3.将重试数据写表,然后使用elastic-job等定时任务进行重试。
4.将重试的请求写入mq等消息中间件中,在mq的consumer中处理。
5.订阅mysql的binlog,在订阅者中,如果发现了更新数据请求,则删除相应的缓存
20、数据为什么会出现不一致的情况?
这样的问题主要是在并发读写访问的时候,缓存和数据相互交叉执行。
一、单库情况下
同一时刻发生了并发读写请求,例如为A(写) B (读)2个请求
A请求发送一个写操作到服务端,第一步会淘汰cache,然后因为各种原因卡主了,不再执行后面业务 (例:大量的业务操作、调用其他服务处理消耗了1s)。
B请求发送一个读操作,读cache,因为cache淘汰,所以为空
B请求继续读DB,读出一个脏数据,并写入cache
A请求终于执行完全,再写入数据到DB
总结:因最后才把写操作数据写入DB,并没同步。cache里面一直保持脏数据
二、主从同步,读写分离的情况下,读从库而产生脏数据
A请求发送一个写操作到服务端,第一步会淘汰cache
A请求写主数据库,写了最新的数据。
B请求发送一个读操作,读cache,因为cache淘汰,所以为空
B请求继续读DB,读的是从库,此时主从同步还没同步成功。读出脏数据,然后脏数据入cache
最后数据库主从同步完成
总结:这种情况下请求A和请求B操作时序没问题,是主从同步的时延问题(假设1s),导致读请求读取从库读到脏数据导致的数据不一致
根本原因:
单库情况下,逻辑处理中消耗1s,可能读到旧数据入缓存
主从同步,读写分离的情况下,在1s的主从同步时延中,到从库的旧数据入缓存
常见的数据优化方案你了解吗?
一、缓存双淘汰法(缓存双删)
先淘汰缓存
再写数据库
往消息总线esb发送一个淘汰消息,发送立即返回。写请求的处理时间几乎没有增加,这个方法淘汰了缓存两次。因此被称为“缓存双淘汰法“,而在消息总线下游,
有一个异步淘汰缓存的消费者,在拿到淘汰消息在1s后淘汰缓存,这样,即使在一秒内有脏数据入缓存,也能够被淘汰掉。
二、异步淘汰缓存(异步删除)
上述的步骤,都是在业务线里面执行,新增一个线下的读取binlog异步淘汰缓存模块,读取binlog总的数据,然后进行异步淘汰。
这里简单提供一个思路
1.思路:
MySQL binlog增量发布订阅消费+消息队列+增量数据更新到Redis
1)读请求走Redis:热数据基本都在Redis
2)写请求走MySQL: 增删改都操作MySQL
3)更新Redis数据:MySQL的数据操作binlog,来更新到Redis
2.Redis更新
数据操作主要分为两块:
一个是全量(将全部数据一次写入到Redis)
一个是增量(实时更新)
这里说的是增量,指的是mysql的update、insert、delate变更数据。这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息
推送至Redis,Redis再根据binlog中的记录,对Redis进行更新,就无需在从业务线去操作缓存内容。
redis的zset为什么用跳表而不用红黑树?
1、跳表的实现更加简单,不用旋转节点,相对效率更高
2、跳表在范围查询的时候的效率是高于红黑树的,因为跳表是从上层往下层查找的,上层的区域范围更广,可以快速定位到查询的范围
3、平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表只需要维护相邻节点即可
4、查找单个key,跳表和平衡树时间复杂度都是O(logN)
个人思考
从redis本身出发,redis是一个单线程的服务,它更加追求查询速度,redis本身是基于内存的,所以它的性能瓶颈在于内存和网络带宽,而不在于CPU。
红黑树本身的实现比较复杂,每次新增、修改都要维护节点的旋转、变色,相对而言更加消耗内存,而跳表修改元素只需要维护前后两个节点即可。
所以在保障查询速度的前提,内存消耗更小的更适合redis
说说你对Redis事务的理解
什么是 Redis 事务?原理是什么?
Redis中的事务是一组命令的集合,是Redis的最小执行单位。它可以保证一次执行多个命令,每个事务是一个单独的隔离操作,事务中的所有命令都会序列化、
按顺序地执行。服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。它的原理是先将属于一个事务的命令发送给Redis,然后依次执行这些命令。
需要注意的点有:
Redis 事务是不支持回滚的,不像MySQL的事务一样,要么都执行要么都不执行;
Redis 服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。直到事务命令全部执行完毕才会执行其他客户端的命令。
Redis 事务为什么不支持回滚?
Redis 的事务不支持回滚,但是执行的命令有语法错误,Redis会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,
则依然会继续执行余下的命令。这样做的原因是因为回滚需要增加很多工作,而不支持回滚则可以保持简单、快速的特性。
Redis 事务相关的命令有哪几个? MULTI、EXEC、DISCARD、WATCH
Redis key 的过期时间和永久有效分别怎么设置? EXPIRE 和 PERSIST 命令
redis事务(单线程的多路IO复用模型)
为什么Redis的操作是原子性的,怎么保证原子性的?
对于Redis而言,命令的原子性指的是:一个操作不可以再分,操作要么执行,要么不执行。
Redis的操作之所以是原子性的,是因为Redis是单线程的。
Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
多个命令在并发中也是原子性的吗?
不一定, 但是我们可以将get和set改成单命令操作,例如incr 。或者使用Redis+Lua的方式实现.
redis的事务支持不是很好,报错也没有回滚,官方建议使用lua,因为lua更简单,而且性能还更好,建议使用lua保证原子性。
使用lua的好处:
1、减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。这点跟管道类似。
2、原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
3、替代redis的事务功能:redis自带的事务功能很鸡肋,报错不支持回滚,而redis的lua脚本几乎实现了常规的事务功能,支持报错回滚操作
redis扩容缩容,插槽slot,一致性hash算法
一致性哈希算法
一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织 圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,
0点的左侧是2的32次方-1,把这个圆环叫做Hash环然后把服务器ip或者主机名字作为关键字Hash,每个服务器都能确定位置,把数据进行相同的Hash算出的位置,
顺时针访问的第一个就是对应的服务器一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。
如果出现数据倾斜,使用虚拟节点的方式解决
Redis内存用完会怎么样
1.加内存
2.回收
这个跟 Redis 的内存回收策略有关。
Redis 的默认回收策略是 noenviction,当内存用完之后,写数据会报错。
Redis 的其他内存回收策略含义:
volatile-lru:从设置了过期时间的数据集中,淘汰最近最少使用的数据
volatile-ttl:从设置了过期时间的数据中,淘汰最早会过期的数据
volatile-random:从已设置过期时间的数据集中,随机淘汰数据
allkeys-lru:从数据集中,淘汰最近最少使用的数据
allkeys-random:从数据集中,随机淘汰数据
淘汰多少可以通过maxmeory-samples进行设置
3.使用集群
最终一致性问题,保证数据不丢失的方案?
redis的key的设置规范
redis的大key处理(比较占内存)
假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如何将它们全部找出来?
使用keys指令可以扫出指定模式的key列表。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会 导致线程阻塞一
段时间,线上服务会停顿,直到指令执行完毕,服务才能恢 复。这个时候可以使用scan指
令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户
端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长
什么是 bigkey?会存在什么影响?
bigkey是指键值占用内存空间非常大的key。例如一个字符串a存储了200M的数据。
bigkey的主要影响有:
网络阻塞;获取 bigkey时,传输的数据量比较大,会增加带宽的压力。
超时阻塞;因为 bigkey占用的空间比较大,所以操作起来效率会比较低,导致出现阻塞的可能性增加。
导致内存空间不平衡;一个bigkey存储数据量比较大,同一个key在同一个节点或服务器中存储,会造成一定影响
什么是热Key问题,如何解决热key问题
什么是热Key呢?在Redis中,我们把访问频率高的key,称为热点key。
如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
而热点Key是怎么产生的呢?主要原因有两个:
1.用户消费的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景
2.用户请求分片集中,超过单Redis服务器的性能,比如固定名称key,Hash落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点Key问题。
那么在日常开发中,如何识别到热点key呢?
1.凭经验判断哪些是热Key;
2.客户端统计上报;
3.服务代理层上报;
如何解决热key问题?
1.Redis集群扩容:增加分片副本,均衡读流量;
2.将热key分散到不同的服务器中;
3.使用二级缓存,即JVM本地缓存,减少Redis的读请求;
使用过 Redis 做异步队列么,你是怎么用的?
一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,
要适当 sleep 一会再重试。如果对方追问可不可以不用 sleep 呢?list 还有个指令叫
blpop,在没有消息的时候,它会阻塞住直到消息到来。如果对方追问能不能生产一次消费
多次呢?使用 pub/sub 主题订阅者模式,可以实现 1:N 的消息队列。
如果对方追问 pub/sub 有什么缺点?
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 RabbitMQ 等。
如果对方追问 redis 如何实现延时队列?
我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但
是你很克制,然后神态自若的回答道:使用 sortedset,拿时间戳作为score,消息内容作为
key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。
到这里,面试官暗地里已经对你竖起了大拇指。但是他不知道的是此刻你却竖起了中指,在椅子背后。
怎么提高缓存命中率?
主要常用的手段有:
提前加载数据到缓存中;
增加缓存的存储空间,提高缓存的数据;
调整缓存的存储数据类型;
提升缓存的更新频率。
Redis 如何解决 key 冲突?
Redis 如果 key 相同,后一个 key 会覆盖前一个 key。如果要解决 key 冲突,最好给 key 取好名区
分开,可以按业务名和参数区分开取名,避免重复 key 导致的冲突。
Redis 报内存不足怎么处理?
Redis 内存不足可以这样处理:
1.修改配置文件 redis.conf 的 maxmemory 参数,增加 Redis 可用内存;
2.设置缓存淘汰策略,提高内存的使用效率;
3.使用 Redis 集群模式,提高存储量。
Redis 常见性能问题和解决方案?
(1) Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件
(2) 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
(3) 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内
(4) 尽量避免在压力很大的主库上增加从库
(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即: Master <- Slave1 <- Slave2 <-Slave3…
缓存和数据库谁先更新呢?
解决方案
1.写请求过来,将写请求缓存到缓存队列中,并且开始执行写请求的具体操作(删除缓存中的数
据,更新数据库,更新缓存)。
- 如果在更新数据库过程中,又来了个读请求,将读请求再次存入到缓存队列(可以搞n个队
列,采用key的hash值进行队列个数取模hash%n,落到对应的队列中,队列需要保证顺序性)
中,顺序性保证等待队列前的写请求执行完成,才会执行读请求之前的写请求删除缓存失败,
直接返回,此时数据库中的数据是旧值,并且与缓存中的数据是一致的,不会出现缓存一致性
的问题。
- 写请求删除缓存成功,则更新数据库,如果更新数据库失败,则直接返回,写请求结束,此时
数据库中的值依旧是旧值,读请求过来后,发现缓存中没有数据, 则会直接向数据库中请求,
同时将数据写入到缓存中,此时也不会出现数据一致性的问题。
- 更新数据成功之后,再更新缓存,如果此时更新缓存失败,则缓存中没有数据,数据库中是新
值 ,写请求结束,此时读请求还是一样,发现缓存中没有数据,同样会从数据库中读取数据,
并且存入到缓存中,其实这里不管更新缓存成功还是失败, 都不会出现数据一致性的问题。
上面这方案解决了数据不一致的问题,主要是使用了串行化,每次操作进来必须按照顺序进行。如
果某个队列元素积压太多,可以针对读请求进行过滤,提示用户刷新页面,重新请求。
潜在的问题,留给大家自己去想吧,因为这个问题属于发散性。
1,请求时间过长,大量的写请求堆压在队列中,一个读请求来得等都写完了才可以获取到数据。
2,读请求并发高
3,热点数据路由问题,导致请求倾斜。
主从复制,读写分离情况下,读主库的数据的方式:
写操作时根据库+表+业务特征生成一个key放到Cache里并设置超时时间(大于等于主从数据同步时间)。
读请求时,同样的方式生成key先去查Cache,再判断是否命中。
若命中,则读主库,否则读从库。代价是多了一次缓存读写,基本可以忽略。