1. Redis
1.1.1. Redis的过期失效机制?
scan
扫描+给每个key存储过期时间戳
叶落山城秋: Redis过期键删除使用惰性删除(消极删除)和定时删除(积极删除)两种策略来删除过期键,
惰性删除就是一旦访问到过期键(所有的读写数据库的命令,set,lrange,sadd,hget,keys 等等),expireIfNeeded函数先判断键是否过期,如果过期就删除,没有过期就正常返回!
定时删除就是activeExpireCycle 函数被调用,规定时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键
2019-12-18补充几个(因为我确实被问到了,如果内存满了..redis怎么清理的..而之前我就记得上面的定时删除和扫描时间戳删除...)
惰性清除。在访问key时,如果发现key已经过期,那么会将key删除。
定时清理。Redis配置项hz定义了serverCron任务的执行周期,默认每次清理时间为25ms,每次清理会依次遍历所有DB,从db随机取出20个key,如果过期就删除,如果其中有5个key过期,那么就继续对这个db进行清理,否则开始清理下一个db。
当执行写入命令时,如果发现内存不够,那么就会按照配置的淘汰策略清理内存,淘汰策略主要由以下几种
noeviction,不删除,达到内存限制时,执行写入命令时直接返回错误信息。
allkeys-lru,在所有key中,优先删除最少使用的key。(适合请求符合幂定律分布,也就是一些键访问频繁,一部分键访问较少)
allkeys-random,在所有key中,随机删除一部分key。(适合请求分布比较平均)
volatile-lru,在设置了expire的key中,优先删除最少使用的key。
volatile-random,在设置了expire的key中,随机删除一部分key。
volatile-ttl,在设置了expire的key中,优先删除剩余时间段的key。
4.0版本后增加以下两种:
volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰。
allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key。
LRU算法
LRU算法的设计原则是如果一个数据近期没有被访问到,那么之后一段时间都不会被访问到。所以当元素个数达到限制的值时,优先移除距离上次使用时间最久的元素。
可以使用HashMap
LFU算法
LFU算法的设计原则时,如果一个数据在最近一段时间被访问的时次数越多,那么之后被访问的概率会越大,实现是每个数据 都有一个引用计数,每次数据被访问后,引用计数加1,需要淘汰数据时,淘汰引用计数最小的数据。在Redis的实现中, 每次key被访问后,引用计数是加一个介于0到1之间的数p,并且访问越频繁p值越大,而且在一定的时间间隔内, 如果key没有被访问,引用计数会减少
1.1.2. Redis持久化方案aof的默认fsync时间是多长?
1s
叶落山城秋: aof 意思是 append only file,只允许追加不允许改写的文件, 这种存储机制是 某些写操作会把命令以协议内容追加到aof_buf缓冲区的末尾,在服务器每次结束一个事件循环之前,会调用flushAppendOnlyFile函数,考虑是否需要将aof_buf缓冲区中的内容写入和保存到aof文件里面,这个函数由appendfsync选项来决定,用户可以配置这个选项
always: 将 aof_buf 缓冲区中的所有内容写入并同步到AOF文件
everysec(默认): 将aof_buf 缓冲区中的所有文件写入到AOF文件,如果上次同步AOF文件的时间距离现在超过1秒钟,那么再次对AOF文件进行同步,并且这个同步操作是由一个线程专门负责执行的
no: 将aof_buf 缓冲区中的所有内容写入到AOF文件,但并不对AOF文件进行同步,何时间同步由操作系统决定
以上三种里 写入和同步 开始不理解,搜索了一下, 操作系统的写入(write) 应该也是 从缓冲区 写入到 文件中.. 所以上面写入并不一定已经写入到了磁盘的AOF文件里..
WRITE:根据条件,将
aof_buf
中的缓存写入到 AOF 文件。SAVE:根据条件,调用
fsync
或fdatasync
函数,将 AOF 文件保存到磁盘中。
1.1.3. Redis持久化方案rdb和aof的区别?
- aof文件比rdb更新的频率高,优先使用aof还原数据
- aof比rdb更安全也更大
- rdb性能比aof好
如果两个都配了优先加载aof
AOF因为是保存了所有执行的修改命令,粒度更细,进行数据恢复时,恢复的数据更加完整,但是由于需要对所有命令执行一遍,效率比较低,同样因为是保存了所有的修改命令,同样的数据集,保存的文件会比RDB大,而且随着执行时间的增加,AOF文件可能会越来越大,所有会通过执行BGREWRITEAOF命令来重新生成AOF文件,减小文件大小。
Redis服务器故障重启后,默认恢复数据的方式首选是通过AOF文件恢复,其次是通过RDB文件恢复。
RDB是保存某一个时间点的所有键值对信息,所以恢复时可能会丢失一部分数据,但是恢复效率会比较高
1.1.4. Redis的aof后台重写的触发条件?
- 可以由用户通过调用 BGREWRITEAOF手动触发
- 服务器在AOF功能开启的情况下,会维持以下三个变量
- 记录当前AOF文件大小的变量aof_current_size
- 记录最后一次AOF重写之后,AOF文件大小的变量 aof_rewrite_base_size
- 增长百分比变量 aof_rewrite_perc
- 每次当 serverCron函数执行时,它都会检查以下条件是否全部满足,如果是的话,就会触发AOF重写
- 没有BGSAVE命令在进行
- 没有BGREWRITEAOF在进行
- 当前AOF文件大小大于server.aof_rewrite_min_size (默认值1MB)
- 当前AOF文件大小和最后一次AOF重写后的大小之间的比率大于等于指定的增长百分比(默认百分比为100%)
1.1.5. 简单介绍下什么是缓存击穿, 缓存穿透, 缓存雪崩? 能否介绍些应对办法?
缓存穿透
解释:
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞
解决:
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存雪崩
解释:
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
解决:
缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
缓存击穿
解释:
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮
解决:
解决方案 | 优点 | 缺点 |
---|---|---|
简单分布式互斥锁(mutex key) | 1. 思路简单 2. 保证一致性 |
1. 代码复杂度增大 2. 存在死锁的风险 3. 存在线程池阻塞的风险 |
“提前”使用互斥锁 | 1. 保证一致性 | 同上 |
不过期 | 1. 异步构建缓存,不会阻塞线程池 | 1. 不保证一致性 2. 代码复杂度增大(每个value都要维护一个timekey) 3. 占用一定的内存空间(每个value都要维护一个timekey) |
资源隔离组件hystrix | 1. hystrix技术成熟,有效保证后端 2. hystrix监控强大 |
1. 部分访问存在降级策略。 |
1.1.6. 解释下RESP?
Redis Serialization Protocol --- redis序列化协议
RESP是redis客户端和服务端之间使用的一种通讯协议, 特点: 实现简单,快速解析,可读性好
RESP可以用于序列化不同的数据类型,如:整型,字符串,数组..并且为错误提供专门的类型,客户端发送请求时,以字符串数组的作为待执行命令的参数,redis服务器根据不同的命令返回不同的类型
RESP是二进制安全协议,并且处理批量数据无序逐个请求处理,因此批量数据传输时,在请求参数中添加了数据长度作为前缀,传输层基于TCP协议,默认端口为6379
RESP协议仅用作redis客户端和服务端之间通信,redis集群节点之间使用另一种二进制协议进行数据交换
RESP支持五种数据类型:
- 简单字符串类型(Simple Strings)
简单字符串以+开头
- 错误类型 (Errors)
错误数据以-开头
- 整型(Integers)
整数以:开头
- 批量字符串类型(Bulk Strings)
批量字符串以$开头
- 数组类型(Arrays)
数组以*开头
1.1.7. Redis有哪些架构模式,它们有什么特别点?
- 单机版
- 内存容量有限
- 处理能力有限
- 无法高可用
主从复制
Redis的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,
其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。
只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步 给从服务器,从而一直保证主从服务器的数据相同。
特点:
- master/slave 角色
- master/slave 数据相同
- 降低 master 读压力在转交从库
问题:
- 无法保证高可用
- 没有解决master写的压力
哨兵
Redis sentinel 是一个分布式系统中监控redis主从服务器,并在主服务器下线时自动进行故障转移,其中三个特性:
监控(Monitoring): Sentinel 会不断的检查你的主服务器和从服务器是否运作正常
提醒(Notification): 当被监控的某个redis服务器出现问题时,Sentinel可以通过API向管理员或者其他应用程序发送通知
自动故障迁移(Automatic failover): 当一个主服务器不能正常工作时,Sentinel会开始一次自动故障迁移操作
特点:
- 保证高可用
- 监控各个节点
- 自动故障迁移
缺点:
- 主从模式,切换需要时间丢数据
- 没有解决master写的压力
集群(proxy型)
Twemproxy 是一个Twitter开源的一个redis和memcache 快速/轻量级代理服务器,Twemproxy是一个快速的单线程代理程序,支持 Memcached ASCII 协议和redis协议
特点:
- 多种hash算法: MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
- 支持失败节点自动删除
- 后端Sharding 分片逻辑对业务透明,业务方的读写方式和操作单个Redis一致
缺点:
- 增加了新的proxy,需要维护其高可用
- failover 逻辑需要自己实现,其本身不能支持故障的自动转移可扩展性差,进行扩缩容都需要手动干预
集群(redis cluster)
从redis 3.0之后版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。
特点:
- 无中心结构(不存在哪个节点影响性能瓶颈),少了proxy层
- 数据按照slot存储分布在多个节点,节点间数据共享,可动态调整数据分布
- 可扩展性,可线性扩展到1000个节点,节点可动态添加和删除
- 高可用性,部分节点不可用时,集群仍可用,通过添加Slave做备份数据副本
- 实现故障自动failover,节点之间通过gossip协议交换状态信息,用投票机制完成Slave到Master的角色提升
缺点:
- 资源隔离性较差,容易出现相互影响的情况
- 数据通过异步复制,不保证数据的强一致性
1.1.8. 什么是哈希槽?
从Redis3.0之后的版本支持redis-cluster集群,Redis-Cluster采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接
结构特点:
- 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽
- 节点的fail是通过集群中超过半数的节点检测失效时才生效
- 客户端与redis节点直连,不需要中间proxy层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
- redis-cluster把所有的物理节点映射到[0~16383]slot上(不一定平均分配),cluster负责维护node<->slot<->value
- Redis集群预分好16384个桶,当需要在Redis集群中放置一个 key-value 时,根据 CRC16(key) mod 16384的值,决定将一个key放到哪个桶中
基本思想:
- 一共有16384个槽,每台服务器分管其中的一部分
- 插入一个数据的时候,先根据CRC16算法计算key对应的值,然后用该值对16384取余数,确定将数据放到哪个槽里面
- 在增加的时候,之前的节点个字分出一些槽给心节点,对应的数据也一起迁出
客户端可以向任何一个Redis节点发送请求,然后由节点将请求重定向到正确的节点上
为什么要选择的槽是16384个呢? crc16会输出16bit的结果,可以看作是一个分布在0-2^16-1之间的数,redis的作者测试发现这个数对2^{14}求模的会将key在0-2^{14-1}之间分布得很均匀,因此选了这个值。
参考: https://www.jianshu.com/p/fa623e59fdcf
参考: https://blog.csdn.net/z15732621582/article/details/79121213
1.1.9. 什么是CAP?
- 一致性(Consistency)
在分布式系统中的所有数据备份,在同一时刻是否同样的值.(等同于所有节点访问同一份最新的数据副本)
- 可用性(Availability)
在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求.(对数据更新具备高可用性)
- 分区容错性(Partition tolerance)
以实际效果而言,分区相当于对通信的时限要求,系统如果不能在时限内达成数据的一致性,就意味着发生了分区的情况,必须就当前操作在C和A之间做出选择
CAP理论就是说在分布式存储系统中,最多只能实现上面的两点!
1.1.10. 乐观锁,悲观锁?
悲观锁(Pessimistic Lock) 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。
1.1.11. Redis五大对象类型和其底层实现?
不骗你们,这题必考!!!
对象 | 对象type属性的值 | Type命令的输出 | 编码常量 | 编码对应的数据结构 | 编码对象 |
---|---|---|---|---|---|
字符串对象 | REDIS_STRING | "string" | REDIS_ENCODING_INT | long类型的整数 | 使用整数值实现的字符串对象 |
REDIS_ENCODING_EMBSTR | embstr编码的简单动态字符串 | 使用embstr编码的简单动态字符串实现的字符串对象 | |||
REDIS_ENCODING_RAW | 简单动态字符串 | 使用简单动态字符串实现的字符串对象 | |||
列表对象 | REDIS_LIST | "list" | REDIS_ENCODING_ZIPLIST | 压缩列表 | 使用压缩列表实现的列表对象 |
REDIS_ENCODING_LINKEDLIST | 双端链表 | 使用双端链表实现的列表对象 | |||
哈希对象 | REDIS_HASH | "hash" | REDIS_ENCODING_ZIPLIST | 压缩列表 | 使用压缩列表实现的哈希对象 |
REDIS_ENCODING_HT | 字典 | 使用字典实现的哈希对象 | |||
集合对象 | REDIS_SET | "set" | REDIS_ENCODING_INTSET | 整数集合 | 使用整数集合实现的集合对象 |
REDIS_ENCODING_HT | 字典 | 使用字典实现的集合对象 | |||
有序集合对象 | REDIS_ZSET | "zset" | REDIS_ENCODING_ZIPLIST | 压缩列表 | 使用压缩列表实现的有序集合对象 |
REDIS_ENCODING_SKIPLIST | 跳跃表和字典 | 使用跳跃表和字典实现的有序集合对象 |
- 查看类型
命令: OBJECT ENCODING + key
字符串对象
- int
- 如果一个字符串对象保存的是整数值,并且这个值可以用long类型表示,那么字符串对象会将整数存在字符串对象结构的ptr属性里面,并将字符串对象的编码设置为int
- raw
- 如果字符串对象保存的是一个字符串值,且字符串值的长度大于32字节,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置成raw
- embstr
- 如果字符串对象保存的是一个字符串值,并且这字符串长度小于等于32字节,那么字符串对象将embstr编码的方式来保存这个值
- embstr编码是专门用于保存短字符串的一种优化方式,编码跟raw一样,都使用redisObject和sdshdr结构来表示字符串对象,但raw编码会调用分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一个连续空间,空间中依次包含redisObject和sdshsr两结构
- 好处:
- embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次
- 释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数
- 因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里,所以这种编码的字符串对象比起raw编码的字符串对象能够更好的利用缓存带来优势
- 字符串总结:
- embstr和raw编码
- 两种存储方式下,都RedisObject和SDS结构(简单动态字符串)来存储字符串,区别在于,embstr对象用于存储较短的字符串,embstr编码中RedisObject结构与ptr指向的SDS结构在内存中是连续的,内存分配次数和内存释放次数均是一次,而raw编码会分别调用两次内存分配函数来分别创建RedisObject结构和SDS结构。
- embstr和raw编码
- int
列表对象
- ziplist(压缩列表)
- 介绍: 压缩列表
- 每个压缩列表节点(entry)保存了一个列表元素
- linkedlist(双端链表)
- 每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素
- 列表对象总结:
- 底层数据结构是一个链表,插入和删除很快,随机访问较慢,时间复杂度是O(N)
- Redis中的List可以作为一个队列来使用,也可以作为一个栈来使用。在实际使用中,常用来做异步队列使用,将可以延时处理的任务序列化成字符串塞进Redis的列表,另外一个线程从列表中轮询数据进行处理
- 老版本中的Redis,元素较少时,使用ziplist来作为底层编码,元素较多时使用双向链表linkedList作为底层编码。因为链表每个节点需要prev,next指针,需要占用16字节,而且每个节点内存都是单独分配,加剧内存碎片化,所以新版本使用quiklist作为底层编码,quiklist的是一个双向链表,但是它的每一个节点是一个ziplist。(默认每个ziplist最大长度为8k字节)
- ziplist(压缩列表)
哈希对象
- ziplist(压缩列表)
- 每当有新的键值对要假如到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再保存了值的压缩列表节点推入到压缩列表表尾
- 保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后
- 先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向
- 每当有新的键值对要假如到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再保存了值的压缩列表节点推入到压缩列表表尾
- hashtable(哈希表)
- 哈希对象中的每个键值对都使用一个字典键值对来保存
- 字典的每个
键
都是一个字符串对象,对象中保存了键值对的键
- 字典的每个
值
都是一个字符串对象,对象中保存了键值对的值
- 字典的每个
- 哈希对象中的每个键值对都使用一个字典键值对来保存
- 哈希对象总结
- value可以是一个hash表,底层编码可以是ziplist,也可以是hashtable(默认情况下,当元素小于512个时,底层使用ziplist存储数据)
- 元素保存的字符串长度较短且元素个数较少时(小于64字节,个数小于512),出于节约内存的考虑,hash表会使用ziplist作为的底层实现,ziplist是一块连续的内存,里面每一个节点保存了对应的key和value,然后每个节点很紧凑地存储在一起,优点是没有冗余空间,缺点插入新元素需要调用realloc扩展内存。(可能会进行内存重分配,将内容拷贝过去,也可能在原有地址上扩展)。
- 元素比较多时就会使用hashtable编码来作为底层实现,这个时候RedisObject的ptr指针会指向一个dict结构,dict结构中的ht数组保存了ht[0]和ht[1]两个元素,通常使用ht[0]保存键值对,ht[1]只在渐进式rehash时使用。hashtable是通过链地址法来解决冲突的,table数组存储的是链表的头结点(添加新元素,首先根据键计算出hash值,然后与数组长度取模之后得到数组下标,将元素添加到数组下标对应的链表中去)
- ziplist(压缩列表)
集合对象
- intset(整数集合)
- 集合对象包含的所有元素都被保存在整数集合里面
- hashtable(哈希表)
- 字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL
- 集合对象
- 当元素都为整数,且元素个数较少时会使用inset作为底层编码,inset结构中的有一个contents属性,content是是一个整数数组,从小到大保存了所有元素。
- 当元素个数较多时,Set使用hashtable来保存元素,元素的值作为key,value都是NULL。
- intset(整数集合)
- 有序集合对象
- ziplist(压缩列表)
- 每个集合元素使用两个紧挨在一起的压缩列表来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)
- 压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向
- skiplist(跳跃表)
- 介绍: 跳跃表
- 一个zset结构同时包含一个字典和一个跳跃表
- 有序集合总结:
- Zset与Set的区别在于每一个元素都有一个Score属性,并且存储时会将元素按照Score从低到高排列。
- 当元素较少时,ZSet的底层编码使用ziplist实现,所有元素按照Score从低到高排序。
- 当元素较多时,使用skiplist+dict来实现,
- skiplist存储元素的值和Score,并且将所有元素按照分值有序排列。便于以O(logN)的时间复杂度插入,删除,更新,及根据Score进行范围性查找。
- dict存储元素的值和Score的映射关系,便于以O(1)的时间复杂度查找元素对应的分值。
- ziplist(压缩列表)
以上参考 < redis设计与实现 > 一书
1.1.12. 怎么保持Redis缓存里的数据与数据库里的一致?
如果上面那个被问到了,那么这个就比问...
读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。
不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。
举一个例子:
- 如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。
如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。如来解决?这里给出两个解决方案,先易后难,结合业务和技术代价选择使用。
解决方案
第一种方案:采用延时双删策略
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。
伪代码如下
public void write( String key, Object data )
{
redis.delKey( key );
db.updateData( data );
Thread.sleep( 500 );
redis.delKey( key );
}
具体的步骤就是:
- 先删除缓存
- 再写数据库
- 休眠500毫秒
- 再次删除缓存
那么,这个500毫秒怎么确定的,具体该休眠多久呢?
需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
当然这种策略还要考虑redis和数据库主从同步的耗时。最后的的写数据的休眠时间:则在读数据业务逻辑的耗时基础上,加几百ms即可。比如:休眠1秒。
设置缓存过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
该方案的弊端
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。
叶落山城秋: 这种方案,还是有误差延时的,对于秒杀这种操作肯定是不行的! 另外,这种操作的流程是 先删除缓存,如果这时候有请求进来了
数据库还没更新操作,这时候如果量比较大可能会发生缓存穿透, 不过可能是单点穿透,这时候又对key写入了以前的值!
此时数据库更新了! 500毫秒后 再次删掉之前的key,重新穿透再缓存一次!
第二种方案:异步更新缓存(基于订阅binlog的同步机制)
技术整体思路:(MySQL binlog增量订阅消费+消息队列+增量数据更新到redis)
- 读Redis:热数据基本都在Redis
- 写MySQL: 增删改都是操作MySQL
- 更新Redis数据:MySQ的数据操作binlog,来更新到Redis
Redis更新
- 数据操作主要分为两大块:
- 一个是全量(将全部数据一次写入到redis)
- 一个是增量(实时更新)(这里说的是增量,指的是mysql的update、insert、delate变更数据。)
- 读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis!
- 数据操作主要分为两大块:
以上摘自: Redis和mysql数据怎么保持数据一致的?
1.1.13. Redis的持久化是怎么实现的?
Redis主要通过AOF和RDB实现持久化。
AOF持久化主要是Redis在修改相关的命令后,将命令添加到aof_buf缓存区的末尾,然后在每次事件循环结束时,根据appendfsync的配置(always是总是写入,everysec是每秒写入,no是根据操作系统来决定何时写入),判断是否需要将aof_buf写入AOF文件。
RDB持久化指的是在满足一定的触发条件时(在一个的时间间隔内执行修改命令达到一定的数量,或者手动执行SAVE和BGSAVE命令),对这个时间点的数据库所有键值对信息生成一个压缩文件dump.rdb,然后将旧的删除,进行替换。
实现原理是fork一个子进程,然后对键值对进行遍历,生成rdb文件,在生成过程中,父进程会继续处理客户端发送的请求,当父进程要对数据进行修改时,会对相关的内存页进行拷贝,修改的是拷贝后的数据。(也就是COPY ON WRITE,写时复制技术,就是当多个调用者同时请求同一个资源,如内存或磁盘上的数据存储,他们会共用同一个指向资源的指针,指向相同的资源,只有当一个调用者试图修改资源的内容时,系统才会真正复制一份专用副本给这个调用者,其他调用者还是使用最初的资源,在ArrayList的实现中,也有用到,添加一个新元素时过程是,加锁,对原数组进行复制,然后添加新元素,然后替代旧数组,解锁)
1.1.14. 怎么防止AOF文件越来越大?
为了防止AOF文件越来越大,可以通过执行BGREWRITEAOF命令,会fork子进程出来,读取当前数据库的键值对信息,生成所需的写命令,写入新的AOF文件。
在生成期间,父进程继续正常处理请求,执行修改命令后,不仅会将命令写入aof_buf缓冲区,还会写入重写aof_buf缓冲区。
当新的AOF文件生成完毕后,子进程父进程发送信号,父进程将重写aof_buf缓冲区的修改命令写入新的AOF文件,写入完毕后,对新的AOF文件进行改名,原子地(atomic)地替换旧的AOF文件。
1.1.15. Redis持久化策略该如何进行选择?
RDB持久化的特点是:文件小,恢复快,不影响性能,实时性低,兼容性差(老版本的Redis不兼容新版本的RDB文件)
AOF持久化的特点是:文件大,恢复慢,性能影响大,实时性高。是目前持久化的主流(主要是当前项目开发不太能接受大量数据丢失的情况)。
需要了解的是持久化选项的开启必然会造成一定的性能消耗
RDB持久化主要在于bgsave在进行fork操作时,会阻塞Redis的主线程。以及向硬盘写数据会有一定的I/O压力。
AOF持久化主要在于将aof_buf缓冲区的数据同步到磁盘时会有I/O压力,而且向硬盘写数据的频率会高很多。
其次是,AOF文件重写跟RDB持久化类似,也会有fork时的阻塞和向硬盘写数据的压力。
以下是几种持久化方案选择的场景:
不需要考虑数据丢失的情况,那么不需要考虑持久化。
单机实例情况下,可以接受丢失十几分钟及更长时间的数据,可以选择RDB持久化,对性能影响小,如果只能接受秒级的数据丢失,只能选择AOF持久化。
在主从环境下,因为主服务器在执行修改命令后,会将命令发送给从服务器,从服务进行执行后,与主服务器保持数据同步,实现数据热备份,在master宕掉后继续提供服务。同时也可以进行读写分离,分担Redis的读请求。
那么在从服务器进行数据热备份的情况下,是否还需要持久化呢?
- 需要持久化,因为不进行持久化,主服务器,从服务器同时出现故障时,会导致数据丢失。(例如:机房全部机器断电)。如果系统中有自动拉起机制(即检测到服务停止后重启该服务)将master自动重启,由于没有持久化文件,那么master重启后数据是空的,slave同步数据也变成了空的。应尽量避免“自动拉起机制”和“不做持久化”同时出现。
所以一般可以采用以下方案:
主服务器不开启持久化,使得主服务器性能更好。
从服务器开启AOF持久化,关闭RDB持久化,并且定时对AOF文件进行备份,以及在凌晨执行bgaofrewrite命令来进行AOF文件重写,减小AOF文件大小。(当然如果对数据丢失容忍度高也可以开启RDB持久化,关闭AOF持久化)
异地灾备,一般性的故障(停电,关机)不会影响到磁盘,但是一些灾难性的故障(地震,洪水)会影响到磁盘,所以需要定时把单机上或从服务器上的AOF文件,RDB文件备份到其他地区的机房。
1.1.16. AOF文件追加阻塞是什么?
修改命令添加到aof_buf之后,如果配置是everysec那么会每秒执行fsync操作,调用write写入磁盘一次,但是如果硬盘负载过高,fsync操作可能会超过1s,Redis主线程持续高速向aof_buf写入命令,硬盘的负载可能会越来越大,IO资源消耗更快,所以Redis的处理逻辑是会对比上次fsync成功的时间,如果超过2s,则主线程阻塞直到fsync同步完成,所以最多可能丢失2s的数据,而不是1s。
1.1.17. Redis为什么是单线程的?
经典题了..最近某大厂也被问了..不过可能还会继续问,如果多线程,会有什么影响之类的
Redis官方FAQ回答: Redis是基于内存的操作,CPU不会成为瓶颈所在,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。 (这里的单线程指的是处理网络请求的模块是单线程,其他模块不一定是单线程的)
Redis采用单线程的优势:
Redis项目的代码会更加清晰,处理逻辑会更加简单。
不用考虑多个线程修改数据的情况,修改数据时不用加锁,解锁,也不会出现死锁的问题,导致性能消耗。
不存在多进程或者多线程导致的切换而造成的一些性能消耗。
Redis采用单线程的劣势:
1.无法充分发挥多核机器的优势,不过可以通过在机器上启动多个Redis实例来利用资源。
1.1.18. Redis性能为什么高?
根据官网的介绍,Redis单机可以到到10W的QPS(每秒处理请求数),Redis这么快的原因主要有以下几点:
完全基于内存,数据全部存储在内存中,读取时没有磁盘IO,所以速度非常快。
Redis采用单线程的模型,没有上下文切换的开销,也没有竞态条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗。
Redis项目中使用的数据结构都是专门设计的,例如SDS(简单动态字符串)是对C语言中的字符串频繁修改时,会频繁地进行内存分配,十分消耗性能,而SDS会使用空间预分配和惰性空间释放来避免这些问题的出现。
空间预分配技术: 对SDS进行修改时,如果修改后SDS实际使用长度为len,
当len<1M时,分配的空间会是2*len+1,也就是会预留len长度的未使用空间,其中1存储空字符
当len>1M时,分配的空间会是len+1+1M,也就是会预留1M长度的未使用空间,其中1存储空字符
采用多路复用IO模型,可以同时监测多个流的IO事件能力,在空闲时,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态唤醒,轮询那些真正发出了事件的流,并只依次顺序的处理就绪的流。可以让单个线程高效的处理多个连接请求(尽量减少网络 I/O 的时间消耗)。
1.1.19. Redis主从同步是怎么实现的?
主从节点建立连接后,从节点会进行判断
如果这是从节点之前没有同步过数据,属于初次复制,会进行全量重同步 那么从节点会向主节点发送PSYNC?-1 命令,请求主节点进行全量重同步。
如果这是从节点不说初次复制(例如出现掉线后重连),
这个时候从节点会将之前进行同步的Replication ID(一个随机字符串,标识主节点上的特定数据集)和offset(从服务器当前的复制偏移量)通过PSYNC 命令发送给主节点,主节点会进行判断,
- 如果Replication ID跟当前的Replication ID不一致,或者是当前buffer缓冲区中不存在对应的offset,那么会跟上面的初次复制一样,进行全量重同步。
- 如果Replication ID跟当前的Replication ID一致并且当前buffer缓冲区中存在对应的offset,那么会进行部分重同步。(部分重同步是Redis 2.8之后的版本支持的,主要基于性能考虑,为了断线期间的小部分数据修改进行全量重同步效率比较低)
全量重同步
主节点会执行BGSAVE命令,fork出一个子进程,在后台生成一个RDB持久化文件,完成后,发送给从服务器,从节点接受并载入RDB文件,使得从节点的数据库状态更新至主节点执行BGSAVE命令时的状态。并且在生成RDB文件期间,主节点也会使用一个缓冲区来记录这个期间执行的所有写命令,将这些命令发送给从节点,从节点执行命令将自己数据库状态更新至与主节点完全一致。
部分重同步
因为此时从节点只是落后主节点一小段时间的数据修改,并且偏移量在复制缓冲区buffer中可以找到,所以主节点把从节点落后的这部分数据修改命令发送给从节点,完成同步。
命令传播
在执行全量重同步或者部分重同步以后,主从节点的数据库状态达到一致后,会进入到命令传播阶段。主节点执行修改命令后,会将修改命令添加到内存中的buffer缓冲区(是一个定长的环形数组,满了时就会覆盖前面的数据),然后异步地将buffer缓冲区的命令发送给从节点。
1.1.20. Redis中哨兵是什么?
Redis中的哨兵服务器是一个运行在哨兵模式下的Redis服务器,核心功能是监测主节点和从节点的运行情况,在主节点出现故障后, 完成自动故障转移,让某个从节点升级为主节点。
1.1.21. Redis哨兵系统是怎么实现自动故障转移的?
认定主节点主观下线
因为每隔2s,哨兵节点会给主节点发送PING命令,如果在一定时间间隔内,都没有收到回复,那么哨兵节点就认为主节点主观下线。
认定主节点客观下线
哨兵节点认定主节点主观下线后,会向其他哨兵节点发送sentinel is-master-down-by-addr命令,获取其他哨兵节点对该主节点的状态,当认定主节点下线的哨兵数量达到一定数值时,就认定主节点客观下线。
进行领导者哨兵选举
认定主节点客观下线后,各个哨兵之间相互通信,选举出一个领导者哨兵,由它来对主节点进行故障转移操作。
选举使用的是Raft算法,基本思路是所有哨兵节点A会先其他哨兵节点,发送命令,申请成为该哨兵节点B的领导者,如果B还没有同意过其他哨兵节点,那么就同意A成为领导者,最终得票超过半数以上的哨兵节点会赢得选举,如果本次投票,没有选举出领导者哨兵,那么就开始新一轮的选举,直到选举出哨兵节点(实际开发中,最先判定主节点客观下线的哨兵节点,一般就能成为领导者。)
领导者哨兵进行故障转移
领导者哨兵节点首先会从从节点中选出一个节点作为新的主节点。选择的规则是:
- 首先排除一些不健康的节点。(下线的,断线的,最近5s没有回复哨兵节点的INFO命令的,与旧的主服务器断开连接时间较长的)
- 然后根据优先级,复制偏移量,runid最小,来选出一个从节点作为主节点。
向这个从节点发送slaveof no one命令,让其成为主节点,通过slaveof 命令让其他从节点成为它的从节点,将已下线的主节点更新为新的主节点的从节点。
1.1.22. 为什么不用红黑树作为zset底层实现?
They are not very memory intensive. It's up to you basically.
缺点:
- 比红黑树占用更多的内存,每个节点的大小取决于该节点的层数
- 空间局部性较差导致缓存命中率低,感觉上会比红黑树更慢
优点:
- 实现比红黑树简单
- 比红黑树更容易扩展,作者之后实现zrank指令时没怎么改动代码。
- 红黑树插入删除时为了平衡高度需要旋转附近节点,高并发时需要锁。skiplist不需要考虑。
- 一般用zset的操作都是执行zrange之类的操作,取出一片连续的节点。这些操作的缓存命中率不会比红黑树低。
1.1.23. rehash因为是渐进式的,那如果长时间没有任何指令操作,那rehash还会继续进行吗?
add 2021-06-30
- 首先 rehash 为了防止大量的计算造成服务不稳定, 采用
渐进式rehash
的操作, 即 在有 增删改查的指令的时候,进行一次rehash操作 - 那如果 没有增删改查的指令的时候,rehash将会怎样呢?
- redis周期函数,会定时检查,如果发现字典正在进行rehash操作, 则会花费
1毫秒
的时间,进行渐进式rehash操作
/* Rehash */
if (server.activerehashing) {
for (j = 0; j < dbs_per_call; j++) {
int work_done = incrementallyRehash(rehash_db);
if (work_done) {
/* If the function did some work, stop here, we'll do
* more at the next cron loop. */
break;
} else {
/* If this db didn't need rehash, we'll try the next one. */
rehash_db++;
rehash_db %= server.dbnum;
}
}
}
1.1.24. 跳表在redis什么地方有被使用
add 2021-06-30
- 有序集合
- 集群节点中用作内部数据结构
1.1.25. redis缓存使用中的热key问题
add 2021-09-10
何为热key问题? 在Redis中,访问频率高的key称为热点key,当某一热点key的请求到Server主机时,由于请求量特别大,导致主机资源不足,甚至宕机,影响正常的服务
热key问题产生原因
- 用户消费的数据远大于生产的数据,比如热卖商品、热点新闻、热点评论等,这些典型的读多写少的场景会产生热点问题
- 请求分片集中,超过单Server的性能极限,比如 固定名称key,哈希落入一台Server,访问量极大的情况,超过Server极限时,就会导致热点Key问题的产生
热key的危害
- 流量集中,达到物理网卡上限,影响其他key的访问。
- 请求过多,缓存分片服务被打垮,不能通过扩容解决,且不能发挥集群多分片的优势。
- 缓存击穿,可能打到DB,引起业务雪崩。
如何找到热key?
- 凭借业务经验,进行预估哪些是热key
- 客户端统计收集,本地统计或者上报
- 如果服务端有代理层,可以在代理层进行收集上报
如何解决热key问题?
- 增加分片副本,分担读流量
- 热key备份,比如key,备份为key1,key2……keyN,同样的数据N个备份,N个备份分布到不同分片,访问时可随机访问N个备份中的一个,进一步分担读流量
- 使用本地缓存,发现热key后,将热key对应数据加载到应用服务器本地缓存中,访问热key数据时,直接从本地缓存中获取,而不会请求到redis服务器。
1.1.26. Redis为什么这么快
add 2021-10-26
内存存储:Redis是使用内存(in-memeroy)存储,没有磁盘IO上的开销。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1)。
单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。注意:单线程是指的是在核心网络模型中,网络请求模块使用一个线程来处理,即一个线程处理所有网络请求。
非阻塞IO:Redis使用多路复用IO技术,将epoll作为I/O多路复用技术的实现,再加上Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
优化的数据结构:Redis有诸多可以直接应用的优化数据结构的实现,应用层可以直接使用原生的数据结构提升性能。
使用底层模型不同:Redis直接自己构建了 VM (虚拟内存)机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
Redis的VM(虚拟内存)机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过VM功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。
Redis提高数据库容量的办法有两种:一种是可以将数据分割到多个RedisServer上;另一种是使用虚拟内存把那些不经常访问的数据交换到磁盘上。需要特别注意的是Redis并没有使用OS提供的Swap,而是自己实现。
1.1.27. Redis相比Memcached有哪些优势?
add 2021-10-26
数据类型:Memcached所有的值均是简单的字符串,Redis支持更为丰富的数据类型,支持string(字符串),list(列表),Set(集合)、Sorted Set(有序集合)、Hash(哈希)等。
持久化:Redis支持数据落地持久化存储,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。 memcache不支持数据持久存储 。
集群模式:Redis提供主从同步机制,以及 Cluster集群部署能力,能够提供高可用服务。Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据
性能对比:Redis的速度比Memcached快很多。
网络IO模型:Redis使用单线程的多路 IO 复用模型,Memcached使用多线程的非阻塞IO模式。
Redis支持服务器端的数据操作:Redis相比Memcached来说,拥有更多的数据结构和并支持更丰富的数据操作,通常在Memcached里,你需要将数据拿到客户端来进行类似的修改再set回去。
这大大增加了网络IO的次数和数据体积。在Redis中,这些复杂的操作通常和一般的GET/SET一样高效。所以,如果需要缓存能够支持更复杂的结构和操作,那么Redis会是不错的选择。
1.1.28. 为什么要用 Redis 做缓存?
add 2021-10-26
从高并发上来说:
- 直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
从高性能上来说:
- 用户第一次访问数据库中的某些数据。 因为是从硬盘上读取的所以这个过程会比较慢。将该用户访问的数据存在缓存中,下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据。
1.1.29. 为什么要用 Redis 而不用 map/guava 做缓存?
add 2021-10-26
缓存分为本地缓存和分布式缓存。以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着jvm的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
使用Redis或memcached之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持Redis或memcached服务的高可用,整个程序架构上较为复杂。
对比:
- Redis 可以用几十 G 内存来做缓存,Map 不行,一般 JVM 也就分几个 G 数据就够大了;
- Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了;
- Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里;
- Redis 可以处理每秒百万级的并发,是专业的缓存服务,Map 只是一个普通的对象;
- Redis 缓存有过期机制,Map 本身无此功能;Redis 有丰富的 API,Map 就简单太多了;
- Redis可单独部署,多个项目之间可以共享,本地内存无法共享;
- Redis有专门的管理工具可以查看缓存数据。
1.1.30. Redis的常用场景有哪些?
add 2021-10-26
1、缓存
缓存现在几乎是所有中大型网站都在用的必杀技,合理的利用缓存不仅能够提升网站访问速度,还能大大降低数据库的压力。Redis提供了键过期功能,也提供了灵活的键淘汰策略,所以,现在Redis用在缓存的场合非常多。
2、排行榜
很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。
3、计数器
什么是计数器,如电商网站商品的浏览量、视频网站视频的播放数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。Redis提供的incr命令来实现计数器功能,内存操作,性能非常好,非常适用于这些计数场景。
4、分布式会话
集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。
5、分布式锁
在很多互联网公司中都使用了分布式技术,分布式技术带来的技术挑战是对同一个资源的并发访问,如全局ID、减库存、秒杀等场景,并发量不大的场景可以使用数据库的悲观锁、乐观锁来实现,但在并发量高的场合中,利用数据库锁来控制资源的并发访问是不太理想的,大大影响了数据库的性能。可以利用Redis的setnx功能来编写分布式的锁,如果设置返回1说明获取锁成功,否则获取锁失败,实际应用中要考虑的细节要更多。
6、 社交网络
点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。如在微博中的共同好友,通过Redis的set能够很方便得出。
7、最新列表
Redis列表结构,LPUSH可以在列表头部插入一个内容ID作为关键字,LTRIM可用来限制列表的数量,这样列表永远为N个ID,无需查询最新的列表,直接根据ID去到对应的内容页即可。
8、消息系统
消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。Redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统。另外,这个不能和专业的消息中间件相比。
1.1.31. Redis持久化机制?
add 2021-10-26
为了能够重用Redis数据,或者防止系统故障,我们需要将Redis中的数据写入到磁盘空间中,即持久化。
Redis提供了两种不同的持久化方法可以将数据存储在磁盘中,一种叫快照RDB
,另一种叫只追加文件AOF
。
RDB
在指定的时间间隔内将内存中的数据集快照写入磁盘(Snapshot
),它恢复时是将快照文件直接读到内存里。
- 优势:适合大规模的数据恢复;对数据完整性和一致性要求不高
- 劣势:在一定间隔时间做一次备份,所以如果Redis意外
down
掉的话,就会丢失最后一次快照后的所有修改。
AOF
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,Redis启动之初会读取该文件重新构建数据,换言之,Redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
AOF采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时, Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集.。
优势
- 每修改同步:
appendfsync always
同步持久化,每次发生数据变更会被立即记录到磁盘,性能较差但数据完整性比较好 - 每秒同步:
appendfsync everysec
异步操作,每秒记录,如果一秒内宕机,有数据丢失 - 不同步:
appendfsync no
从不同步
劣势
- 相同数据集的数据而言
aof
文件要远大于rdb
文件,恢复速度慢于rdb
aof
运行效率要慢于rdb
,每秒同步策略效率较好,不同步效率和rdb
相同
1.1.32. 如何选择合适的持久化方式?
add 2021-10-26
- 如果是数据不那么敏感,且可以从其他地方重新生成补回的,那么可以关闭持久化。
- 如果是数据比较重要,不想再从其他地方获取,且可以承受数分钟的数据丢失,比如缓存等,那么可以只使用RDB。
- 如果是用做内存数据库,要使用Redis的持久化,建议是RDB和AOF都开启,或者定期执行bgsave做快照备份,RDB方式更适合做数据的备份,AOF可以保证数据的不丢失。
补充:Redis4.0 对于持久化机制的优化
Redis4.0相对与3.X版本其中一个比较大的变化是4.0添加了新的混合持久化方式。
简单的说:新的AOF文件前半段是RDB格式的全量数据后半段是AOF格式的增量数据,如下图:
优势:混合持久化结合了RDB持久化 和 AOF 持久化的优点, 由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据以AOF方式保存了,数据更少的丢失。
劣势:兼容性差,一旦开启了混合持久化,在4.0之前版本都不识别该aof文件,同时由于前部分是RDB格式,阅读性较差。
1.1.33. Redis持久化数据和缓存怎么做扩容?
add 2021-10-26
如果Redis被当做缓存使用,使用一致性哈希实现动态扩容缩容。
如果Redis被当做一个持久化存储使用,必须使用固定的keys-to-nodes映射关系,节点的数量一旦确定不能变化。否则的话(即Redis节点需要动态变化的情况),必须使用可以在运行时进行数据再平衡的一套系统,而当前只有Redis集群可以做到这样。
1.1.34. 什么是缓存预热?
add 2021-10-26
缓存预热是指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。
如果不进行预热,那么Redis初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。
缓存预热解决方案:
数据量不大的时候,工程启动的时候进行加载缓存动作;
数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
数据量太大的时候,优先保证热点数据进行提前加载到缓存。
1.1.35. 什么是缓存降级?
add 2021-10-26
缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
1.1.36. Redis真的是单线程?
add 2021-10-26
讨论 这个问题前,先看下 Redis的版本中两个重要的节点:
- Redisv4.0(引入多线程处理异步任务)
- Redis 6.0(在网络模型中实现多线程 I/O )
所以,网络上说的Redis是单线程,通常是指在Redis 6.0之前,其核心网络模型使用的是单线程。
且Redis6.0引入多线程I/O,只是用来处理网络数据的读写和协议的解析,而执行命令依旧是单线程。
Redis在 v4.0 版本的时候就已经引入了的多线程来做一些异步操作,此举主要针对的是那些非常耗时的命令,通过将这些命令的执行进行异步化,避免阻塞单线程的事件循环。
在 Redisv4.0 之后增加了一些的非阻塞命令如
UNLINK
、FLUSHALL ASYNC
、FLUSHDB ASYNC
。
1.1.37. Redis 6.0为何引入多线程?
add 2021-10-26
很简单,就是 Redis的网络 I/O 瓶颈已经越来越明显了。
随着互联网的飞速发展,互联网业务系统所要处理的线上流量越来越大,Redis的单线程模式会导致系统消耗很多 CPU 时间在网络 I/O 上从而降低吞吐量,要提升 Redis的性能有两个方向:
- 优化网络 I/O 模块
- 提高机器内存读写的速度
后者依赖于硬件的发展,暂时无解。所以只能从前者下手,网络 I/O 的优化又可以分为两个方向:
- 零拷贝技术或者 DPDK 技术
- 利用多核优势
零拷贝技术有其局限性,无法完全适配 Redis这一类复杂的网络 I/O 场景,更多网络 I/O 对 CPU 时间的消耗和 Linux 零拷贝技术。而 DPDK 技术通过旁路网卡 I/O 绕过内核协议栈的方式又太过于复杂以及需要内核甚至是硬件的支持。
总结起来,Redis支持多线程主要就是两个原因:
可以充分利用服务器 CPU 资源,目前主线程只能利用一个核
多线程任务可以分摊 Redis 同步 IO 读写负荷
1.1.38. Redis 6.0 采用多线程后,性能的提升效果如何?
add 2021-10-26
Redis 作者 antirez 在 RedisConf 2019 分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。
国内也有大牛曾使用 unstable 版本在阿里云 esc 进行过测试,GET/SET 命令在 4 线程 IO 时性能相比单线程是几乎是翻倍了。
1.1.39. 介绍下Redis的线程模型?
add 2021-10-26
Redis的线程模型包括Redis 6.0之前和Redis 6.0。
下面介绍的是Redis 6.0之前。
Redis 是基于 reactor 模式开发了网络事件处理器,这个处理器叫做文件事件处理器(file event handler)。由于这个文件事件处理器是单线程的,所以 Redis 才叫做单线程的模型。采用 IO 多路复用机制同时监听多个 Socket,根据 socket 上的事件来选择对应的事件处理器来处理这个事件。
IO多路复用是 IO 模型的一种,有时也称为异步阻塞 IO,是基于经典的 Reactor 设计模式设计的。多路指的是多个 Socket 连接,复用指的是复用一个线程。多路复用主要有三种技术:Select,Poll,Epoll。
Epoll 是最新的也是目前最好的多路复用技术。
模型如下图:
文件事件处理器的结构包含了四个部分:
- 多个 Socket。Socket 会产生 AE_READABLE 和 AE_WRITABLE 事件:
- 当 socket 变得可读时或者有新的可以应答的 socket 出现时,socket 就会产生一个 AE_READABLE 事件
- 当 socket 变得可写时,socket 就会产生一个 AE_WRITABLE 事件。
- IO 多路复用程序
- 文件事件分派器
- 事件处理器。事件处理器包括:连接应答处理器、命令请求处理器、命令回复处理器,每个处理器对应不同的 socket 事件:
- 如果是客户端要连接 Redis,那么会为 socket 关联连接应答处理器
- 如果是客户端要写数据到 Redis(读、写请求命令),那么会为 socket 关联命令请求处理器
- 如果是客户端要从 Redis 读数据,那么会为 socket 关联命令回复处理器
多个 socket 会产生不同的事件,不同的事件对应着不同的操作,IO 多路复用程序监听着这些 Socket,当这些 Socket 产生了事件,IO 多路复用程序会将这些事件放到一个队列中,通过这个队列,以有序、同步、每次一个事件的方式向文件时间分派器中传送。当事件处理器处理完一个事件后,IO 多路复用程序才会继续向文件分派器传送下一个事件。
下图是客户端与 Redis 通信的一次完整的流程:
- Redis 启动初始化的时候,Redis 会将连接应答处理器与 AE_READABLE 事件关联起来。
- 如果一个客户端跟 Redis 发起连接,此时 Redis 会产生一个 AE_READABLE 事件,由于开始之初 AE_READABLE 是与连接应答处理器关联,所以由连接应答处理器来处理该事件,这时连接应答处理器会与客户端建立连接,创建客户端响应的 socket,同时将这个 socket 的 AE_READABLE 事件与命令请求处理器关联起来。
- 如果这个时间客户端向 Redis 发送一个命令(set k1 v1),这时 socket 会产生一个 AE_READABLE 事件,IO 多路复用程序会将该事件压入队列中,此时事件分派器从队列中取得该事件,由于该 socket 的 AE_READABLE 事件已经和命令请求处理器关联了,因此事件分派器会将该事件交给命令请求处理器处理,命令请求处理器读取事件中的命令并完成。操作完成后,Redis 会将该 socket 的 AE_WRITABLE 事件与命令回复处理器关联。
- 如果客户端已经准备好接受数据后,Redis 中的该 socket 会产生一个 AE_WRITABLE 事件,同样会压入队列然后被事件派发器取出交给相对应的命令回复处理器,由该命令回复处理器将准备好的响应数据写入 socket 中,供客户端读取。
- 命令回复处理器写完后,就会删除该 socket 的 AE_WRITABLE 事件与命令回复处理器的关联关系。
1.1.40. Redis 6.0 多线程的实现机制?
add 2021-10-26
1.1.41. Redis 6.0开启多线程后,是否会存在线程并发安全问题?
add 2021-10-26
从实现机制可以看出,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。
所以我们不需要去考虑控制 Key、Lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。
1.1.42. Redis 6.0 与 Memcached 多线程模型的对比?
add 2021-10-26
相同点:都采用了 Master 线程 -Worker 线程的模型。
不同点:Memcached 执行主逻辑也是在 Worker 线程里,模型更加简单,实现了真正的线程隔离,符合我们对线程隔离的常规理解。
而 Redis 把处理逻辑交还给 Master 线程,虽然一定程度上增加了模型复杂度,但也解决了线程并发安全等问题。
1.1.43. Redis事务的概念?
add 2021-10-26
Redis的事务并不是我们传统意义上理解的事务,我们都知道 单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。
事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。
总结:
Redis事务中如果有某一条命令执行失败,之前的命令不会回滚,其后的命令仍然会被继续执行。鉴于这个原因,所以说Redis的事务严格意义上来说是不具备原子性的。
Redis事务中所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。
当使用Append-Only模式时,Redis会通过调用系统函数write将该事务内的所有写操作在本次调用中全部写入磁盘。然而如果在写入的过程中出现系统崩溃,如电源故障导致的宕机,那么此时也许只有部分数据被写入到磁盘,而另外一部分数据却已经丢失。Redis服务器会在重新启动时执行一系列必要的一致性检测,一旦发现类似问题,就会立即退出并给出相应的错误提示。此时,我们就要充分利用Redis工具包中提供的Redis-check-aof工具,该工具可以帮助我们定位到数据不一致的错误,并将已经写入的部分数据进行回滚。修复之后我们就可以再次重新启动Redis服务器了。
1.1.44. Redis事务的三个阶段
add 2021-10-26
- multi 开启事务
- 大量指令入队
- exec执行事务块内命令,截止此处一个事务已经结束。
- discard 取消事务
- watch 监视一个或多个key,如果事务执行前key被改动,事务将打断。unwatch 取消监视。
事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队.
1.1.45. Redis事务支持隔离性吗?
add 2021-10-26
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
1.1.46. Redis为什么不支持事务回滚?
add 2021-10-26
- Redis 命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面,这些问题不能在入队时发现,这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中.
- 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
1.1.47. Redis事务其他实现?
add 2021-10-26
- 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行, 其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完。
- 基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐。
1.1.48. 主从复制的原理?
add 2021-10-26
1、主从架构的核心原理
当启动一个slave node的时候,它会发送一个PSYNC命令给master node
如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization
开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。
slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。
2、主从复制的断点续传
从Redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份
master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制
但是如果没有找到对应的offset,那么就会执行一次resynchronization
3、无磁盘化复制
master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了
repl-diskless-sync repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来
4、过期key处理
slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。
1.1.49. 由于主从延迟导致读取到过期数据怎么处理?
add 2021-10-26
- 通过scan命令扫库:当Redis中的key被scan的时候,相当于访问了该key,同样也会做过期检测,充分发挥Redis惰性删除的策略。这个方法能大大降低了脏数据读取的概率,但缺点也比较明显,会造成一定的数据库压力,否则影响线上业务的效率。
- Redis加入了一个新特性来解决主从不一致导致读取到过期数据问题,增加了key是否过期以及对主从库的判断,如果key已过期,当前访问的master则返回null;当前访问的是从库,且执行的是只读命令也返回null。
1.1.50. 主从复制的过程中如果因为网络原因停止复制了会怎么样?
add 2021-10-26
如果出现网络故障断开连接了,会自动重连的,从Redis 2.8开始,就支持主从复制的断点续传,可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。
master node会在内存中创建一个backlog
,master和slave都会保存一个replica offset
,还有一个master id
,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制。
但是如果没有找到对应的offset,那么就会执行一次resynchronization
全量复制。
1.1.51. Redis主从架构数据会丢失吗,为什么?
add 2021-10-26
有两种数据丢失的情况:
- 异步复制导致的数据丢失:因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。
- 脑裂导致的数据丢失:某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着,此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master。这个时候,集群里就会有两个master,也就是所谓的脑裂。此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了。因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据。
1.1.52. 如何解决主从架构数据丢失的问题?
add 2021-10-26
数据丢失的问题是不可避免的,但是我们可以尽量减少。
在Redis的配置文件里设置参数
min-slaves-to-write 1
min-slaves-max-lag 10
min-slaves-to-write
默认情况下是0,min-slaves-max-lag
默认情况下是10。
上面的配置的意思是要求至少有1个slave,数据复制和同步的延迟不能超过10秒。如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了。
减小min-slaves-max-lag
参数的值,这样就可以避免在发生故障时大量的数据丢失,一旦发现延迟超过了该值就不会往master中写入数据。
那么对于client,我们可以采取降级措施,将数据暂时写入本地缓存和磁盘中,在一段时间后重新写入master来保证数据不丢失;也可以将数据写入kafka消息队列,隔一段时间去消费kafka中的数据。
1.1.53. Redis哨兵是怎么工作的?
add 2021-10-26
每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个 PING 命令。
如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被当前 Sentinel 标记为主观下线。
如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线 。
当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次 (在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有Master,Slave发送 INFO 命令 )。
若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会变成主观下线。若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
sentinel节点会与其他sentinel节点进行“沟通”,投票选举一个sentinel节点进行故障处理,在从节点中选取一个主节点,其他从节点挂载到新的主节点上自动复制新主节点的数据。
1.1.54. 故障转移时会从剩下的slave选举一个新的master,被选举为master的标准是什么?
add 2021-10-26
如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来,会考虑slave的一些信息。
- 跟master断开连接的时长。 如果一个slave跟master断开连接已经超过了down-after-milliseconds的10倍,外加master宕机的时长,那么slave就被认为不适合选举为master.
( down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
slave优先级。 按照slave优先级进行排序,slave priority越低,优先级就越高
复制offset。 如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高
run id 如果上面两个条件都相同,那么选择一个run id比较小的那个slave。
1.1.55. 同步配置的时候其他哨兵根据什么更新自己的配置呢?
add 2021-10-26
执行切换的那个哨兵,会从要切换到的新master(salve->master)那里得到一个configuration epoch,这就是一个version号,每次切换的version号都必须是唯一的。
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间,然后接替继续执行切换,此时会重新获取一个新的configuration epoch 作为新的version号。
这个version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的,其他的哨兵都是根据版本号的大小来更新自己的master配置的。
1.1.56. Redis cluster中是如何实现数据分布的?这种方式有什么优点?
add 2021-10-26
Redis cluster有固定的16384个hash slot(哈希槽),对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot。
Redis cluster中每个master都会持有部分slot(槽),比如有3个master,那么可能每个master持有5000多个hash slot。
hash slot让node的增加和移除很简单,增加一个master,就将其他master的hash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去。每次增加或减少master节点都是对16384取模,而不是根据master数量,这样原本在老的master上的数据不会因master的新增或减少而找不到。并且增加或减少master时Redis cluster移动hash slot的成本是非常低的。
1.1.57. Redis cluster节点间通信是什么机制?
add 2021-10-26
Redis cluster节点间采取gossip协议进行通信,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更之后U不断地i将元数据发送给其他节点让其他节点进行数据变更。
节点互相之间不断通信,保持整个集群所有节点的数据是完整的。 主要交换故障信息、节点的增加和移除、hash slot信息等。
这种机制的好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;
缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后。
1.1.58. 常见的分布式锁有哪些解决方案?
add 2021-10-26
实现分布式锁目前有三种流行方案,即基于关系型数据库、Redis、ZooKeeper 的方案
1、基于关系型数据库,如MySQL
基于关系型数据库实现分布式锁,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等。
缺点:
- 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
- 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
- 这把锁只能是非阻塞的,因为数据的insert操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
- 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。
2、基于Redis实现
优点:
- Redis 锁实现简单,理解逻辑简单,性能好,可以支撑高并发的获取、释放锁操作。
缺点:
- Redis 容易单点故障,集群部署,并不是强一致性的,锁的不够健壮;
- key 的过期时间设置多少不明确,只能根据实际情况调整;
- 需要自己不断去尝试获取锁,比较消耗性能。
3、基于zookeeper
优点:
- zookeeper 天生设计定位就是分布式协调,强一致性,锁很健壮。如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。
缺点:
- 在高请求高并发下,系统疯狂的加锁释放锁,最后 zk 承受不住这么大的压力可能会存在宕机的风险。
1.1.59. Redis实现分布式锁
add 2021-10-26
分布式锁的三个核心要素
1、加锁
使用setnx来加锁。key是锁的唯一标识,按业务来决定命名,value这里设置为test。
setx key test
当一个线程执行setnx返回1,说明key原本不存在,该线程成功得到了锁;当一个线程执行setnx返回0,说明key已经存在,该线程抢锁失败;
2、解锁
有加锁就得有解锁。当得到的锁的线程执行完任务,需要释放锁,以便其他线程可以进入。释放锁的最简单方式就是执行del指令。
del key
释放锁之后,其他线程就可以继续执行setnx命令来获得锁。
3、锁超时
锁超时知道的是:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程北向进来。
所以,setnx的key必须设置一个超时时间,以保证即使没有被显式释放,这把锁也要在一段时间后自动释放。setnx不支持超时参数,所以需要额外指令,
expire key 30
上述分布式锁存在的问题
通过上述setnx
、del
和expire
实现的分布式锁还是存在着一些问题。
1、SETNX 和 EXPIRE 非原子性
假设一个场景中,某一个线程刚执行setnx,成功得到了锁。此时setnx刚执行成功,还未来得及执行expire命令,节点就挂掉了。此时这把锁就没有设置过期时间,别的线程就再也无法获得该锁。
解决措施:
由于setnx
指令本身是不支持传入超时时间的,而在Redis2.6.12版本上为set
指令增加了可选参数, 用法如下:
SET key value [EX seconds][PX milliseconds] [NX|XX]
- EX second: 设置键的过期时间为second秒;
- PX millisecond:设置键的过期时间为millisecond毫秒;
- NX:只在键不存在时,才对键进行设置操作;
- XX:只在键已经存在时,才对键进行设置操作;
- SET操作完成时,返回OK,否则返回nil。
2、锁误解除
如果线程 A 成功获取到了锁,并且设置了过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁;随后 A 执行完成,线程 A 使用 DEL 命令来释放锁,但此时线程 B 加的锁还没有执行完成,线程 A 实际释放的线程 B 加的锁。
解决办法:
在del释放锁之前加一个判断,验证当前的锁是不是自己加的锁。
具体在加锁的时候把当前线程的id当做value,可生成一个 UUID 标识当前线程,在删除之前验证key对应的value是不是自己线程的id。
还可以使用 lua 脚本做验证标识和解锁操作。
3、超时解锁导致并发
如果线程 A 成功获取锁并设置过期时间 30 秒,但线程 A 执行时间超过了 30 秒,锁过期自动释放,此时线程 B 获取到了锁,线程 A 和线程 B 并发执行。
A、B 两个线程发生并发显然是不被允许的,一般有两种方式解决该问题:
- 将过期时间设置足够长,确保代码逻辑在锁释放之前能够执行完成。
- 为获取锁的线程增加守护线程,为将要过期但未释放的锁增加有效时间。
4、不可重入
当线程在持有锁的情况下再次请求加锁,如果一个锁支持一个线程多次加锁,那么这个锁就是可重入的。如果一个不可重入锁被再次加锁,由于该锁已经被持有,再次加锁会失败。Redis 可通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时释放锁。
5、无法等待锁释放
上述命令执行都是立即返回的,如果客户端可以等待锁释放就无法使用。
- 可以通过客户端轮询的方式解决该问题,当未获取到锁时,等待一段时间重新获取锁,直到成功获取锁或等待超时。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。
- 另一种方式是使用 Redis 的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。
具体实现参考:https://xiaomi-info.github.io/2019/12/17/Redis-distributed-lock/
1.1.60. Redis如何做内存优化?
add 2021-10-26
控制key的数量。当使用Redis存储大量数据时,通常会存在大量键,过多的键同样会消耗大量内存。Redis本质是一个数据结构服务器,它为我们提供多种数据结构,如hash,list,set,zset 等结构。使用Redis时不要进入一个误区,大量使用get/set这样的API,把Redis当成Memcached使用。对于存储相同的数据内容利用Redis的数据结构降低外层键的数量,也可以节省大量内存。
缩减键值对象,降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长度。
- key长度:如在设计键时,在完整描述业务情况下,键值越短越好。
- value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。
编码优化。Redis对外提供了string,list,hash,set,zet等类型,但是Redis内部针对不同类型存在编码的概念,所谓编码就是具体使用哪种底层数据结构来实现。编码不同将直接影响数据的内存占用和读写效率。可参考文章:https://cloud.tencent.com/developer/article/1162213
1.1.61. 如果现在有个读超高并发的系统,用Redis来抗住大部分读请求,你会怎么设计?
add 2021-10-26
如果是读高并发的话,先看读并发的数量级是多少,因为Redis单机的读QPS在万级,每秒几万没问题,使用一主多从+哨兵集群的缓存架构来承载每秒10W+的读并发,主从复制,读写分离。
使用哨兵集群主要是提高缓存架构的可用性,解决单点故障问题。主库负责写,多个从库负责读,支持水平扩容,根据读请求的QPS来决定加多少个Redis从实例。如果读并发继续增加的话,只需要增加Redis从实例就行了。
如果需要缓存1T+的数据,选择Redis cluster模式,每个主节点存一部分数据,假设一个master存32G,那只需要n*32G>=1T,n个这样的master节点就可以支持1T+的海量数据的存储了。
Redis单主的瓶颈不在于读写的并发,而在于内存容量,即使是一主多从也是不能解决该问题,因为一主多从架构下,多个slave的数据和master的完全一样。假如master是10G那slave也只能存10G数据。所以数据量受单主的影响。 而这个时候又需要缓存海量数据,那就必须得有多主了,并且多个主保存的数据还不能一样。Redis官方给出的 Redis cluster 模式完美的解决了这个问题。
以上部分资源来自网络
以上内容来自: