Redis
2024-01-08 11:47:35 2 举报AI智能生成
Redis是一个开源的,基于内存的数据结构存储系统,可以用作数据库、缓存和消息中间件。它支持多种数据结构,如字符串、列表、集合、散列和有序集合,并提供了丰富的操作命令。Redis具有高性能、高可用性和易扩展性的特点,广泛应用于互联网领域。它的设计目标是提供快速、可靠的数据访问服务,以满足大规模数据处理的需求。Redis支持主从复制、分片和事务等高级功能,使得它可以在复杂的应用场景中发挥重要作用。通过使用Redis,开发者可以轻松实现数据的高速读写和高效管理,从而提高应用程序的性能和可扩展性。
极客时间
redis
模版推荐
作者其他创作
大纲/内容
答疑篇
数数组和压缩列表作为底层数据结构的优势是什么?
Redis 基本 IO 模型中还有哪些潜在的性能瓶颈?
AOF 重写过程中有没有其他潜在的阻塞风险?
为什么主从库间的复制不使用 AOF?
在主从切换过程中,客户端能否正常地进行请求操作呢?
replication buffer 和 repl_backlog_buffer 的区别?
哨兵实例是不是越多越好? 增大down-after-milliseconds是否可以减少误判
为什么 Redis 不直接用一个表,把键值对和实例的对应关系记录下来?而使用哈希槽的方式?
Redis 什么时候做 rehash?
缓存雪崩可以使用服务熔断、服务降级、请求限流三种方法来应对,那这三种方法是否可以应对缓存穿透问题?
在执行事务时,如果 Redis 实例发生故障,而 Redis 使用的是 RDB 机制,那么,事务的原子性还能得到保证吗?
假设我们将 min-slaves-to-write 设置为 1,min-slaves-max-lag 设置为 15s,<br>哨兵的 down-after-milliseconds 设置为 10s,哨兵主从切换需要 5s,而主库因为某些原因卡住了 12s。<br>此时,还会发生脑裂吗?主从切换完成后,数据会丢失吗?<br>
为什么官方声称的redis cluster集群规模不建议超过1000?
数据类型
键值对的保存
问题:键值对需求采用String造成内存增加<br>
采用String一组键值对内存占用(16+16+32=64)
dictEntry(本身24,内存分配器分配2^n,所以是32)
key
RedisObject对象头 16bytes
value
RedisObject对象头 16bytes
采用(hash)字典
entry<br>
(String)字符串底层实现
Redis对象头结构体<br>
固定长度16bytes
字符串SDS(Simple Dynamic String)结构体
分配内存空间64bytes时,留给数组内容content的空间大小为64-16(对象头)-3(SDS另外三个字段)-1(内容结尾的\0)
存储方式
int(长度<8)
embstr(44>长度>8)
raw(长度>44)
(hash)字典底层实现
哈希表
结构
扩容时使用渐进式rehash多了一层:dict
真正的hashtable结构:dictht
dictEntry
扩容
渐进式rehash
缩容
只有一个条件:元素个数<数字长度*10%,不考虑是否bgsave
压缩列表
zlbytes:记录整个列表的大小 4bytes
zltail:记录尾结点与开头的偏移量 4bytes
zllen:记录节点数量
entry
1/5 byte pre_len
记录前一个节点的长度,单位:bytes
前一节点长度>254?1byte :5byte
4 byte len
自身长度
1 byte encoding
记录content里保存数据的类型和长度
content
节点的值
zlend:特殊值0xFF,标记压缩列表的末端 1bytes
集合类的使用
聚合统计
set
差集
sdiff key1 [ke2 key3 ……]
交集
sinter key1 [key2 key3 ……]
并集
sunion key1 [key2 key3 ……]
排序统计
List
Sorted Set
分页优先考虑Sorted Set
二值统计
bitmap<br>
<b>getbit</b> key offset
<b>setbit</b> key offset 0/1
bitcount key [start end]
功能:统计1的个数
场景:统计签到了多少天
bitpos key [start end] 0/1
功能:查找指定范围内第一个0/1
场景:第1次签到的时间
bittop
功能:按位与
场景:连续签到统计
基数统计
set
千万级别时内存占用太大
hash
千万级别时内存占用太大
HyperLogLog
pfadd key value [value2 ……]
pfcount key
pfmerge newkey [oldkey1 oldkey2 ……]
统计总结
1、集群模式使用多个key聚合的命令,由于key分布在不同的实例上,多个实例无法做聚合运算
2、建议把统计数据与业务数据拆分开,实例单独部署,防止做统计操作时影响业务
GEO
<b>GeoHash算法:</b>把二维数组映射到一维数组
1、对经度和维度分别编码
2、把经纬度编码合成一个编码
底层实现:zset
命令
添加数据
<b>geoadd </b>集合key 精度 纬度 key
计算距离
<b>geodist</b> 集合key key1 key2 km/m/ml/ft
获取经纬度
<b>geppos</b> 集合key key1 [key2 ……]
获取geohash编码
<b>geohash</b> 集合key key
获取附近元素
<b>georadiubymenber</b> 集合key key number km/m [withcoord] [withdist] [withhash] [count n desc]
注意事项
1、数据量过大会导致redis阻塞
2、建议Geo单独部署,不要使用集群环境
3、对Geo数据按国家、省、市、区进行拆分,降低单个zset集合的大小
消息队列解决方案
要求
消息顺序
幂等性
保证消息可靠性
基于List的解决方案
基本命令
LPUSH key value1 [value2]
BRPOP key1 [key2] timeout
BRPOPLPUSH source targetlist timeout
缺点:无法多消费者同时消费
基于stream的解决方案
影响Redis性能的5大因素
如何避免主线程阻塞
交互对象中的操作
客户端
网络IO
Redis采用多路复用,不会导致阻塞
复杂度高的增删改查操作
操作复杂度为O(N)的操作:比如集合元素的全量查询、聚合统计(交集、并集、差集)
bigkey的删除操作
清空数据库(FLUSHDB、FLUSHALL)
磁盘
AOF日志同步写(注意只是这条写回策略)
主从节点
从库接收RDB文件后会FLUSHDB,见第三条
从库清空数据库后要把RDB加载到内存
切片集群
Redis Cluster方案,实例增删时,数据在不同的实例间迁移,遇到bigkey会阻塞
异步的子线程机制
Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由它们负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行。
采用子线程的场景
惰性删除(lazy-free)
配置参数
“尝试”异步释放什么时候会变成真正异步执行
a)当Hash/Set底层采用哈希表存储(非ziplist/int编码存储)时,并且元素数量超过64个
b) 当ZSet底层采用跳表存储(非ziplist编码存储)时,并且元素数量超过64个
c) 当List链表节点数量超过64个(注意,不是元素数量,而是链表节点的数量,List的实现是在每个节点包含了若干个元素的数据,这些元素采用ziplist存储)
AOF配制成everysec
Redis和CUP
cpu
CPU Socket
物理核1(共享L3缓存)
逻辑核1(共享L1、L2缓存)
逻辑核2(共享L1、L2缓存)
物理核2(共享L3缓存)
……
CPU Socket
……
cpu多核(NUMA)对Redis性能的影响
问题:Redis实例被频繁调度到不同的核上运行,就需要重新从L3缓存甚至内存加载数据
解决方案:绑核
1、把Redis实例核CPU的核绑定
2、网络中断程序绑核时和Redis绑定在同一个CPU Socket
绑核的风险和解决方案
1、一个Redis绑定在一个物理核上(两个逻辑核)
2、优化Redis源码【打扰了】
关于Redis变慢
如何判断Redis是否变慢
1、查看Redis响应延迟
2、基于当前环境基线性能判断,如果运行延迟是极限性能的2倍以上,就是变慢了
病因分析
自身操作特性
1、慢查询命令
产生原因:
Set、List等集合类型的复杂操作
解决办法:
1、通过slowlog命令或者latency monitor工具找到变慢的请求
2、使用SSCAN多次迭代返回需要的数据,排序、交集、并集等操作放到客户端完成
2、过期key操作
产生原因:
大量key同时过期,Redis删除操作是阻塞的
解决办法:
避免同时过期
问题:哪些命令可以代替keys
SCAN<br>
Rehash时不会漏key
缩容会导致重复key
HSCAN/SSCAN/ZSCAN
Hash/Set/Sorted Set在元素较少底层采用intset/ziplist存储时,会无视$count直接返回所有数据
文件系统
AOF重写导致IO压力阻塞落盘的fsync
产生原因:
1、AOF落盘策略配置为always时,不会使用后台子线程来落盘,此时是同步操作
2、如果遇到AOF重写,造成磁盘IO过大,fsync被阻塞,此时主线程写入新请求需要AOF落盘,发现上次fsync还没返回,就会被阻塞
解决办法:
1、业务允许数据丢失的情况下
2、使用固态硬盘
操作系统
内存swap
产生原因:
系统内存不足
解决办法:
增加机器内存/使用Redis分片集群/Redis迁移到单独的机器运行
排查办法:
内存大页
会产生的问题:
AOF重写和RDB生成时,由于<b>写时复制机制</b>,在内存大页时会导致大量拷贝,影响Redis正常的内存访问操作,导致变慢
排查办法:
cat /sys/kernel/mm/transparent_hugepage/enabled
解决办法
echo never /sys/kernel/mm/transparent_hugepage/enabled
变慢时常用checklist
获取当前环境基线性能
是否有慢查询命令
是否有key同时过期
是否存在bigkey
Redis AOF(appendfsync)配置级别是什么
Redis实例内存是否过大?有没有发生swap
Redis实例运行环境是否开启了内存大页机制
主从集群主库实例数据量大小是否控制在2-4G,避免主从复制从库加在过大的RDB文件而阻塞
是否使用了多核CPU或者NUMA架构的机器运行Redis实例
内存碎片
产生原因:
1、本身内存分配为了减少分配次数,都是按固定大小划分内存空间,就会有冗余
2、删除键值对,释放内存,内存如果够大会被下次分配给新键值对,直到剩下一部分不够分配给新键值对了,就成了永远没法使用的碎片
查看碎片率
info memory 查看mem_fragmentation_ratio
应对经验:
1、1到1.5是合理的
2、大于1.5可以考虑采取措施降低碎片率
小于1说明Redis使用swap了,会导致性能下降
清理内存碎片
1、重启(基本不可取)
2、4.0rc版本以后
第一步:启动自动内存碎片清理
第二步:配置自动内存清理的条件,同时满足才会开始清理
第三部:设置两个参数控制清理操作占用的CPU时间比例的上下限
缓冲区
客户端——服务端
避免客户端和服务器端发送处理速度不匹配,<b>服务器端</b>为<b>每个客户端</b>都设置了一个输入缓冲区和输出缓冲区
输入缓冲区:
溢出原因:
1、写入了bigkey直接导致溢出
2、服务器端处理速度过慢,如:主线程出现了间限性阻塞
查看输入缓冲区内存:client list
溢出危害
1、占用内存总量超过maxmemory配置项,触发数据淘汰,淘汰不了直接oom
2、单个客户端溢出(超过1G)会被直接关闭
解决办法
1、调大缓冲区,Redis客户端输入缓冲区上限1G,代码写死,无法调整【行不通】
2、数据发送上避免bigkey,数据处理上避免主线程阻塞
输出缓冲区:
构成
1、大小为16k的固定缓冲空间,暂存ok响应和出错信息
2、可以动态增加的缓冲区
导致溢出的原因:
1、服务器端返回bigkey的大量结果
2、执行了MONITOR命令
3、缓冲区大小设置不合理
解决办法
1、生产禁止持续使用MONITOR
2、设置缓冲区大小的上限阈值、设置输出缓冲区持续写入数据的数量和时间阈值
普通客户端:阻塞式发送,可以对缓冲区不作限制
订阅客户端:不属于阻塞式发送,需要设置缓冲区大小限制
主从集群
全量同步:缓冲区replication buffer
作用:主节点给从节点同步RDB文件时,新增的写命令都保存在从节点的输出缓冲区,等RDB传输完,再发送给从节点同步
溢出危害
发生溢出,主节点会立即关闭和从节点进行复制的连接,导致全量复制失败
防止溢出
1、控制主节点数据量大小(2-4G),让RDB文件不会太大
2、根据主节点数据量大小、写负载压力、本身内存设置复制缓冲区大小
3、主节点复制缓冲区的内存开销是每个从节点客户端输出缓冲区占用内存的总和,所以需要控制主从连接节点的个数
增量同步:复制积压缓冲区 repl_backlog_size
作用:主库会记录自己写到的位置,每一个从库都有自己读到的位置,重连之后,如果从库读到的位置未被覆盖,就做增量同步
溢出危害:从库读的位置被覆盖,从而导致从库需要重新全量同步
解决办法:repl_backlog_size合理设置参数的值
总结
溢出导致的问题
1、网络连接关闭:针对输入输出缓冲区
2、命令数据丢失:针对复制积压缓冲区
防止溢出解决方案
1、命令数据过大:普通客户端避免bigkey,全量同步输出缓冲区避免RDB过大
2、数据处理较慢:介绍Redis主线程阻塞操作
3、缓冲区过小:client-output-buffer-limit设置合理的输出缓冲区,输入缓冲区无解
使用redis做只读、和读写缓存的区别
数据库和缓存一致性解决方式
最终一致性方案
分布式事务方案
从双写一致性问题来说,不管是数据库还是缓存的先后都可能不一致<br>同步策略 中最常用的还是先写库后删缓存。<br>异步策略可以采用解耦,将修改缓存放入到队列,但维护逻辑会更加复杂
缓存最大值配置及淘汰策略(目前共7种淘汰)<br>noeviction 代表不淘汰,添加数据会报错<br>
使用如下命令设置缓存的最大值,超过后就会触发淘汰机制了<br>CONFIG SET maxmemory 4gb<br>
在设置了过期时间的数据中进行淘汰,包括 volatile-random、volatile-ttl、volatile-lru、volatile-lfu
在所有数据范围内进行淘汰,包括 allkeys-lru、allkeys-random、allkeys-lfu
秒杀场景中的应用
秒杀场景下,库存扣减操作不能实时使用数据库来操作,可以想想为什么?
秒杀要点:
1. 拦截住大部分的请求,只有少量请求实际到达后端
2. 前端页面CDN 静态化处理
3.库存信息过期时间处理,为防止过期突然请求到数据库<br>导致缓存击穿,不要给库存设置过期时间<br>
4.数据库处理订单异常,库存已扣减,但订单处理异常的情况下,需要增加<br>订单重试功能,保证订单最终能成功处理,保障一致性
方式1:使用lua脚本,将查询库存余量,判断,扣减操作封装为一个脚本
方式2:使用分布式锁
基础概念
键值数据库的基本功能,以simpleKV推到redis
一个键值数据库包括了<b>访问框架</b>、<b>索引模块</b>、<b>操作模块</b>和<b>存储</b>模块四部分
访问
(redis、memcache)通过网络框架以 Socket 通信的形式对外提供键值对操作
(rocksDB)通过函数库调用的方式供外部应用使用
索引
索引的作用是让键值数据库根据 key 找到相应 value 的存储位置,进而执行操作<br>引出索引数据结构......
Hash
B+树
字典树
跳表
操作逻辑
GET/SCAN,根据 value 的存储位置返回 value 值即可
PUT,需要为该键值对分配内存空间
DELETE需要删除键值对,并释放相应的内存空间,这个过程由分配器完成
存储
redis内存分配器提供了多种选择
redis的持久化
AOF
RDB
simpleKV还缺少的模块:集群横向扩展、数据分片存储、内存过载的淘汰策略等等。。。
redis的数据结构
redis快的原因
内存数据库
hash表、跳表等高效数据结构
io多路复用
数据结构和底层数据对应关系
渐进式rehash
什么时候做rehash
1、5>装载因子>=1,且没有进行RDB生成或者AOF重写
2、装载因子>=5
整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,<br>那为什么 Redis 还会把它们作为底层数据结构呢?<br>
redis的高性能IO模型
redis真的只是单线程吗?
只是在<b>网络IO</b>和<b>键值读写</b>上来说是单线程的。<br><b>持久化,集群同步、淘汰</b>等是由其他线程完成的<br>
单线程为什么这么快?
IO多路复用有时间仔细看一下
造成redis性能瓶颈的原因
1、(主线程)耗时的操作
a、操作bigkey:写入bigkey分配内存已经删除时释放内存是耗时操作
b、复杂度过高的命令:范围查询,并且查询的数据量大
c、大量key集中过期
d、执行淘汰策略:内存不够时需要淘汰key,耗时长
e、AOF刷盘开启always模式:会导致频繁写入磁盘,写磁盘是个慢操作
f、生成RDB:fork子进程生成数据快照时,如果实例过大,也会阻塞
2、并发量大,单线程读写IO瓶颈
redis高可靠
数据尽量少丢失
AOF日志(Append Only File)【appendonly yes 开启】
对比数据库的(write ahead log)WAL
记录的是redis收到的每一条命令
子主题
为什么先写内存后记录日志?
避免进行语法的检查
不会阻塞当前的写操作。
两个风险
1、AOF文件过大,导致写入变慢,阻塞线程
AOF重写机制
重写不会阻塞主线程,fork出<b style="">bgrewriteaof</b>线程完成
一个拷贝,两处日志
fork时会把主线程内存页表拷贝一份给bgrewriteaof子进程
新的操作会写入<b>原AOF日志</b>和<b>AOF重写日志</b>的缓冲区,等到根据拷贝重写完成,再把缓冲区的写入重写AOF,然后替代旧的AOF
AOF日志重写时会有哪些风险
a、fork拷贝内存页表阻塞整个进程
b.过程中如果有操作bigkey,开启了Huge Page机制,也会阻塞
2、写完内存之后AOF还没落盘就宕机导致数据丢失
写回策略
Always
同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
Everysec
每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘
No
操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
缺点
恢复时较慢,需要一条一条的执行命令
RDB(Redis DateBase)快照【默认开启】
记录某一时刻,redis中的<b>所有内容(全量快照)</b>,数据恢复时很快。
save和bgsave
save在主线程中执行,会导致阻塞
bgsave会fork一个子进程,专门用于写入RDB。避免主线程的阻塞,也是RDB文件生成的默认值,
最佳实践
redis 4.0提出混合使用AOF日志和内存快照
服务尽量少中断
集群数据同步
主从库模式
全量同步
在从库执行【replicaof 主库ip 主库端口】 指定要同步的主库
阶段一:
从库给主库发送【psync 主库runID 复制进度offset】
主库用【FULLRESYNC {runID} {offset}】响应,表示采用全量复制
阶段二:
主库执行bgsave生成RDB文件,然后发送给从库
从库接收RDB文件后,清空当前数据库,然后加载RDB文件
阶段三
<b>replication buffer</b>记录主库RDB文件生成后收到的写操作<br>
把replication buffer中的修改操作发给从库,从库重新执行这些操作
主从级联模式分担全量复制时的压力
基于长连接的命令传播<br>
网络断连或者阻塞之后就会有下面的增量同步
增量同步
replication buffer
通过client-output-buffer-limit限制buffer大小
repl_backlog_buffer
重连之后,从库根据自己的slave_repl_offset和主库的master_repl_offset决定全量同步还是增量同步
repl_backlog_size 参数需要适当增大,以减少全量同步
哨兵,一种特殊的Redis进程
监控
如何监控:周期性地发送ping给各个主从库
减少误判
主观下线:如果在down-after-milliseconds配置的时间内未回复或pong错误,则判断“主观下线”
客观下线:多实例的哨兵集群,如果超过一半/(配置文件quorum值)判断“主观下线”,那么就最终判定为“客观下线”,开始下一步选主
选主
筛选
配置项down-after-milliseconds 主从库断连的最大连接时间,如果发生次数超过了10次,就被筛掉
打分
1、优先级最高的从库得分
配置项slave-priority可以给不同的从库设置不同的优先级,某个从库优先级最高就直接是新主库
2、和旧主库同步程度最接近的从库得高分
根据repl_backlog_buffer中的slave_repl_offset最接近master_repl_offset的就是新主库
3、ID号最小的从库高分
实在上面两点都一样的情况下,ID号最小的从库会被选为新主库
通知
选主流程结束后,使用通知告知各节点,利用replicaof 形成新的主从关系
哨兵集群
建立连接
sentinel——主库
配置文件配置主库信息,以和主库建立联系
sentinel——sentinel
基于pub/sub(发布订阅)机制,在主库上发现其他sentinel的信息并建立连接
sentinel——从库
每个sentinel都向主库发送INFO命令,主库就会把从库列表返回给哨兵
sentinel——客户端
客户端通过【SUBSCRIBE */{具体事件名}】订阅事件,了解比如主从切换的进度
主库下线事件
+sdown(实例进入“主观下线”状态)
-sdown(实例退出“主观下线”状态)
+odown(实例进入“客观下线”状态)
-down(实例退出“客观下线”状态)
从库重新配置事件
+salve-reconf-sent(哨兵发送SLAVEOF命令重新配置从库)
+slave-reconf-inprog(从库配制了新主库,单尚未进行同步)
+slave-reconf-done(从库配制了新主库,完成了同步)
新主库切换
+switch-master(主库地址发生变化)
选leader执行主从切换
1、从“主观下线”到确认“客观下线”
sentinel实例判断主库“主观下线”后,给其他实例发送is-master-down-by-addr命令,其他实例回复Y/N,当收到的票数大于quorum时,此实例会标记主库为“客观下线”
子主题
2、确认“客观下线”后,进行leader选举
此sentinel实例标记主库为“客观下线”后,马上发起leader选举,并给自己投一票,此时每个收到信息,并且未投票的sentinel实例,都会给他投票。满足两个条件1、拿到半数以上赞成票2、拿到的票数大于等于配置文件中的quorum时,就可以成为leader
切片集群(数据分片存储)
redis数据量增大
1、 纵向扩展
方式:使用大内存主机
优点:实施起来简单、直接
缺点
1、主线程fork子线程时由于内存太大会阻塞
2、受到硬件和成本的限制
2、横向扩展
方式:切片集群
问题
1、数据在多个实例如何分布
2、客户端如何找到数据所在实例
Redis Cluster方案<br>
数据切片和实例如何对应
自动分配哈希槽
<b>cluster create</b>创建集群,槽会自动平均分布在集群实例上
手动分配哈希槽
<b>cluster meet</b>命令手动建立实例间的链接
<b>cluster addslots</b>指定每个实例上的哈希槽个数
手动分配时需要把16384个槽都分完,否则Redis集群无法正常工作
客户端定位数据
数据不变时
1、Redis实例会把自己的哈希槽信息发给相连的其他实例(每个实例都会有全部的哈希槽信息)
2、客户端会把哈希槽信息缓存本地,客户端请求时会计算对应的hash槽然后给相应实例发送请求
数据变化时<br>
重定向机制
1、客户端给实例发送数据读写操作,实例没有相应数据
2、实例返回MOVE命令响应结果,里面包含了新实例地址
3、客户端向新地址发送请求,同时更新本地缓存
ASK机制
1、客户端给实例发送数据读写操作,实例里没有相应数据,且实例数据迁移一半,未完成迁移
2、实例返回ASK报错信息,指向新实例地址
3、客户端给新实例发送ASKING信息,让实例允许执行接下来的操作,然后再发送操作命令
4、此次操作不会更新客户端本地缓存
Codis方案
Twenproxy方案
不支持在线扩容
实践篇下
缓存异常
缓存与数据库不一致
对于数据新增不会存在该问题,只发生在数据删改时,此时<b>需要修改数据库,删除缓存</b>
由于分两步操作,总会有失败的可能性,为了保证数据库是最新,通常采用先修改数据库,后删除缓存<br><b>(个人认为是否删除缓存)可以视情况而定,如果缓存逻辑简单,不需要计算,也是可以直接修改缓存的</b>
由于缓存删除失败会导致数据不一致,可以将删除操作解耦,放入消息队列进行重试,一定次数后预警;
并发情况下的不一致
在并发情况下,哪怕两个操作都成功了,也可能因为其他线程的读取顺序问题导致不一致
<b>延迟双删策略</b>,先删除,然后操作数据库,等待一小段时间(大于一个redis读取时间均值),<br>再次删除缓存。该时间是为了将错误读取的数据再次删除。<br>可以减少几率,但仍有可能不一致<br>
<b>分布式锁,用于解决写并发:<br>(</b>写+写并发:线程A和线程B同时更新同一条数据,更新数据库的顺序是先A后B,但更新缓存时顺序是先B后A,这会导致数据库和缓存的不一致<b>),<br>同一时间只允许一个线程修改库和更新缓存。</b><br>
缓存穿透
缓存中不存在,恶意的频繁访问,穿过缓存读数据
返回空值或缺省值
使用布隆过滤器快速判断数据是否存在,避免从数据库中查询数据是否存在,减轻数据库压力
缓存击穿
热点数据的频繁访问,突然过期;
解决方法:热点数据不设置过期时间,或者取数据时使用分布式锁,同一时间只有一个线程读数据
缓存雪崩
大量键值同一时间过期导致
避免大量设置相同的过期时间,可以在固定的基础上加一个随机时间
使用服务降级来应对雪崩,例如非重要情况直接返回,重要的任保留
缓存实例故障导致
此时缓存请求会全部落到数据库上,应对方案是采取服务熔断,或者限流。尝试恢复缓存实例
做好应对方案,多实例部署
redis的LRU和LFU
LRU最近最久未使用
Redis 是用 RedisObject 结构来保存数据的,RedisObject 结构中设置了一个 lru 字段,用来记录数据的访问时间戳
redis并没有使用链表来维护LRU,而是简单的在RedisObject实体中维护lru字段,根据时间戳大小来淘汰数据
当触发缓存淘汰时,使用的是随机抽样,在选中的key中,淘汰掉 lru 时间戳最小的
由于LRU只根据判断时间,当遇到扫描式的单词查询时,效果不佳。
LFU最近最少使用,在LRU的基础上<br>添加了访问次数判断。<br>
LFU会先根据访问次数判断,淘汰次数最少的,当次数相同时,淘汰掉时间更小的<br>
Redis 在实现 LFU 策略的时候,只是把原来 24bit 大小的 lru 字段,又进一步拆分成了两部分。<br>其中时间戳占16位,访问次数占8位(最大255)
当 LFU 策略筛选数据时,Redis 会在候选集合中,根据数据 lru 字段的后 8bit 选择访问次数最少的数据进行淘汰。<br>当访问次数相同时,再根据 lru 字段的前 16bit 值大小,选择访问时间最久远的数据进行淘汰<br>
由于访问次数只有8位,最大值也就255,所以redis并不是每访问一次+1,而是根据配置项<br><b>lfu_log_factor</b> 指数来调整,<br>
并发访问控制
无锁的原子操作
将多个命令合为一个执行(也称单命令操作);如increase,decrease等<br>当需要对类似库存值的属性进行增减时,INCR/DECR 命令可以直接作为单命令运行
将多个操作编写到 lua 脚本中,已脚本为单位执行(疑问:脚本是在单机器上运行还是多机器?)
lua 脚本尽量只编写通用的逻辑代码,避免直接写死变量。变量通过外部调用方传递进来,这样 lua 脚本的可复用度更高
建议先使用SCRIPT LOAD命令把 lua 脚本加载到 Redis 中,然后得到一个脚本唯一摘要值,<br>再通过EVALSHA命令 + 脚本摘要值来执行脚本,这样可以避免每次发送脚本内容到 Redis,减少网络开销。<br>
例如需要限制客户端一分钟内的访问次数,可以使用 ip 作为key,次数作为value;每访问一次 value+1<br>由于 lua 脚本是原子性的,第一次执行时,设置好过期时间为60s,<br>后续在增减操作中判断是否超过阈值。限制客户端的访问<br>
执行脚本示例:<br>redis-cli --eval lua.script keys , args<br>
单命令操作的适用范围较小,lua适用范围更广
分布式锁
单节点实现分布式锁:<br>
加锁:SETNX命令,该命令在执行时会判断键值对是否存在,如果不存在,<br>就设置键值对的值,如果存在,就不做任何设置
解锁: 执行完业务逻辑后,使用 DEL 命令删除锁变量。<br>这里存在删除失败的场景,所以需要设置过期时间,以防止死锁出现;<br>过期时间需要根据业务操作时间来确定,需要设置的稍大一些
多节点实现分布式锁, RedLock思想<br>(单节点下,宕机后会导致锁失败),<br>基于java的实现称为Redission<br>
让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例<br>成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁了,否则加锁失败。<br>
redlock 加锁详细步骤(释放同理)
基于 Redis 使用分布锁的注意点:
1、使用 SET $lock_key $unique_val EX $second NX 命令保证加锁原子性,并为锁设置过期时间
2、锁的过期时间要提前评估好,要大于操作共享资源的时间
3、每个线程加锁时设置随机值,释放锁时判断是否和加锁设置的值一致,防止自己的锁被别人释放
4、释放锁时使用 Lua 脚本,保证操作的原子性
5、基于多个节点的 Redlock,加锁时超过半数节点操作成功,并且获取锁的耗时没有超过锁的有效时间才算加锁成功
6、Redlock 释放锁时,要对所有节点释放(即使某个节点加锁失败了),因为加锁时可能发生服务端加锁成功,由于网络问题,给客户端回复网络包失败的情况,所以需要把所有节点可能存的锁都释放掉
7、使用 Redlock 时要避免机器时钟发生跳跃,需要运维来保证,对运维有一定要求,否则可能会导致 Redlock 失效。例如共 3 个节点,线程 A 操作 2 个节点加锁成功,但其中 1 个节点机器时钟发生跳跃,锁提前过期,线程 B 正好在另外 2 个节点也加锁成功,此时 Redlock 相当于失效了
8、如果为了效率,使用基于单个 Redis 节点的分布式锁即可,此方案缺点是允许锁偶尔失效,优点是简单效率高
9、如果是为了正确性,业务对于结果要求非常严格,建议使用 Redlock,但缺点是使用比较重,部署成本高
延迟重启场景
概念分析
事务
MULTI、EXEC命令,前者用于声明事务开启,在MULTI之后声明的都会加到命令队列中 ,<br>后者用于提交命令,将整个队列一起执行。<br>
,Redis 中并没有提供回滚机制。虽然 Redis 提供了 DISCARD 命令,但是,这个命令<br>只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果<br>
原子性
在执行 EXEC 命令前,客户端发送的操作命令本身就有错误(如语法错误)。不执行,<b>保证原子性</b>
事务操作入队时,命令和操作的数据类型不匹配,但 Redis 实例没有检查出错误。<b>不保证,正确的那个命令会执行。</b>
在执行事务的 EXEC 命令时,Redis 实例发生了故障,导致事务执行失败。<br>如果 Redis 开启了 AOF 日志,那么,只会有部分的事务操作被记录到 AOF 日志中。我们需要使用 redis-check-aof <br>工具检查 AOF 日志文件,这个工具可以把未完成的事务操作从 AOF 文件中去除,<b>此时可以保证原子性</b><br>
一致性可以保证,(不管是否开启了持久化)
隔离性
并发操作在 EXEC 命令前执行,此时,隔离性的保证要使用 WATCH 机制来实现,否则隔离性无法保证;
并发操作在 EXEC 命令后执行,此时,隔离性可以保证
持久性,无法保证。AOF 和 RDB 两种策略都存在数据丢失情况
主从不一致问题的原因及解决方式
读到过期数据,过期淘汰策略问题
定期删除策略
惰性淘汰策略(访问时才进行淘汰,但新版本已经不会影响,已过期的会返回空)
EXPIRE 和 PEXPIRE(设置一段时间后过期,遇到主从全量复制时,会导致从库的过期时间延后)
EXPIREAT 和 PEXPIREAT(设置在指定时间点过期,没有上述问题,业务上需要计算时间,且主从强依赖时间同步。)
同步方式为异步,因网络延迟或其他阻塞命令导致延迟
保证网络连接,减小延迟(废话)
使用外部程序来监控主从库间的复制进度
配置不合理导致
protected-mode 设置为了 yes 导致哨兵间无法通信。改为no 即可
cluster_node_timeout 心跳检测超时时间过小,导致宕机误判断。设置为10~20s即可
解决脑裂现象,脑裂的出现原因示主节点 <b>阻塞 </b>导致“假故障”(请回想阻塞原因有哪些),在执行主从<b>切换过程中</b>又恢复了。<br><b>在切换完成之前</b>,原主节点依旧在接收读写操作,<b>当切换完成后</b>,原主节点会由于执行 slave of 成为从节点;<br>导致数据全量同步,丢失切换前的数据。<b>避免脑裂方式为:限制切换过程中,主节点不能接收客户端的请求。减小脑裂几率</b>
min-slaves-to-write:这个配置项设置了主库能进行数据同步的最少从库数量;
min-slaves-max-lag:这个配置项设置了主从库间进行数据复制时,从库给主库发送 ACK 消息的最大延迟(以秒为单位)
将两者结合起来使用,至少 X 个从库能在 Y 秒时间内和主库通信,否则,主节点停止接收客户端的请求
集群方案选择-codis和redis cluster的优劣分析
codis
codis由四部分组成,codis-server、codis-proxy、codis-dashboard、codis-fe
server为普通的redis实例,proxy接收客户端请求,并把请求转发给 codis server<br>dashboard和fe共同组成集群管理工具,fe属于web界面操作。dashboard提供后台支持<br>增删 codis server、codis proxy 和进行数据迁移。<br>
<b>codis-proxy自身实现了RESP交互协议,客户端连接单实例和集群是一样的,不需要切换</b>
集群数据分布
Codis 集群一共有 1024 个 Slot,可以手动分配,默认情况下,dashboard会均分到各个codis-server上
当客户端要读写数据时,会使用 CRC32 算法计算数据 key 的哈希值,并把这个哈希值对 1024 取模。<br>而取模后的值,则对应 Slot 的编号,找到对应的codis-server<br>
slot 和codis-server的对应关系称为路由表,codis-proxy会缓存一份,同时也会存放一份在zookeeper中。<br>当接收到请求后,根据路由表转发到对应的codis-server<br>
当数据位置发生变化时(例如有实例增减),路由表被修改了,codis dashbaord 就会把修改后的路由表发送给 codis proxy
扩容:增加 codis server (实例)和增加 codis proxy(转发代理)
迁移过程中,以codis的slot为单位,将需要迁移的slot上的key,一个一个的迁移<br>为提升迁移效率,可以通过异步迁移命令 SLOTSMGRTTAGSLOT-ASYNC 的参数 numkeys 设置每次迁移的 key 数量。<br>
bigkey会转化成指令,例如一个1w条记录的List,会转成1w个 LPUSH/RPUSH 指令,<br>同时完全迁移完成之前,添加一个过期时间,完成后再取消该时间。避免中途连接断开导致的问题<br>
redis cluster
1. Codis 应用得比较早,成熟度更高,如果你想选择一个成熟稳定的方案,Codis 更加合适些
2. 如果你的业务应用中大量使用了单实例的客户端,而现在想应用切片集群的话,建议你选择 Codis,这样可以避免修改业务应用中的客户端
3. codis是基于redis 3.2.8开发的,Codis 并不支持 Redis 后续的开源版本中的新增命令和数据类型<br>比如 BITOP、BLPOP、BRPOP,以及和与事务相关的 MUTLI、EXEC 等命令<br>
4. 从数据迁移性能维度来看,Codis 能支持异步迁移,异步迁移对集群处理正常请求的性能影响要比使用同步迁移的小。<br>所以,如果你在应用集群时,数据迁移比较频繁的话,Codis 是个更合适的选择<br>
评论
0 条评论
下一页