文章目录
- Redis
-
- Redis存储模型
- 缓存击穿,雪崩,穿透,数据一致性
- 持久化
- 高性能IO模型
-
- [Redis 主线程(6.0 前)串行处理所有命令,避免了锁竞争和线程切换的开销。所有操作天然具有原子性,例如 INCR 命令无需额外同步机制。](#Redis 主线程(6.0 前)串行处理所有命令,避免了锁竞争和线程切换的开销。所有操作天然具有原子性,例如 INCR 命令无需额外同步机制。)
- redis为什么不用多线程:多线程面临的共享资源的并发访问控制问题(存在死锁、线程上下文切换)
- redis网络IO和键值对读写是单线程完成的,持久化,异步删除,集群数据同步是额外线程实行的
- 后台线程:用来执行耗时操作,例如释放持久化过程的临时文件,bgsave持久化,AOF重写刷磁盘,大对象的清理
- redis异步机制
- Redis主从
- redis事务,并发
- 内存管理
Redis
Redis存储模型
内存数据库,所有操作都在内存上完成,内存的访问速度快
键和值的组织结构
-
全局哈希表
-
哈希表由一个数组实现,数组上的每一个元素被称为哈希桶entry,存储了key和value的指针以及next指针,指向哈希冲突的entry
-
Hash冲突:两个 key 的哈希值和哈希桶计算对应关系时,正好落在了同一个哈希桶中,链式Hash解决
-
reHash操作
-
Hash冲突过多时查找效率变低,开始reHash
-
reHash:给第二个全局Hash表分配空间,创建更大的Hash表,并采用渐进式Hash每次将一个Hash桶拷贝并分散到第二个Hash表中
-
迁移过程中两个表一起用
value数据结构的底层实现结构
-
SDS简单动态字符串:动态扩展和缩减的字符串类型
-
整数数组
- 数组下标访问的数组,intset只存储数字和能转为数字的字符串
-
双向链表
- 双向的,链表指针逐个访问,记录了表头表尾偏移量
-
压缩列表
- 类似于一个数组,但是表头存储了长度,列表尾和偏移量,查找第一个或最后一个元素很快
-
哈希表
- 哈希表的操作特点就是上述讲的
-
跳表
-
有序链表查找很慢,所以在有序链表基础上增加了多层索引形成跳表
-
通过索引位置的几个跳转,实现数据的快速定位
-
内存友好型,redis基于内存,不用B+树的复杂页分裂合并计算,层级高也能很快的扫描
-
redis操作
-
redis原子性操作
-
MYLTI:表示原子性操作开始,将后续命令放入内部队列,一起执行
-
EXEC原子性操作结束,收到命令表示操作入队完整
-
-
单元素操作是基础
- 每一种集合类型对单个数据实现的增删改查操作,基本O(1)时间复杂度(HGET、HSET 和 HDEL,SADD、SREM、SRANDMEMBER),HMGET这类操作m个元素时间复杂度O(m)
-
范围操作非常耗时
-
集合类型中的遍历操作,可以返回集合中的所有数据,这类操作很耗时,尽量避免(HGETALL,SMEMBERS,LRANGE,ZRANGE)
-
redis提供了SCAN命令,以及针对集合提供的SSCAN,HSCAN,避免使用KEYS返回所有key,用SCAN设置参数,可以返回指定数据量的条件数据
-
-
统计操作通常高效
-
集合类型对集合中所有元素个数的记录(LLEN 和 SCARD)
-
聚合统计:交并补场景
-
通过两个set集合实现,计算集合间的操作
-
数据量过大会阻塞redis主线程,可以让一个从库专门去运算
-
-
排序统计
-
redis中SortedSet和List为有序集合
-
List:插入顺序有序
-
SortedSet:权重值排序
-
-
-
二值状态统计
-
使用bitmap实现签到系统
-
将某个特殊键的value的偏移量位k-1的位设置为1,表示第k天签到了,bitmap可统计1数量
-
1亿用户10天签到
-
每日日期作为key,1亿长度bitmap
-
bitmap中对应每个用户当天签到情况
-
bitmap与操作就是连续签到用户
-
-
-
-
基数统计
- 集合不重复元素个数:Set天然去重(Hash的key属性独特也可以),SCARD即可,数据量过大可以使用HyperLogLog(近似值)
-
-
例外情况只有几个
- 压缩列表和双向链表都会记录表头和表尾的偏移量,LPOP、RPOP、LPUSH、RPUSH较为快速
五大基本数据类型
-
String字符串
-
使用简单动态字符串,内存开销大
-
String结构(value)
-
Simple Dynamic String结构
-
buf:字节数组,保存实际数据
-
alloc:实际分配长度;len已用长度
-
-
RedisObject
-
Redis 内部用于管理各种数据类型的通用结构,redis的key也是如此封装的,ptr指向SDS,SDS存储的是key字符串(哈希表中存储的也是RedisObject)
-
元数据:存储引用时间,次数等
-
ptr指针,指向实际数据
-
存储若为8Byte的Long类型,则直接存在ptr
-
存储的若为字符串小于44B则和SDS存在同一空间,否则分开存储,ptr指向SDS,减少内存碎片
-
-
-
-
-
List列表
-
双向链表+压缩列表实现,也可作为栈和队列的实现,内部元素可重复Map<String,List>可用于 FIFO 队列场景,不能作为随机存取集合
-
元素少时用压缩列表,是一块内存区域
-
元素多时用双向链表实现
-
-
实现消息队列:LPUSH,RPOP实现消息的生产消费(RPOP阻塞式,防止队列无数据),不推荐,消息可能会过多,无上限 Redis5.0提供的Streams也可以作消息队列,与RabbitMQ相比更轻量
-
-
Hash散列
-
压缩链表+哈希表实现,Map<String,Map<String,String>>
- 哈希中字段数量少且小,用压缩链表,大用HashTable
-
-
Set集合
-
整数数组+哈希表实现,Map<String,HashSet>无序集合,类似于Java中的HashSet,存储一个列表数据且不重复,redis内部实现了交集并集补集(实现共同关注,共同粉丝喜好)
- 只用整数或可转为整数的字符串且内存小时用IntSet,否则用Hash实现。为了减少内存使用
-
-
ZSet有序集合
-
跳表+字典实现Map<String,SortedSet<>> SortedSet增加了一个权重score,可以通过权重排序,支持范围获取,logn的高度和复杂度
-
跳表:多级索引链表,数据单向有序适用于做二分查找,每层少一半数据量
-
为什么用跳表:Redis是基于内存的,对于IO并不敏感,需要节约空间存储,只需要按照概率(每个元素有一个随机的高度)维护即可,跳表这种数据结构不需要左旋右旋等操作,插入删除效率高,跳表区间来查找数据效率高于红黑树
-
字典+跳表方便了快速排序和查找
-
-
三种特殊数据类型
-
BitMap位图
-
存储的是连续的二进制数字,用于用户签到情况,用户行为统计
-
布隆过滤器实现
-
-
HyperLogLog
- 基数计数概率算法,通过稀疏矩阵或稠密矩阵计数,计算某类事件的不同唯一实例数量,效率高,占空间小,但是有些许误差
-
Geospatial
-
GEO,存储地理位置, 存储经纬(116.034579)很精确!。能实现两个地方距离计算,获取指定位置附近元素
-
基于Sorted Set实现,权重值是对经纬度进行GeoHash编码的结果
-
GeoHash:为了能高效地对经纬度进行比较,将经纬度计算成编码(编码:对经纬度通过,二分区间,区间编码并组合)
-
操作:GEOADD ,GEORADIUS(查找范围内元素)
-
-
自定义数据类型
-
RedisObject组成
-
元数据
-
type:五大基本数据类型之一
-
encoding:编码方式,表示redis底层实现的数据结构
-
lru:最后一次访问时间
-
refcount:引用次数
-
*ptr:指向数据的指针
-
-
-
自定义步骤
-
缓存击穿,雪崩,穿透,数据一致性
缓存穿透
-
大量请求的key是不合理的,数据不在缓存和数据库中,请求直接打到数据库上,对数据库加压
-
解决方案
-
根据key的设计缓存无效key
-
布隆过滤器
- bit 数组来保存所有的数据,把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中,不存在直接返回null
-
接口限流
- 使用lua脚本配合redis对于被频繁访问的接口添加ip或id在固定时长限制访问次数
-
前端进行请求检测(恶意请求)
-
缓存击穿
-
请求的key是热点数据,数据失效后,大量的请求打入数据库中,给数据库加压
-
解决方案
-
热点数据提前预热,将热点数据提前放入缓存中并设置合理过期时间
-
加锁,数据加锁只允许一个请求去访问数据库获得该数据,并载入缓存,其他请求之后从缓存中查询
-
热点数据永不过期
-
缓存雪崩
-
大量的应用请求无法在 Redis 缓存中进行处理,发送到数据层,导致数据库层压力剧增
-
大量数据同时失效
-
随机化过期时间,防止同一时间大量key过期。
-
服务降级:针对核心数据,允许查询缓存,缓存确实读取db,非核心数据,返回预定义值或error
-
-
redis缓存实例宕机了
-
部署redis集群,redis主从节点切换
-
业务层实现限流机制:令牌桶,ip或id限流
-
服务熔断:接口暂时不提供服务
-
redis旁路缓存
-
缓存命中 Redis 中有相应数据,就直接读取 Redis,性能非常快
-
缓存缺失:没有数据 ,读取数据库存入redis
-
应用程序需要获取数据,需要代码显示调用redis的get接口,数据缺失则连接数据库获取并在程序中显式调用SET
-
缓存类型
-
只读缓存
-
只在收到读请求时使用redis,发生数据删改时标记redis数据失效,更新mysql再次查询缓存时查db更新redis
-
能够加速读请求
-
-
读写缓存
-
读写缓存的这种设计,它天然就是不可靠的,是一种牺牲数据一致性换取性能的设计
-
读写请求都会发送到缓存,能够加速读写请求
-
两种策略
-
同步直写:请求 发送到缓存时也会同步发送到 数据库,二者都更新完才返回,访问性能降低(数据库改慢)
-
异步写回策略:优先考虑响应延迟,请求现在缓存中处理,异步修改数据库,存在 数据丢失风险
-
-
-
只读与读写同步直写区别
-
直写数据被再次读取时可以缓存命中,性能高。但是数据库和缓存都要返回,有额外开销
-
只读在数据更新时客户端缓存失效key,开销少,再次查询存在缓存缺失
-
-
数据一致性
-
redis做旁路缓存
-
新增数据:db添加数据,无数据一致性问题
-
流程:查询数据,redis,去查询mysql,查询后把数据写入缓存。
-
更新删除数据:必须先更新mysql,只有mysql才有事物机制。之后再写redis。
-
采用延迟双删策略,先删除缓存,线程A更新数据库,让该线程sleep一段时间,再次删除缓存。一般没见过用的
-
缓存双删缺陷
-
1.复杂点在于延迟时间的确定上,一般是1-2s后执行二次删除。
-
2.最大问题在两次删除缓存数据引起的缓存穿透,对于流量系统巨大的系统,难以接受这段负载压力
-
租约方案:多个请求到达缓存,没有缓存则返回一个token租约,token与key绑定,记录该请求,用户更新时会传递校验该token。更新后删除租约。
-
实际就是没缓存时放一个请求进去查询数据库再放到缓存上,其他请求再查询缓存即可
-
确保每次只有一个请求拥有更新数据的权限,从而避免了并发修改时导致的数据不一致问题
-
-
-
比对版本号(时间戳)的方式避免将旧数据写入缓存,在执行数据设置操作时进行比对,只写入较新的数据。
-
-
这实际是一个性能高和一致性的冲突,要保证高性能,就要接受数据的不一致。要保证强一致性,就要上锁。就是CAP理论
-
-
修改删除数据:需要同步删除缓存
-
先删除缓存,再更新db,数据库更新失败(或者慢),请求来了,缓存又读数据库,读取到旧值
-
这样,其他线程再次读取数据会把新值从db中读取到redis中去,双删的等待时间需要具体情况设置
-
db更新重试
-
-
先更新db,再删除缓存,缓存删除失败或慢,再读取,读到的是缓存的旧值
-
等待缓存删除完,期间会有数据不一致问题。(高并发时,即使都成功执行,也存在数据不一致问题)
-
采用mq实现重试机制,任务失败后将任务存入消息队列,定时读取mq中的任务并执行,成功后删除mq中任务信息
-
-
-
持久化
目的
- 内存数据持久化到磁盘,以便应对机器重启快速恢复,或是数据同步,避免从后端数据库中进行恢复
AOF日志
-
与mysql的redolog的WAL写前日志不同,AOF是写后日志,Redis 是先执行命令,把数据写入内存,然后才记录日志
-
AOF 里记录的是 Redis 收到的每一条命令
-
好处:因为是写后日志,不用语法检查,不阻塞写操作
-
风险:刚执行完一个命令,还没有来得及记日志就宕机了,数据丢失风险(可以再读db),可能阻塞下一个操作(因为AOF是读写磁盘)
-
-
AOF缓冲区的数据写入到磁盘是阻塞的,调用fync函数(强制进行硬盘同步),fync阻塞直至写入磁盘操作成功
-
AOF写回策略
-
Always同步写回,写命令执行完就将日志写入磁盘,可以做到基本不丢数据,但影响主线程性能(fsnc)
-
NO操作系统控制的写回,操作系统决定何时将缓冲区内容写回磁盘,性能高,数据丢失风险大(write)
-
Everysec每秒写回,上述两种方案的折中方案,可能会丢一秒数据,性能有影响(fsnc)
-
-
写回策略系统调用
-
write 只要把日志记录写到内核缓冲区,无需等待日志写回到磁盘;
-
fsync 需要把日志记录写回到磁盘后才能返回,时间较长,阻塞主进程,且在AOF重写时IO资源开销到达,fsnc容易阻塞
-
-
AOF重写机制
-
Redis 根据数据库的现状创建一个新的 AOF 文件,对每一个键值对用一条命令记录它的写入
-
一个拷贝两处日志
-
重写开始时,redis主线程会fork出一个子进程bgRewirteAOF,fork会把主线程内存拷贝一份,子进程使用拷贝的内存数据写AOF
-
重写的同时,有新的写操作,正在使用的老 AOF 日志也记录,这个操作也会被记录到AOF 重写日志缓冲区,新操作也记录到AOF中
-
最后用新的 AOF 文件替代旧文件了
-
-
RDB内存快照
-
AOF日志记录只需要记录命令,但恢复需要执行大量操作日志,数据恢复时,我们可以直接把 RDB 文件读入内存
-
把某一时刻的状态以文件的形式写到磁盘上,也就是RDB快照,分为全量快照和增量快照
-
redis提供两个命令生成RDB
-
save:主线程执行,阻塞,一般不用
-
bgsave,创建子进程执行,避免主线程阻塞,是默认配置
-
-
避免阻塞和正常处理写操作并不是一回事
-
执行快照需要的将大量内存数据写入磁盘,较慢,期间存在写操作
-
采用写时复制技术COW,开始读取主内存数据,某条数据要修改时,复制一份副本用于RDB记录
-
-
创建快照是否阻塞主进程?
-
save命令:同步保存操作,会阻塞Redis主进程
-
bgsave:fork出一个子进程,子进程执行快照操作,不阻塞主进程,是默认选项
-
-
增量快照
-
快照不能频繁执行,影响磁盘IO等性能
-
做了一次全量快照后,后续的快照只对修改的数据进行快照记录
-
但是如果大量数据都被修改的,记住被修改数据的代价较大,需要大量内存
-
Redis4.0混合方案
-
内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作
-
快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了
-
AOF记录了历史日志,文件比RDB大
高性能IO模型
Redis 主线程(6.0 前)串行处理所有命令,避免了锁竞争和线程切换的开销。所有操作天然具有原子性,例如 INCR 命令无需额外同步机制。
-
多路复用机制
-
主线程在事件循环中监听所有连接的读写事件
-
当某个 socket 有数据到达时,内核通过回调机制通知 Redis
-
6.0+ 版本引入多线程处理网络 IO(命令执行仍保持单线程)
-
单核 Redis 可达到 10万+ QPS
-
-
正常请求需要执行:监听请求,建立连接,读取解析请求,读数据返回,过程存在阻塞点(accept连接,recv读请求),多路复用机制就是为了避开这些阻塞点
-
accept需要建立连接时,socket()调用listen方法可以转为监听套接字,监听请求,并调用accept返回已连接的套接字交给redis线程处理
-
recv同理,读请求是数据未到达,可以让redis线程处理其他操作,数据到达是通知redis
-
事件会被放进一个事件队列,Redis 单线程对该事件队列不断进行处理(无需轮询)
-
套接字:理解为操作底层网络协议的抽象接口
-
Linux多路复用机制:同时存在多个监听套接字和已连接套接字
-
redis为什么不用多线程:多线程面临的共享资源的并发访问控制问题(存在死锁、线程上下文切换)
redis网络IO和键值对读写是单线程完成的,持久化,异步删除,集群数据同步是额外线程实行的
后台线程:用来执行耗时操作,例如释放持久化过程的临时文件,bgsave持久化,AOF重写刷磁盘,大对象的清理
redis异步机制
-
redis单线程阻塞原因
-
集合的全量查询和聚合操作(关键路径操作)无法异步执行
-
慢查询命令
-
redis变慢可以去查询一些执行速度慢的命令的,并对应优化(大key查询,集合聚合操作),执行聚合操作可以在客户端完成 (前端)
-
-
bigkey的删除
-
清空数据库
-
AOF日志同步写
- 写回策略对数据做落盘保存会导致主线程阻塞
-
从库加载RDB文件(关键路径操作)
-
redis的写操作也时关键路径操作,需要等待返回,短时间内大数据量的写也可能造成阻塞
-
大量key的同时过期触发连续的redis定时清理任务也会导致阻塞
-
-
主线程中执行上述操作,会导致阻塞无法提供其他服务
-
异步机制
-
概念:Redis 主线程启动后会创建三个子线程分别负责AOF日志写操作键值对删除操作以及文件的关闭异步执行
-
主线程通过一个链式任务队列和子线程进行交互,收到键值对删除或db清空命令时,redis线程将任务放入队列后返回执行完成信息(惰性删除),避免对主线程的性能影响
-
非redis主线程关键路径上的操作可以被异步执行(无需等待返回值的)
-
AOF日志重写
- AOF配置成everysec后,主线程会把AOF写入职封装成任务,放入任务队列
-
bigkey删除
-
清空数据库
-
-
Redis主从
Redis主从同步
-
redis高可靠,提供了主从模式,主从库采用读写分离方式
-
主从库都可以接收读操作
-
写操作在主库执行,然后主库将写操作同步给从库
-
-
实现
-
第一次同步:启动多个redis实例,slaveof命令命令主从库关系
-
三个阶段
-
一阶段:主从库建立连接,并通知主库解即将同步,主库确认,即将开始同步
-
二阶段:从库清空数据,主库将所有数据同步给从库(主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库)。从库收到数据后,在本地完成数据加载
-
三阶段:主库把二阶段过程中收到的新命令记录在replication buffer并传给主库
-
-
-
主从级联模式
- 主库既要生成RDB又要传输,压力大,所以可以采用"主 - 从 - 从"模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。
-
基于长连接命令传播
- 主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库通过这个连接将后续操作传给从库
-
增量复制
-
主从库连接断了:2.6之前的策略是再进行一次全量复制
-
之后采用增量复制,连接断了后通过主从库的repl_backlog_buffer判断操作差距并=同步恢复
-
主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区
-
repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置
-
-
从库过期数据
-
主从数据不一致,无法强一致性,建议保证良好网络环境,以及使用程序监控从库复制进度
-
读到过期数据:使用 Redis 3.2 及以上版本(从库过期数据会返回空值),EXPIREAT/PEXPIREAT 命令设置过期时间实现主从库数据同步过期
-
哨兵机制
-
哨兵:特殊的redis进程实例,负责监控,选主库,通知
-
监控:哨兵周期性给主从库发送ping命令,从库定时未响应,标记下线,主库则开始自动切换主库流程
-
选举主库:哨兵就从很多个从库,按照一定的规则选择一个从库实例作为主库
-
通知:把新主库的连接信息发给其他从库,让它们执行 slaveof 命令和新主库建立连接,并进行数据复制
-
-
主库下线
-
主观下线
-
哨兵一定时间内收不到ping回应,主管认为主库挂了,对于从库直接标记下线
-
但是可能存在误判,所以主库并未标记下线,可能是主库繁忙来不及主力ping,误判切换主库开销大
-
-
客观下线
- 哨兵也是存在集群的,引入多个哨兵主观判断主库下线,则主库标记下线,误判概率小
-
-
哨兵主库选举
-
筛选
- 筛选出从库正在运行,网络连接好的,断连少的出来
-
打分
-
给筛选出来的从库打分,分高的成为主库
-
1.优先级高的分高,slave-priority配置
-
2.旧主库同步程度最接近的从库得分高,偏移量
-
3.ID 号小的从库得分高(默认规定)
-
-
主节点下线,从节点变为主节点,防止脑裂
-
-
哨兵集群
-
哨兵集群组成
- Redis 提供了 pub/sub 发布订阅机制,使得不同的哨兵可以相互发现,组成集群
-
哨兵通过哨兵向主库发送 INFO 命令,可以获得从库命令从而ping检查
-
依赖依赖 pub/sub 机制,可以实现哨兵和客户端之间的信息同步(客户端订阅频道消息)
- 相互发现:每个哨兵节点会定期广播自己的信息。
状态共享:哨兵节点分享对主节点和从节点的监控结果。
事件通知:当发生主从切换等事件时,通过 Pub/Sub 通知其他哨兵。
- 相互发现:每个哨兵节点会定期广播自己的信息。
-
依赖 pub/sub 机制通知集群成员有关故障或配置更改的事件
-
redis切片集群
-
启动多个 Redis 实例组成一个集群,然后按照一定的规则,把收到的数据划分成多份,每一份用一个实例来保存
-
横向扩展与纵向扩展
-
纵向扩展:增加配置,内存磁盘等,经济开销大,且内存存储过大,RDB过慢
-
横向配置:增加当前Redis实例个数,数据分片
-
-
数据切片规则与定位
-
Redis Cluster方案
-
使用16384哈希槽按照规则分区,分给各个切片
-
Redis实例在集群中有新增删除,且为了负载均衡,哈希槽会重新分配
-
数据定位:Redis根据键的哈希值确定它属于哪个哈希槽,再根据哈希槽与节点的映射关系,找到具体负责的分片实例。若本实例不存在哈希槽,重定向再找
-
重定向机制:本Redis实例没有,会发送下一个实例的访问地址给客户端,客户端重新发送请求到新的Redis实例
-
-
-
数据倾斜
-
数据量倾斜
-
数据中有bigkey,导致某实例数据量增加
-
slot槽手工分配表不均,导致某些实例上有大量数据
-
使用HashTag,只计算{}中的hash,导致入同一哈希槽
-
-
数据访问倾斜
- 热点数据存在,大量请求打到热点数据所在实例
-
处理方法
- 避免创建大key,大key拆分,合理分配slot,优先避免数据倾斜,不用HashTag。采用热点数据多副本方法应对热点数据(只能针对只读的热点数据)
-
redis事务,并发
redis事务在后续命令无错的情况下基本可以保证单命令一致性隔离性,只保证多条命令顺序执行
redis事务中单命令是原子性的,多个命令执行会被其他命令打断,只能保证命令顺序执行,且没有回滚机制
redis事务
-
原子性:一个事务中多个操作必须同时完成或失败
- Redis事务提供了部分原子性,保证事务中的命令按顺序执行,但无法保证整体的原子性操作和自动回滚。(AOF日志可以保证一些原子性)
-
一致性:数据在事务执行前后是一致的
-
隔离性:执行一个事务时其他操作无法存取正执行事务访问的数据
-
持久性:执行事务后,数据的修改持久化
-
Redis 的事务机制可以保证一致性和隔离性,但是无法保证持久性
并发访问正确性
-
原子操作
-
单命令原子操作
-
多个操作在 Redis 中实现成一个操作,也就是单命令操作(比如INCR/DECR,setnx)
-
redis是单线程模型,Redis 执行某个命令操作时,其他命令是无法执行的
-
-
lua脚本
-
执行的操作存在复杂逻辑判断等需要使用lua脚本,Redis 把整个 Lua 脚本作为整体执行,不会被其他命令打断
-
使用 Redis 的 EVAL 命令来执行脚本,避免把不需要做并发控制的操作写入脚本中,影响redis性能(执行lua脚本时间过长会阻塞)
-
-
-
分布式锁(会导致并发性能降低)
-
分布式锁:redis是分布式系统,锁不能是本地的锁,会有多个客户端需要获得锁,redis可以被多方连接且共享,所以用来实现分布式锁
-
加锁
-
进程SETNX某个字段作为key,并expire成功创建则获得锁,value是相关的unique值(以防释放其他进程的锁)设置过期时间(防止释放其他进程的锁),上述是一个原子操作(setnx和expire是两个命令,需要lua脚本)
-
加锁后其他线程setnx无法创建就无法获得锁
-
-
解锁
- 先判断value是否为当前进程创建的锁,判断为是则释放锁,判断和释放锁操作不是一个原子操作,要有lua脚本(涉及读取判断删除多个操作)
-
redlock
- redis部署多个实例时,若主节点宕机,从库来不及同步,锁会丢失,RedLock算法保证一半以上的redis实例加锁,并根据加锁时间计算ttl实现
-
Redission分布式锁实现
- 默认单节点,支持RedLock模式,启动一个后台线程(看门狗),每隔一段时间(默认10秒,锁超时时间的1/3)检查锁是否仍被持有。lua脚本
-
锁的ttl
-
锁如果不设置过期时间可能会无法释放,ttl过短过长都不好
-
创建锁时过期时间的设置根据业务来确定,还有一种方案时创建一个守护进程,在业务执行时延长锁的过期时间
-
-
开发时使用的注意点:key的设置,finally代码块释放锁
-
redis高并发秒杀实战
-
秒杀场景:瞬时并发访问量非常高,读多写少,使用redis拦截大量请求避免数据库压力过大
-
使用mysql会出现超卖问题,RR,RC隔离级别,
-
实现方案
-
redis可以保证库存查验和库存扣减原子性执行,且支持高并发
-
redis原子操作,lua脚本可以实现
-
使用分布式锁来保证多个客户端能互斥执行这两个操作,让客户端向 Redis 申请分布式锁,只有拿到锁的客户端才能执行库存查验和库存扣减
-
-
冷热流量隔离
- 秒杀场景读多写少,可以使用切片集群中的不同实例来分别保存分布式锁和商品库存信息,避免影响正常商品
内存管理
给数据设置过期时间,防止内存用光
过期key删除策略
-
惰性删除:使用redis查询某个键时,若该键过期则删除掉,释放内存,减少额外开销
-
定期删除
-
redis每100ms抽样k条数据删除其中的过期数据,若过期数据比例超过25%则重复上述过程,k可配置
-
如果redis同一时间有大量key过期,会触发连续的定期删除,影响正常服务的进行redis变慢
-
Redis 4.0 后可以用异步线程机制来减少阻塞影响
-
redis缓存淘汰策略
-
AllKeys(所有key)
-
lru
-
random
-
lfu
-
-
Volatile
-
lru
-
lfu
-
random
-
ttl:根据剩余过期时间删除
-
-
内存清理是 Redis 中用于应对内存不足问题的一种机制
-
缓存污染:redis中不再被访问的数据且未被删除,占用缓存
-
Redis的lru实现:采样n个数据 , 根据RedisObject中记录的最近访问时间删除采样数据一部分
- 因为只看数据的访问时间,使用 LRU 策略在处理扫描式单次查询(一次性的大量扫描)操作时,数据只会被读取一次,但都是刚访问过,无法解决缓存污染
-
lfu:类似于LRU,但是考虑了数据的访问时效性和数据的访问次数
- lfu对比上述lru,更关注访问频率,而lru更关心访问时效
-
数据淘汰:与最初和db读取的数据相比是否修改,修改了就是脏数据,修改db后淘汰(redis做读写缓存时)
- 在再查询一次db消耗大,可能异步或同步db已修改,不是脏数据
redis内存碎片
-
内存碎片化产生原因
-
操作系统内存分配器分配策略:内存分配器划定了一系列大小的内存空间,分配时按照这些内存空间大小分配,可能会大
-
Redis负载特征:redis在申请内存空间是的需求大小不同,导致分配的内存大。redis的键值的修改和删除会导致空间的扩容和释放。
-
-
会导致redis的内存实际利用率降低
-
内存碎片查看:INFO memory指令,返回的信息中mem_fragmentation_ratio即为内存使用率(实际使用/所需),1-1.5正常,大于1.5需要降低内存碎片率(小于1,操作系统分配的内存比所需的少操作系统出问题了 (swap))
-
内存碎片的清理
-
1.重启 redis实例(存在数据持久化和数据丢失问题)
-
2.redis自动碎片清理,将内存碎片周围数据拷贝到别处从而产生一块较大的连续空间。(但是执行碎片清理进行数据拷贝会造成redis阻塞,性能降低)
-
redis缓冲区
-
客户端输入输出缓冲区
-
输入缓冲区缓存客户端命令,redis取出处理后将结果返回到输出缓冲区,取出返回
-
缓冲区溢出
-
输入缓冲区溢出:写入bigkey需要大量信息存入缓冲区,服务器处理请求过慢,请求堆积
-
输出缓冲区溢出:服务器返回bitkey,执行montitor返回大量监控信息,缓冲区大小不合理
-
避免:缓冲区调大,加快命令发送处理速度
-
-
-
主从集群缓冲区
-
全量复制缓冲区
- 执行RDB接收加载时,主节点收到的RDB命令写入复制缓冲区
-
复制积压缓冲区(就是主从复制的环形缓冲区)
-
增量复制时使用的缓冲区
-
环形缓冲区溢出,新命令会覆盖旧命令,导致主从节点重新进行全量复制
-
-