笔记7 Redis
1. Redis为什么这么快
对于普通单机Redis,能达到读11w/s,写8w/s |
- 基于内存:最重要的一点
- 单线程模型:减少上下文切换
- IO多路复用,主线程负责接收客户端请求并放入队列中,通过事件循环机制逐个处理请求
2. Redis的优缺点
2.1 优点
-
基于内存操作,读写速度快
-
支持多种数据类型,相比其他缓存数据库来说
-
支持持久化,可以通过开启RDB和AOF两种持久化机制,有效的避免数据丢失问题
-
支持简单事务,将一组操作合并
-
支持主从复制,可以读写分离提高并发
2.2 缺点
-
Redis容量受内存限制,不适合海量数据的存储
-
对数据的结构化查询支持较差(keys 、scan命令)
-
扩容较难,需要进行数据迁移
-
无法支持复杂事务
3. Redis是单线程吗
- 对数据的读写是单线程的
- 6.0版本之前,只有AOF和主从同步是其他线程
- 6.0版本支持IO多线程特性,可以开启多线程处理客户端读请求,需要修改配置文件
4. Redis数据类型和应用场景
4.1 常见数据类型:String、Hash、List、Set、Zset
- String:
- 底层数据结构是SDS(简单动态字符串),SDS使用len属性来记录数据长度,内部编码有3种:int、raw、embstr
- value最多容纳512M
- 应用场景:
- 缓存对象、设置过期时间
- 常规计数,使用 incr +1、decr -1
- 分布式锁:setnx
- List
- 3.2版本前:底层实现是双向链表或压缩列表
- 列表元素个数小于521、每个元素的值都小于64字节,则使用压缩列表
- 其他场景使用双向链表
- 3.2版本后,使用qiucklist快速链表实现
- 3.2版本前:底层实现是双向链表或压缩列表
- Hash
- 底层数据结构是压缩列表或哈希表
- 列表元素个数小于521、每个元素的值都小于64字节
- Set
- zset
- 底层数据结构是压缩列表、跳表
- 列表元素个数小于128、每个元素的值都小于64字节,使用压缩列表,其他场景使用跳表
4.2 拓展的数据类型:BitMap(2.2)、HyperlogLog(2.8)、GEO(3.2)、Stream(5.0)
- BitMap 位图
- 使用string作为底层数据结构,是一连串的二进制数组0和1,可以通过offset定位元素
- 适用于位运算、二值统计场景,省去很多内存空间
- 统计在线人数:用户id为偏移量,在线状态0和1
- 统计连续签到用户:每天签到都用一个bitmap存储,与运算
- HyperlogLog 提供去重计数 (基于概率,不精确)
- GEO
- 基于zset实现,通过GeoHash编码将经纬度进行转换
- 可以实现搜索附近的功能
- Stream
- 消息队列、发布订阅
- 缺点:Redis丢数据,基于内存有瓶颈
5. Redis作为缓存中间件,在使用时怎么保证一致性
一致性:指的是数据库数据与缓存的数据一致 |
Redis作为缓存,共识:
- 设置过期时间,不论是做容错还是释放内存都需要设置过期时间
- 数据最终一致性,允许有一小段时间不一致
实现最终一致性:
- 延时双删:删缓存、写数据库、休眠再删缓存
- 弊端:很多时候数据的写可以是很多操作触发的,从接口处很难收口
- 监听binlog异步删除:通过canal订阅binlog日志打到kafka消费写Redis
6. Redis的持久化机制
6.1 RDB:全量备份、快照备份(默认开启)
将某一时刻的内存数据,以二进制的方式写入磁盘,生成dump.rdb文件 RDB快照是全量快照,是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响 执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于写时复制技术(Copy-On-Write, COW) |
优点
- 加载速度远超AOF文件,只需将二进制文件读入内存
- RDB文件是压缩的二进制文件,适用于备份、全量复制
缺点
- 会造成数据丢失:每隔一段时间做一次RDB,间隔时间内故障会丢失数据
// 通过save/bgsave命令可以保存快照 |
6.2 AOF:增量备份
每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里 |
优点
- 根据不同的fsync策略将数据丢失的风险降到最低
缺点:
- 文件体积大,数据恢复慢,需要一条条执行命令(文件太大会触发AOF重写,压缩文件)
7. 过期删除机制
Redis的过期删除是惰性删除+定期删除 |
- 定时删除:给每个设置过期的数据维护一个定时器,到期删除,会占用大量CPU资源
- 惰性删除:访问数据时再去判断数据是否过期
- 定期删除:每隔一段时间抽取检查,释放内存资源
8. Redis内存满了怎么办,内存淘汰策略(LRU)
1. 可以分为3大类,8小类
- 不进行数据淘汰
- noeviction:Redis3.0之后默认淘汰策略,会返回错误
- 在设置了过期时间的数据中淘汰
- volatile-random:淘汰随机数据
- volatile-lru:淘汰最久未使用
- volatile-lfu:淘汰使用频次最少
- volatile-ttl:淘汰最接近过期的数据
- 在所有数据中淘汰
- allkeys-random:淘汰随机数据
- allkeys-lru:淘汰最久未使用
- allkeys-lfuu:淘汰使用频次最少
2. LRU算法
怎么自定义实现LRU
- 通过LinkedHashMap提供的构造方法
- 自定义实现,LinkedHashMap+TreeMap排序进行实现
存在问题:缓存污染问题,有些数据可能只会读一次,但是却在缓存中占用很长时间
public class LRUCache<K, V> {
private Map<K, V> map;
private final int cacheSize;
public LRUCache(int initialCapacity) {
map = new LinkedHashMap<K, V>(initialCapacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > cacheSize;
}
};
this.cacheSize = initialCapacity;
}
}
3. LFU算法(Redis4.0版本引入)
将最近不常访问的数据淘汰掉,解决缓存污染问题
9. 事务机制
关键字:multi 开启事务、exec 执行事务、discard 清除命令列表、watch 监听
原理:在Server端缓存了命令列表,通过单线程的特性执行这批命令,让其他操作切不进来
不推荐使用Redis事务
- 事务开启后,每次命令都是一次交互调用,非批量提交,并且阻塞其他线程
- Redis命令执行失败会继续执行
推荐事务实现方式:Lua脚步
Lua是用标准C语言编写,Redis本身就是C语言编写,2.6版本之后支持Lua内嵌
通过脚本可以实现复杂逻辑 -- 可以举例指标消费过程、分布式锁、秒杀场景
10. Redis高可用 集群策略
主从策略、哨兵架构、cluster集群、codis中间件 |
主从策略
- 所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的
- 但是,主服务器并不会等到从服务器实际执行完命令后,再把结果返回给客户端,而是主服务器自己在本地执行完命令后,就会向客户端返回结果了。如果从服务器还没有执行主服务器同步过来的命令,主从服务器间的数据就不一致了
- 缺点:发生宕机时需要手动恢复
哨兵模式 Redis Sentinel
- 哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。
- 哨兵集群通过定时查询(每秒)检查主节点的健康状态,如果监控到主节点下线
- 第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑选出一个从节点,并将其转换为主节点。
- 第二步:让已下线主节点属下的所有「从节点」修改复制目标,修改为复制「新主节点」;
- 第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端;
- 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点;
RedisCluster集群模式(实现了拓展多主节点)
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。 在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,0~16383 |
Codis中间件
通过中间件代理每个key路由到哪个Redis
11. 使用Redis做缓存会遇到什么问题,怎么避免(缓存击穿、缓存穿透、缓存雪崩)
11.1 缓存击穿
- 现象:
- 大量请求同一时间访问,缓存中没有该数据,都打到了数据库
- 可能是热点数据过期,也有可能是秒杀等营销活动
- 解决方案:
- 预热:提前加载数据到缓存
- 如果是热点数据失效场景,热点数据不设置过期时间,通过定时更新避免脏数据
- 加锁限制
- 应用:
- 资源告警策略数据:提前写入缓存,不设置过期时间
11.2 缓存穿透
- 现象:
- 用户访问的数据,既不在缓存中,也不在数据库中,每次请求都直达数据库
- 解决方案:
- 非法请求限制:校验恶意请求
- 缓存默认值,最简单的处理方式
- 使用布隆过滤器判断数据是否存在(推荐)可以通过Redission、guava实现
- 原理:
- 存储:通过一个数组(存0和1)和多个hash函数
- 当数据存入时,如3个hash函数计算出来3个下标,将数组对应下标标记1
- 判断数据是否存在:如果3个hash函数计算后通过数组得到的值都是1
- 判断不存在则一定不存在,判断存在则有一定的误判率
- 影响因素:数组容量、函数个数
- 原理:
11.3 缓存雪崩
- 现象:
- 当缓存数据大面积过期后,用户访问的数据如果不在缓存里,大量请求直接打到数据库
- 解决方案:
- 将缓存失效时间随机打散
- 设置缓存不过期
12. Redis实现分布式锁
1. Redis手动实现分布式锁:
关键命令: setnx key value
- 如果key不存在,设置value并返回1
- 如果key存在,返回0
存在问题和解决方案:
- 当获取到锁的线程异常退出,会造成锁无法释放 -- 设置过期时间
- 设置了过期时间,但是线程处理时间长导致过期自动释放锁 -- 通过守护线程自动续约
- 错误释放了其他线程获取到的锁 -- 需要设置全局唯一的value值,释放锁时进行判断
- 无法重复获取锁 -- 另外存储锁重入次数,获取锁+1,释放锁-1,可以使用hash
- 释放锁判断当前线程不是原子操作 -- 引入lua实现原子操作
- 并发场景下容易获取锁失败 -- 需要在规定时间内重试再获取锁
- 可靠性问题 -- 引入RedLock
Cluster集群、哨兵集群都是主从架构数据同步,写操作都是发生在主节点没问题 但是Redis主从复制默认是异步的,如果主从同步过程中宕机会导致重复获取到锁 RedLock: 通过部署5个主节点,确保过半节点都加锁成功才成功获取到锁 增加了成本,也增加了系统复杂性,一个获取分布式锁的操作应该是高效的 |
2. 成熟的解决方案:使用Redission实现分布式锁
解决以上所有问题
13. 大Key,热Key问题
大key需要进行拆分:
- String类型
- 超过1M就是大key
- 可以序列化和压缩,可以拆分的拆分成hash,不能拆分的使用其他数据库存储
- list、hash、set、zset类型
- 元素超过1w就是大key
- 可以通过数据规模,切分成多个容器
热key问题可以通过jvm层缓存进行规避
14. Redis pipeline批量执行
- pipeline缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令
- 使用pipeline组装的命令个数不能太多,不然数据量过大客户端阻塞的时间可能过久,同时服务器也被迫回复一个队列答复,占用很多内存
15. Redis怎么实现限流
应对场景:突发流量、恶意流量、业务本身需要 限流说明:在单位时间内允许多少请求,如1分钟1000个请求 限流目的:保护后端服务器不至于被流量打垮崩溃 限流对象:接口、用户、ip、授权第三方等等 限流算法:滚动窗口、滑动窗口、漏桶、令牌桶 |
限流算法
- 滚动窗口:通过周期访问计数,如统计每分钟请求量
- 滑动窗口:基于当前时间点往前统计,单位时间窗内的请求量
- 漏桶算法:模拟了一个漏桶,请求进入漏桶后以固定的速率处理,超出漏桶容量的请求则会被拒绝或丢弃
- 令牌桶:维护一个固定容量的令牌桶,固定的速率放入令牌,每个请求必须获取一个令牌才能被处理,超出令牌数量则请求被拒绝或排队等待
限流算法lua实现
lua脚步实现方式:
redission实现方式: 引入Redission,通过api实现限流 Java触发方式
|
1. 漏桶(恒定速率流出)
- 算法实现:
- 核心计算逻辑:排队数量 = 上次排队数量 - (时间差 * 速率),当排队数量>=容量即限流
- 特性:恒定的消费速率,严格按照评估的服务承受能力进行限流
2. 令牌桶(恒定速率流入)
- 算法实现:
- 核心计算逻辑: 令牌数量 = 上次令牌数量 +(时间差 * 速率),当令牌数量<=0即限流
- 特性:恒定的请求新增速率,但是由于初始化原因极端情况下会出现一个周期内2倍速率
local key = KEYS[1] |
16. Redis性能变差,可能是什么原因
-
存在大key,操作和释放都耗时,也占用带宽
-
内存溢出了,每次操作都会剔除一部分数据
-
内存不足使用了swap,一部分内存数据存在磁盘上
-
网络带宽过载
-
频繁的短连接
17. 使用lua脚本注意事项
lua脚本是在一台机器上运行的,会根据调用脚本时传的key计算哈希槽之后路由到对应的节点
-
所以当我们需要在脚本里操作多个key时,需要确保多个key在同一个节点
-
代码里可以通过CRC16计算slot,redis命令可以用cluster keyslot key111 计算
-
可以通过花括号{}确保多个key路由到同个节点
-
redissiin也是这么实现的,在redission使用布隆过滤器时,会生成两个key,会根据key原值生成一个二进制数组的值,还会再生成一个元数据配置的key 命名是{key}:config