Redis专项

笔记7 Redis

1. Redis为什么这么快

对于普通单机Redis,能达到读11w/s,写8w/s

  • 基于内存:最重要的一点
  • 单线程模型:减少上下文切换
  • IO多路复用,主线程负责接收客户端请求并放入队列中,通过事件循环机制逐个处理请求

2. Redis的优缺点

2.1 优点

  1. 基于内存操作,读写速度快

  2. 支持多种数据类型,相比其他缓存数据库来说

  3. 支持持久化,可以通过开启RDB和AOF两种持久化机制,有效的避免数据丢失问题

  4. 支持简单事务,将一组操作合并

  5. 支持主从复制,可以读写分离提高并发

2.2 缺点

  1. Redis容量受内存限制,不适合海量数据的存储

  2. 对数据的结构化查询支持较差(keys 、scan命令)

  3. 扩容较难,需要进行数据迁移

  4. 无法支持复杂事务

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快速链表实现
  • 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作为缓存,共识:

  • 设置过期时间,不论是做容错还是释放内存都需要设置过期时间
  • 数据最终一致性,允许有一小段时间不一致

实现最终一致性:

  1. 延时双删:删缓存、写数据库、休眠再删缓存
    1. 弊端:很多时候数据的写可以是很多操作触发的,从接口处很难收口
  2. 监听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命令可以保存快照
save 900 1 // 900 秒之内,对数据库进行了至少 1 次修改;
save 300 10 // 300 秒之内,对数据库进行了至少 10 次修改;
save 60 10000 // 60 秒之内,对数据库进行了至少 10000 次修改。

6.2 AOF:增量备份

每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里

优点

  • 根据不同的fsync策略将数据丢失的风险降到最低

缺点:

  • 文件体积大,数据恢复慢,需要一条条执行命令(文件太大会触发AOF重写,压缩文件)

7. 过期删除机制

Redis的过期删除是惰性删除+定期删除

  • 定时删除:给每个设置过期的数据维护一个定时器,到期删除,会占用大量CPU资源
  • 惰性删除:访问数据时再去判断数据是否过期
  • 定期删除:每隔一段时间抽取检查,释放内存资源

8. Redis内存满了怎么办,内存淘汰策略(LRU)

1. 可以分为3大类,8小类

  1. 不进行数据淘汰
    1. noeviction:Redis3.0之后默认淘汰策略,会返回错误
  2. 在设置了过期时间的数据中淘汰
    1. volatile-random:淘汰随机数据
    2. volatile-lru:淘汰最久未使用
    3. volatile-lfu:淘汰使用频次最少
    4. volatile-ttl:淘汰最接近过期的数据
  3. 在所有数据中淘汰
    1. allkeys-random:淘汰随机数据
    2. allkeys-lru:淘汰最久未使用
    3. allkeys-lfuu:淘汰使用频次最少

2. LRU算法

怎么自定义实现LRU

  1. 通过LinkedHashMap提供的构造方法
  2. 自定义实现,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事务

  1. 事务开启后,每次命令都是一次交互调用,非批量提交,并且阻塞其他线程
  2. 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

存在问题和解决方案:

  1. 当获取到锁的线程异常退出,会造成锁无法释放 -- 设置过期时间
  2. 设置了过期时间,但是线程处理时间长导致过期自动释放锁 -- 通过守护线程自动续约
  3. 错误释放了其他线程获取到的锁 -- 需要设置全局唯一的value值,释放锁时进行判断
  4. 无法重复获取锁 -- 另外存储锁重入次数,获取锁+1,释放锁-1,可以使用hash
  5. 释放锁判断当前线程不是原子操作 -- 引入lua实现原子操作
  6. 并发场景下容易获取锁失败 -- 需要在规定时间内重试再获取锁
  7. 可靠性问题 -- 引入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脚步实现方式:

  1. 通过 script load 加载脚本到Redis服务器,并返回sha1
  2. 再通过 evalsha <sha1> <key> <rate> <capacity> 调用脚本并返回是否限流
  3. sha1:上传script的脚步编号
    1. key:限流对象
    2. rate:速率,每秒多少个请求
    3. capacity:缓存容量

redission实现方式:

引入Redission,通过api实现限流

Java触发方式

  1. 通过 自定义注解 + AOP编程 实现限流的触发
  1. 如果是动态配置限流,由于读取频繁应本地缓存定时更新

1. 漏桶(恒定速率流出)

  • 算法实现:
  • 核心计算逻辑:排队数量 = 上次排队数量 - (时间差 * 速率),当排队数量>=容量即限流
  • 特性:恒定的消费速率,严格按照评估的服务承受能力进行限流

2. 令牌桶(恒定速率流入)

  • 算法实现:
  • 核心计算逻辑: 令牌数量 = 上次令牌数量 +(时间差 * 速率),当令牌数量<=0即限流
  • 特性:恒定的请求新增速率,但是由于初始化原因极端情况下会出现一个周期内2倍速率

local key = KEYS[1]
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
-- 时间单位是秒
local now = redis.call('TIME')[1]
-- 上次计算时间、上次计算数量
local lastTime = redis.call('GET', key .. ':lastTime')
lastTime = (lastTime and lastTime or now)
local countResult = redis.call('GET', key .. ':countResult')
countResult = (countResult  and countResult or capacity)

-- 核心计算
local seconds = now - lastUpdateTime
countResult = math.min(capacity, countResult + rate * seconds)

-- 判断限流
if countResult <= 0 then
    return 0 -- 返回0表示限流
end
-- 更新计算时间为now,更新计算结果为+1
countResult = countResult - 1
redis.call('SET', key .. ':lastTime', now)
redis.call('SET', key .. ':count', countResult)
return 1

16. Redis性能变差,可能是什么原因

  1. 存在大key,操作和释放都耗时,也占用带宽

  2. 内存溢出了,每次操作都会剔除一部分数据

  3. 内存不足使用了swap,一部分内存数据存在磁盘上

  4. 网络带宽过载

  5. 频繁的短连接

17. 使用lua脚本注意事项

lua脚本是在一台机器上运行的,会根据调用脚本时传的key计算哈希槽之后路由到对应的节点

  • 所以当我们需要在脚本里操作多个key时,需要确保多个key在同一个节点

  • 代码里可以通过CRC16计算slot,redis命令可以用cluster keyslot key111 计算

  • 可以通过花括号{}确保多个key路由到同个节点

  • redissiin也是这么实现的,在redission使用布隆过滤器时,会生成两个key,会根据key原值生成一个二进制数组的值,还会再生成一个元数据配置的key 命名是{key}:config

  • 19
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值