REDIS

缓存穿透

指查询一个一定不存在的数据,缓存不命中走DB,大量请求落到DB上把数据库压垮

解决方式

  1. 上游限流,如网关限流,接口调用次数限制

    RateLimiter(guava令牌桶限流)

    可用在网关上,可用在获取缓存的公共方法上 public static T cacheHelper(String key, Supplier supplier)

  2. 缓存空对象,这样可以请求不落DB,但业务层需要加上相应空值判断

  3. 布隆过滤器,内部存储不存在的key,但是在新加key时要去除布隆过滤器里相应的拦截位

缓存雪崩

redis挂了,全部请求走DB

解决方式

  1. 缓存高可用,降低全挂的概率

  2. 服务降级,分布式缓存挂了,走本地缓存

    如何保证本地缓存性?

    • 设定较短过期时间
    • 引入MQ机制,异步更新
  3. DB限流

    • Java Semaphore 控制并发量,达到并发数后阻塞等待
    • RateLimiter(guava令牌桶限流)加在获取缓存的公共方法上,缓存没命中读DB之前(supplier.get())先获取令牌,如果QPS超过了限制,那么就用30%概率响应失败,降低DB访问频率。对于用户来说,系统并没有死透,刷新几下就会成功一次
    • 开源框架 Hystrix (可限流,可降级,降成只返默认值、欢迎页等等)

缓存击穿

指某个极度热点数据在某个时间点过期时,恰好在这个时间点对这个 KEY 有大量的并发请求

解决方式

  1. 热点数据不设置过期时间,更新操作依赖于其他逻辑,比如后台管理系统

  2. 使用互斥锁,缓存不命中,查询 DB 前,使用分布式锁,保证有且只有一个线程去查询 、更新

    String get(String key) {
        String value = reids.get(key);
        if(value == null){
            String randomStr = "XXX";
            if(redis.set(nxKey, randomStr, "NX", "PX", 60)){
                value = db.get();
                if(value != null){
                    redis.setex(key, timeOut, value);
                }
            } else{
                Thread.sleep(10);
            	get(key);//未获取到锁的线程,等一下再试
            }
        }
        return value;
    }
    
  3. 不依赖redis过期策略,用程序判断过期更新,过期时间存在value里,发现快要过期时,使用互斥锁去更新(不太靠谱,因为可能那段时间就是没人请求)

    String get(String key) {
        String value = reids.get(key);
        boolean logicTimeOut = timeOut(value);//判断是否将要过期
        if(logicTimeOut){
            String randomStr = "XXX";
            if(redis.set(nxKey, randomStr, "NX", "PX", 60)){
                value = db.get();
                if(value != null){
                    redis.set(key, value);
                }
            }
        }
        return value;
    }
    

缓存数据一致性问题

引起一致性问题即数据库与缓存数据不一样,引原因展开来说有很多种情况,最主要是分清到底是哪种情况引发的问题,主要分为两类

一、并发更新,缓存覆盖

  1. 高并发下,读DB老数据,更新至缓存(覆盖了其他程序的更新)

    • A读缓存,未命中
    • A读取DB
    • B更新DB及缓存
    • A更新缓存(此时缓存和DB数据不一致)
  2. 缓存操作和DB操作不在同一个事务中(DB更新成功,缓存更新失败、DB更新失败,缓存更新成功)

    这种情况只能依赖分布式事务去解决了

解决方式

  1. 使用互斥锁,把并行写操作变成串行

    读操作时,若命中则直接返回,若缓存未命中,则先获取分布式锁,再去读取DB及更新缓存

    写操作时,先获取分布式锁,再去写

二、数据库、缓存双写过程存在部分失败(这里又可以区分成:删除缓存还是更新缓存,一般是删,懒加载,因为可能很长时间都不读)

  1. 先删缓存再更新DB(这种方式部分失败不会引起不一致,但会有以下情况产生不一致)

    • A删缓存
    • B读缓存,未命中
    • B读取数据库并更新缓存
    • A更新数据库,此时缓存内数据是由
  2. 先更新DB再删缓存

    • 更新DB成功
    • 更新缓存时失败,此时缓存内还是老数据,引发不一致

其实不管是什么操作都有可能引起不一致问题,最常用的解决方式就是

  1. 设置较短过期时间,依赖过期来去除脏数据
  2. 异步强制失效,更新后写人任务表或队列,执行删除key操作,让程序重新读,降低不一致概率
  3. 留个后门,比如管理后台,手动更新

Redis为什么这么快?

1、多路复用线程模型

2、数据结构优化

Redis线程模型

redis采用单线程多路复用文件处理器模型,类似Reactor模型,IO多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。

主要包含4个部分:

  • IO多路复用程序

  • 事件队列

  • 事件分发器

  • 事件处理器

    ​ 连接应答处理器

    ​ 命令请求处理器

    ​ 命令回复处理器
    在这里插入图片描述

处理过程:

  • 客户端 socket01 向 redis 的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该事件压入事件队列中。文件事件分派器从队列中获取该事件,交给连接应答处理器连接应答处理器创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联
  • 假设此时客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将事件压入队列,此时事件分派器从队列中获取到该事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联
  • 如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。

Linux epoll IO多路复用机制

1、通过epoll_create()创建eventPoll结构,包含“IO操作事件对象红黑树”、“已就绪IO操作事件双向链表”

2、通过**epoll_ctl()**向红黑树中添加/修改IO操作事件,同时将IO操作事件对象与网络驱动程序绑定

3、当绑定的事件发生时,通过回调,将IO操作事件对象回写进“已就绪IO操作事件双向链表”

4、调用epoll_wait()获取“已就绪IO操作事件链表”的节点数量,当数量大于0时遍历链表,操作IO

epoll_create()
| —— IO操作事件对象[红黑树]																
|		o
|	   /  \
|     o     o  <—— epoll_ctl()
|						   | 1、添加/修改[IO操作事件对象]
|						   | 2、将事件对象绑定到[网卡驱动程序]
|								 
| —— 已就绪IO操作事件链表	 [驱动程序] ——事件就绪——> ep_poll_calback()//事件添加至就绪链表
|	o <-> o <-> o						          <———————————————|	
|
|	int readyFdsCount = epoll_wait(); //获取“已就绪IO操作事件链表”的节点数量
|   for(int i = 0; i < readyFdsCount; i++){
|		// events[i].do io...    
|	}
  • 没有FD监听数量限制(客户端操作服务器时就会产生文件描述符(简称FD):writefds(写)、readfds(读)、和exceptfds(异常) )
  • 使用回调通知而不是轮询方式,不会随着FD数目的增加效率下降

Linux select多路复用机制

1、select用于阻塞监听一组IO操作的IO事件,发生IO事件则结束阻塞,若无事件变化则线程空闲

2、它只能监听变化,并不能知道到底具体是哪个IO事件发生了变化

3、监听IO操作数量有限制,默认1024个

while true {
    select(IO操作事件数组[]);
    for i in streams[] {
        if i has data
        read until unavailable	
    }
}

redis数据结构

string

简单动态字符串(simple dynamic string SDS),不直接使用C语言的字符串而是单独定义一个结构,增加已占用、剩余可用这两个属性,目的是为了减少判断

C语言字符串是以空白符为结束标记,这样带空白符的数据就不能用字符串存,现在可以直接通过len属性判断结尾,这样就没有了存储限制

C语言的字符串拼接要判断当前申请的空间是否能存的下,只能遍历字符串才能知道长度,现在可以通过free属性直接判断剩余可用空间

struct sdshdr {  
    int len;    // buf 中已占用空间的长度  
    int free;   // buf 中剩余可用空间的长度  
    char buf[]; // 字符数据数组
}

在这里插入图片描述

hash
  1. 所有元素长度都小于64字节,元素个数小于512时,使用zipList结构作为底层实现
  2. 不满足以上条件时,用hash结构

最主要的特点是使用两个桶结构,一个用来存储,一个用来扩容,即渐进式扩容模式

当桶全部存满以后触发扩容(还有其他条件),首先将计数器rehashidx设置为0,表示目前处于rehash状态,随后分配ht[1]空间,大小是原桶的两倍(即dictht中的table大小扩大两倍),当下一次对字典CRUD时,在ht[1]中进行操作的同时,找到ht[0]对应的桶位置,将对应位置上的数据向ht[1]迁移,rehashidx加一,当ht[0]全部迁移完毕(used数为0)则将ht[1]与ht[0]位置对换,同时rehashidx设为-1表示非rehash状态

typedef struct dict {
    dictType *type;// 类型特定函数
    void *privedata;// 私有数据
    dictht  ht[2];// 哈希表
    in trehashidx; // rehash 索引(标识)
}

typedef struct dictht {
   dictEntry **table;//哈希表数组(桶)
   unsigned long size; //哈希表大小
   unsigned long sizemask; //哈希表大小掩码,用于计算索引值
   unsigned long used; //该哈希表已有节点的数量
}

typeof struct dictEntry{
   void *key; //键
   union{     //值
      void *val;
      uint64_tu64;
      int64_ts64;
   }
   struct dictEntry *next;//下一个节点指针
}

在这里插入图片描述

zset
  1. 所有元素长度都小于64字节,元素个数小于512时,使用zipList结构作为底层实现(按照元素memeber和分值socore有序)
  2. 不满足以上条件时使用skiplist(跳表)+ hash作为底层实现,类似与TreeMap(hash + 红黑树),数据用hash存储,用跳表来实现有序,跳表的查询效率不如红黑树但也很高,插入的效率比红黑树高,属于综合性能比较好的数据结构

Set底层实现也是分两种

  1. 元素都是整数、元素个数少于512时用intSet作为底层实现
  2. 不满足以上条件时,使用hash作为底层实现,key作为set元素的value,value为null

skiplist

跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。具有如下性质:

  1. 由多层结构组成
  2. 每一层都是一个有序的链表,排列顺序为由高层到底层,都至少包含两个链表节点,分别是前面的head节点和后面的null节点
  3. 最底层的链表包含了所有的元素
  4. 如果一个元素出现在某一层的链表中,那么在该层之下的链表也全都会出现(上一层的元素是当前层的元素的子集
  5. 每个节点包含上下左右4个指针
  • 搜索:从最高层的链表节点开始,如果比当前节点要大和比当前层的下一个节点要小,那么则往下找,也就是和当前层的下一层的节点的下一个节点进行比较,以此类推,一直找到最底层的最后一个节点,如果找到则返回,反之则返回空
  • 插入:首先通过随机法确定插入的层数,插入时要保持前后左右的节点关联性。
  • 删除:在各个层中找到包含指定值的节点,然后将节点从链表中删除即可,如果删除以后只剩下头尾两个节点,则删除这一层

在这里插入图片描述

zipList
在这里插入图片描述

  • zlbytes: ziplist的长度(单位: 字节),是一个32位无符号整数

  • zltail: ziplist最后一个节点的偏移量,反向遍历ziplist或者pop尾部节点的时候有用。

  • zllen: ziplist的节点(entry)个数

  • entry: 节点

  • zlend: 固定值0xFF,用于标记ziplist的结尾
    在这里插入图片描述

  • prevlengh: 记录上一个节点的长度,为了方便反向遍历ziplist

  • encoding: 当前节点的编码规则,下文会详细说

  • data: 当前节点的值,可以是数字或字符串

过期策略与淘汰策略

过期策略

Redis 提供了 3 种数据过期策略并同时使用!,它们是非互斥

  • 被动删除:当读/写一个已经过期的 key 时,会触发惰性删除策略,直接删除掉这个过期 key 。
  • 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以 Redis 会定期主动淘汰一批已过期的 key 。
  • 主动删除:当前已用内存超过 maxmemory 限定时,触发主动清理策略,即**「淘汰策略」**
淘汰策略

redis.conf -> maxmemory这个值来开启内存淘汰功能

# maxmemory 0 设置最大内存使用量,如果这个值设置成0,代表无内存使用限制

# maxmemory-policy noeviction 设置淘汰策略 默认是什么?

从已设置过期时间的数据集中挑选最近最少使用的数据淘汰(随机挑选)当内存达到限制的时候无法写入非过期时间的数据集

1、volatile-lru

从已设置过期时间的数据集中挑选最少使用的数据淘汰(随机挑选)当内存达到限制的时候无法写入非过期时间的数据集

2、volatile-ttl

从已设置过期时间的数据集中挑选将要过期的数据淘汰(随机挑选)当内存达到限制的时候无法写入非过期时间的数据集

3、volatile-random

从已设置过期时间的数据集中任意选择数据淘汰

4、allkeys-lru

从数据集中挑选最近最少使用的数据淘汰。当内存达到限制的时候,对所有数据集挑选最近最少使用的数据淘汰,可写入新的数据集

5、allkeys-random

从数据集中任意选择数据淘汰,当内存达到限制的时候,对所有数据集挑选随机淘汰,可写入新的数据集

6、no-enviction

当内存达到限制的时候,不淘汰任何数据,不可写入任何数据集,所有引起申请内存的命令会报错

Cluster

cluster适用场景 高可用、海量数据、横向扩容,如果数据量不大,用主从 + 哨兵sentinel就行(哨兵如何读写分离)

什么时候整个集群不可用?

  • 如果集群任意master挂掉且当前master没有slave集群进入fail状态,因集群的slot映射[0-16383]不完整,cluster-require-full-coverage参数(兼容部分失败)默认关闭
  • 集群超过半数以上master挂掉,无论是否有slave,集群进入fail状态.

集群元信息采用非集中式,即每个节点都有集群完整信息,采用gossip协议进行集群通信(展开),节点采用哈希槽机制,每个节点占用一定范围的槽位,共16383(2的14次方)

redis-cli请求集群时,集群首先用CRC16算法对key的有效部分(即用"{}"扩上的部分)进行计算(n & 16383)算出对应槽位,并判断当前节点是否匹配这个槽位,如果不是,则向客户端回复重定向信息(MOVED{slot}{ip}{port}格式)客户端再次发起请求(注意:集群模式mget这种批量操作如果键槽比较分散会查询较慢需要优化)

故障转移过程,集群节点故障后其他节点如果发现心跳失败,则对其进行主观下线,并且把这个消息在集群内广播,当超过一半的节点都发现主观下线时,会升级为客观下线,此时如果这个节点有从节点,则在从节点里选一个作为主节点,选举方式raft,选完主节点将触发旧的主节点替换,即把槽位委派给自己,并且向集群内广播,通知自己是主节点

节点的新增和删除
新增:

  1. 创建新主节点
  2. 调用ruby脚本 ruby redis-trib.rb add-node (新)192.168.127.130:7006 (原集群任意主节点)192.168.127.130:7000
  3. 选择一个主节点并从其上面抽取一部分槽位,会提示输入抽取槽位数量、接收槽位的节点ID(槽位来源某个节点还是所有节点?) ruby redis-trib.rb reshard (原集群任意主节点,因为只有主节点知道集群信息)192.168.127.130:7000

删除:

  1. 迁移待删除节点的槽位至其他主节点,ruby redis-trib.rb reshard (待删除节点)192.168.127.130:7006 会提示输入分配的槽位数量、接收槽位的节点ID
  2. ruby redis-trib.rb del-node (待删除节点)192.168.127.130:7006 71ecd970838e9b400a2a6a15cd30a94ab96203bf
Cluster模式下的批量操作的优化
  • 一种是使用**”{}”来将要hash的key的部分**包裹起来,rediscluster写入数据时只会对key中被”{}”包裹部分进行哈希取模计算slot位置。即存入时使用 “a{123}”和”b{123}”是在同一个slot上。这样就可以批量读取存放在同一个slot上的数据。

  • 第二种方法是在批量读取时,先计算所有数据的存放节点。具体做法是,我们已经知道了rediscluster对数据哈希取模的算法,可以先计算数据存放的slot位置,可以通过jedis.clusterSlots()方法知道每个节点分管的slot段。这样就可以通过key来计算出数据存放在哪个节点上。然后根据不同的节点将数据分成多批。对不同批的数据进行分批pipeline处理。

cluster集群详细搭建过程

https://www.cnblogs.com/PatrickLiu/p/8458788.html

Sentinel

Redis-Sentinel是Redis官方推荐的高可用性(HA)解决方案,Master-slave的模式时主节点宕机从节点并不会自动切换。Redis-sentinel能监控多个master-slave集群,**发现Master宕机后,sentinel集群进行选举,选举出一个sentinel,根据Master配置找到Slave节点并选举其中一个升级为新的主节点(会修改相关节点的配置文件)来实现自动切换,主从切换统筹工作由单个sentinel去做。**sentinel本身也是一个redis进程,当使用哨兵模式时,客户端就不再直连redis服务机器了,而是连接哨兵机器

  • sentinel侧重于高可用,而cluster侧重于存储与扩展(cluster相当于在哨兵基础上增加了扩展性)
  • 当Master宕机时才会触发切换
  • sentinel自身的高可用至少要有3个节点,在选举leader时必须超过50%的节点投票才能选出来,如果只有2个节点,挂了一个后sentinel集群直接不可用
  • 集群模式也好单个哨兵也好,配置文件内只需配置要监控的Master节点,无需配置其他哨兵节点,它可以自动识别监控该Master的其他哨兵,并维护到一个列表中,这个列表保存了 Sentinel 已知的,监视同一个主服务器的所有其他Sentinel。

应用场景

1、【ZSET】榜单应用 TopN 游戏分数排行

redis 120.0.0.1:6379> zadd game 98 lucy

redis 120.0.0.1:6379> zadd game 99 lilei

redis 120.0.0.1:6379> zadd game 85 poly

取所有人的分数

redis 120.0.0.1:6379> ZREVRANGE game 0 -1

1> “lilei”

2> “lucy”

3> “poly”

获取Top2

redis 120.0.0.1:6379> ZREVRANGE game 0 1

1> “lilei”

2> “lucy”

2、【LIST】最新操作(最新评论、最新关注的粉丝)

利用list结构来做,可以从头插入元素也可以从尾部插入,再利用“ltrim”来截取/保留指定长度的数据,这就可以实现“最新”操作。

redis 120.0.0.1:6379> RPUSH mylist one

redis 120.0.0.1:6379> RPUSH mylist two

redis 120.0.0.1:6379> RPUSH mylist three

redis 120.0.0.1:6379> LREANGE mylist 0 -1

1> “one”

2> “two”

3> “three”

redis 120.0.0.1:6379> LTRIM mylist 0 1

redis 120.0.0.1:6379> LREANGE mylist 0 -1

1> “one”

2> “two”

3、【INCR】计数-统计某用户点击量/访问频率

不同数据结构的计数命令不同,拿set结构举例,它使用“scard”命令获取集合内元素的数量

redis 120.0.0.1:6379> SET login_times 0

redis 120.0.0.1:6379> INCR login_times

redis 120.0.0.1:6379> GET login_times

redis 120.0.0.1:6379> 1

4、消息队列

Redis只提供了阻塞获取,因队列长度是无限的,所以不存在队列存满需等待消费者消费的情况,即只提供阻塞版本的获取即可。

生产者/消费者模式(PUSH/POP)

基于阻塞队列实现

BRPOP key [key …] timeout 超时时间内,从队列右侧阻塞式弹出元素

BLPOP key [key …] timeout

(可先生产再消费,也可消费者先阻塞获取等待生产者入队)

入队

127.0.0.1:6379>RPUSH my_list “Spark” “hadoop”"hive"

(integer) 3

127.0.0.1:6379>LRANGE my_list 0 -1

1)“Spark”

2)“Hadoop”

3)“Hive”

出队

127.0.0.1:6379>BLPOP my_list other_list1 other_list2 60

1)“my_list1” #执行弹出操作的列表

2)“Spark” #被弹出的项

发布-订阅模式(PUB/SUB)

基于Redis自身提供了PUB/SUB机制

SUBSCRIBE my_channel_1 [my_channel_2 …]
订阅给定的一个或多个频道的信息

PUBLISH my_channel_1 message
将信息message发送到指定频道my_channel_1

ClientA

127.0.0.1:6379>SUBSCRIBE my_chnnel_1

Redinging message… <press Ctrl + c to quit>

(等待生产者发布…,发布完成,收到消息)

1)“subscribe”

2)“chennel”

3)“1”

1)“message”

2)“my_chennel_1”

3)“this is my message!”

ClientB

127.0.0.1:6379>PUBLISH my_chnnel_1 "this is my message!"

(integer) 1

https://github.com/doocs/advanced-java
https://github.com/Snailclimb/JavaGuide
https://github.com/CL0610/Java-concurrency

SH my_channel_1 message**
将信息message发送到指定频道my_channel_1

ClientA

127.0.0.1:6379>SUBSCRIBE my_chnnel_1

Redinging message… <press Ctrl + c to quit>

(等待生产者发布…,发布完成,收到消息)

1)“subscribe”

2)“chennel”

3)“1”

1)“message”

2)“my_chennel_1”

3)“this is my message!”

ClientB

127.0.0.1:6379>PUBLISH my_chnnel_1 "this is my message!"

(integer) 1

https://github.com/doocs/advanced-java
https://github.com/Snailclimb/JavaGuide
https://github.com/CL0610/Java-concurrency

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值