Redis缓存设计与性能优化

Redisson

getLock获取锁对象->尝试加锁-》添加看门狗时间默认30s,可以自己设置-》非公平锁,设置锁过期时间,eval表达式,判断是否被加锁,没有则直接加锁,设置过期时间,返回null,重入锁,返回null,返回剩余时间-》添加订阅,在futrue执行完后会递归执行更新锁失效时间

加锁成功直接返回,否则会添加订阅->执行future-》在for循环里 再次尝试加锁,失败则阻塞锁剩余时间,用信号量,共享锁阻塞,被唤醒继续执行,知道获取锁成功

解锁unlock-》执行eval表达式,释放锁成功会发布消息

LockPubSub里面会接受发布的消息,共享锁释放唤醒阻塞的线程

琐失效

在集群环境下,主从,一个主节点宕机了,从节点没有备份到加锁的数据,从节点上线没有数据,别的线程获取锁可以成功

redlock也不能避免

不仅会影响性能,即使是超过半数同步了,可能用的aof持久化,在那一秒没有同步到数据,主节点宕机,变为从节点上线。别的线程也可以超过半数,加锁成功

缓存设计

缓存失效(击穿)

由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存 直达数据库,可能会造成数据库瞬间压力过大甚至挂掉
对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。
缓存读延期

缓存穿透

缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。

设置空缓存加超时时间解决缓存穿透问题

布隆过滤器

当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。

布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。

布隆过滤器不能删除数据,如果要删除得重新初始化数据

缓存雪崩

缓存层支撑不住或宕掉后, 流量会像奔逃的野牛一样, 打向后端存储层。

由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会打到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。

预防和解决缓存雪崩

保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。

依赖隔离组件为后端限流熔断并降级。比如使用Sentinel或Hystrix限流降级组件。

服务降级

当业务应用访问的是非核心数据(例如电商商品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取

提前演练。

热点缓存key重建优化

并发量非常大。

重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。

避免大量线程同时重建缓存

利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。

分布式锁+双重检测

//从缓存里查数据
        product = getProductFromCache(productCacheKey);
        if (product != null) {
            return product;
        }
//加分布式锁解决热点缓存并发重建问题
        RLock hotCreateCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_CREATE_PREFIX + productId);
        hotCreateCacheLock.lock();
        try {
            product = getProductFromCache(productCacheKey);
            if (product != null) {
                return product;
            }

缓存与数据库双写不一致

对于并发几率很小的数据,这种几乎不用考虑这个问题
并发很高,如果业务上能容忍短时间的缓存数据不一致,缓存加上过期时间依然可以解决大部分业务对于缓存的要求。
通过加分布式读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。
用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存

针对的都是读多写少的情况加入缓存提高性能,如果写多读多的情况又不能容忍缓存数据不一致,那就没必要加缓存

开发规范与性能优化

value设计

拒绝bigkey

非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题

bigkey会导致redis阻塞,网络拥塞(每次获取要产生的网络流量较大),

过期删除(设置了过期时间,当它过期后,会被删除,如果没有使用过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞Redis的可能性)

拆优化bigkey:big list: list1、list2、...listN,big hash:可以将数据分段存储

不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall)

命令使用

禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令

合理使用select

redis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。

使用批量操作提高效率

注意控制一次批量操作的元素个数

原生命令:例如mget、mset。

非原生命令:可以使用pipeline提高效率。

1. 原生命令是原子操作,pipeline是非原子操作。
2. pipeline可以打包不同的命令,原生命令做不到
3. pipeline需要客户端和服务端同时支持。

Redis事务功能较弱,不建议过多使用,可以用lua替代

客户端使用

避免多个应用使用一个Redis实例

不相干的业务拆分,公共数据做服务化。

使用带有连接池的数据库

maxTotal

最大连接数,这个值不是越大越好,通过业务QPS进行预估可以比理论值大一些。不能超过redis的最大连接数maxclients。

maxIdle

业务需要的最大连接数,maxTotal是为了给出余量,所以maxIdle不要设置过小,否则会有new Jedis(新连接)开销。

连接池的最佳性能是maxTotal = maxIdle,避免连接池伸缩带来的性能干扰,maxIdle可以设置为按上面的业务期望QPS计算出来的理论连接数,maxTotal可以再放大一倍

minIdle

至少需要保持的空闲连接数,连接数超过了minIdle,那么继续建立连接,如果超过了maxIdle,当超过的连接执行完业务后会慢慢被移出连接池释放掉。

连接池预热

如果系统启动完马上就会有很多的请求过来,那么可以给redis连接池做预热,比如快速的创建一些redis连接,执行简单命令,类似ping(),快速的将连接池里的空闲连接提升到minIdle的数量。

统一将预热的连接还回连接池,最后执行close方法

Redis对于过期键清除

被动删除

当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key

主动删除

定期(默认每100ms)主动淘汰一批已过期的key,这里的一批只是部分过期key,所以可能会出现部分key已经过期但还没有被清理掉的情况,导致内存并没有被释放

前已用内存超过maxmemory限定时,触发主动清理策略

针对设置了过期时间的key做处理:

volatile-ttl:在筛选时,会针对设置了过期时间的键值对,根据过期时间的先后进行删除,越早过期的越先被删除。
volatile-random:就像它的名称一样,在设置了过期时间的键值对中,进行随机删除。
volatile-lru:会使用 LRU 算法筛选设置了过期时间的键值对删除。
volatile-lfu:会使用 LFU 算法筛选设置了过期时间的键值对删除。

针对所有的key做处理:

allkeys-random:从所有键值对中随机选择并删除数据。
allkeys-lru:使用 LRU 算法在所有数据中进行筛选删除。
allkeys-lfu:使用 LFU 算法在所有数据中进行筛选删除。

不处理:

noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时Redis只响应读操作。
LRU 算法

淘汰很久没被访问过的数据,以最近一次访问时间作为参考。

LFU 算法

淘汰最近一段时间被访问次数最少的数据,以次数作为参考。

当存在热点数据时,LRU的效率很好,但偶发性的、周期性的批量操作会导致LRU命中率急剧下降,缓存污染情况比较严重。这时使用LFU可能更好点。
根据自身业务类型,配置好maxmemory-policy(默认是noeviction),推荐使用volatile-lru。如果不设置最大内存,当 Redis 内存超出物理内存限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),会让 Redis 的性能急剧下降
当Redis运行在主从模式时,只有主结点才会执行过期删除策略,然后把删除操作”del key”同步到从结点删除数据

Redis队列与Stream

Stream

每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用xadd指令追加消息时自动创建。

Consumer Group

每个消费组 (Consumer Group) 的状态都是独立的,相互不受影响

last_delivered_id

每个 Stream 都可以挂多个消费组,每个消费组会有个游标last_delivered_id在 Stream 数组之上往前移动,表示当前消费组已经消费到哪条消息了。每个消费组都有一个 Stream 内唯一的名称 需要指定从 Stream 的某个消息 ID 开始消费,这个 ID 用来初始化last_delivered_id变量。

Consumer

同一个消费组 (Consumer Group) 可以挂接多个消费者,任意一个消费者读取了消息都会使游标last_delivered_id往前移动。

pending_ids

记录了当前已经被客户端读取,但是还没有 ack的消息。

消息 ID

timestampInMillis-sequence,可以由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面加入的消息的 ID 要大于前面的消息 ID

消息内容

形如 hash 结构的键值对

常用操作命令

xadd

追加消息

xrange

获取消息列表,会自动过滤已经删除的消息

xdel

删除消息,这里的删除仅仅是设置了标志位,不会实际删除消息。

xlen

消息长度

del

删除 Stream

xread

消费消息

count

“count 1”表示从 Stream 读取1条消息,缺省当然是头部

block

以阻塞的方式读取尾部最新的一条消息,直到新的消息的到来

xreadgroup

消费组消费

xack

确认一条消息

XPENDIING

获消费组或消费内消费者的未处理完毕的消息

XCLAIM

用以进行消息转移的操作,将某个消息转移到自己的Pending列表中

Redis队列几种实现

基于List的 LPUSH+BRPOP 的实现

基于Sorted-Set的实现

多用来实现延迟队列

PUB/SUB,订阅/发布模式

典型的广播模式,一个消息可以发布到多个消费者;

消息一旦发布,不能接收。擅长处理广播,即时通讯,即时反馈的业务。

基于Stream类型的实现

Redis中的线程和IO模型

Redis中的线程和IO概述

  1. Redis 基于 Reactor 模式开发了自己的网络事件处理器 - 文件事件处理器(file event handler,后文简称为 FEH),而该处理器又是单线程的,所以redis设计为单线程模型。

  1. 采用I/O多路复用同时监听多个socket,根据socket当前执行的事件来为 socket 选择对应的事件处理器。

  1. 当被监听的socket准备好执行accept、read、write、close等操作时,操作对应的文件事件就会产生,这时FEH就会调用socket之前关联好的事件处理器来处理对应事件。

  1. 所以虽然FEH是单线程运行,但通过I/O多路复用监听多个socket,不仅实现高性能的网络通信模型,又能和 Redis 服务器中其它同样单线程运行的模块交互,保证了Redis内部单线程模型的简洁设计

socket

文件事件就是对socket操作的抽象,每个操作对应不同文件事件

I/O多路复用程序

负责监听多个socket。

会将所有产生事件的socket放入队列, 通过该队列以有序、同步且每次一个socket的方式向文件事件分派器传送socket。

上一个socket产生的事件被对应事件处理器执行完后, I/O 多路复用程序才会向文件事件分派器传送下个socket,

I/O多路复用程序的实现

通过包装常见的 select、epoll、 evport 和 kqueue 这些 I/O 多路复用函数库实现的

文件事件分派器

接收 I/O 多路复用程序传来的socket, 并根据socket产生的事件类型, 调用相应的事件处理器

文件事件处理器

这些处理器是一个个函数, 它们定义了某个事件发生时, 服务器应该执行的动作。

文件事件的类型

AE_READABLE事件:当socket可读(比如客户端对Redis执行write/close操作),或有新的可应答的socket出现时(即客户端对Redis执行connect操作)

AE_WRITABLE事件:当socket可写时(比如客户端对Redis执行read操作)

客户端和Redis服务器通信的整个过程

Redis6.0之前的版本真的是单线程吗?

Redis在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。

但如果严格来讲从Redis4.0之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值