一.为什么是使用Redis

1.为什么使用redis作为MySQL的缓存

主要因为redis具备高性能和高并发的两种特性

一.为什么是使用Redis

1.为什么使用redis作为MySQL的缓存

主要因为redis具备高性能和高并发的两种特性

1.redis具有高性能

redis可以缓存MySQL的数据到内存中,供下一次用户获取,在内存中获取相较于在磁盘中快了很多。

mysql数据改变时,同步reids缓存数据即可,但是存在双写一致性问题

2.redis具有高并发

单台设备的Redis的QPS(Query Per Second,每秒钟处理完请求的次数)是MySQL的10倍,Redis单机的QPS能轻松破10w,

MySQL单机的QPS很难破1w。

所以,直接访问Redis能够承受的请求是远远大于直接访问MySQL的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的部分请求会直接到缓存这里而不用经过数据库

2.Redis 和 Memcached 有什么区别?

相同点:

  • 都是基于内存的数据库,一般都用来做缓存

  • 都有过期策略

  • 两者性能都很高

区 别 :

  • redis的数据类型更丰富,memcached只有key-value数据类型

  • redis支持数据持久化,而memcached不支持

  • redis支持集群模式,memcached没有原生的集群模式

  • redis支持发布订阅模式、lua脚本、事务等,而memcached不支持

二.Redis定义

1.什么是Redis?

redis是一种基于内存的非关系型数据库Nosql(Not Only Sql),是一种高级的key-value的存储系统,读写都在内存中完成,读写速度很快,常用于缓存,消息对列,分布式锁等;

redis支持的数据类型较多,且将信息存储在内存上,并且会定期刷新到磁盘或记录文件上,实现主从同步功能;

1.1 Redis的数据类型

键(key):
  • 键值不能重复

  • 用来标识存储的数据

  • String字符串类型

  • 规则

    • 不能太长,影响查询效率

    • 不能太短,容易重复,同时可读性也比较差

    • 规范:Student_Name_List

值(value)

redis常见的五种数据类型:

  1. String(字符串)
    • 存储类型:字符串、整数、浮点数value最长可容纳512M

      • String类型的底层数据结构是int和SDS(简单动态字符串)

      • 不同于C语言字符,SDS不仅可以保存文本数据还可以保存二进制数据(图片、音频、视频、压缩文件)

      • SDS获取字符串长度的时间复杂度为O(1),通过len属性记录字符串的长度

      • Redis的SDS的Api是安全的,拼接字符串不会造成缓存区溢出(拼接字符串时会先检查SDS空间是否够,如果不够会自动扩容)

      • 有三种编码(encoding)格式:int、raw和embstr(只读,要修改先转为raw);(redis2.+是32字节,redis3.0-4.0是39字节,redis5.0是44字节)

        • 如果保存的是整数值,则将整数值保存在字符串对象的ptr属性里,并将编码设置为int

        • 如果保存的是字符串,且长度≤32字节,则使用SDS保存,并将编码格式设置为embstr,embstr通过一次内存分配函数来分配一块连续的内存空间保存redisObject和SDS(分配和释放都只需要调用一次函数,且所有的数据保存在一块连续的内存中,可以更好的利用CPU缓存提升性能)

        • 如果保存的是字符串,且长度>32字节,则使用SDS保存,并将编码格式设置为raw,raw通过2次内存分配函数来分配2块内存空间保存redisObject和SDS

    • 读写能力:对整个字符串或一部分进行操作;对整数或浮点数进行自增或自减

    • 场景:缓存对象、常规计数、分布式锁、共享session 信息等

      • 缓存对象:

        • 直接缓存整个对象的Json

        • 将key进行分离为user:ID:属性,采用MSet存储

      • 常规计数:

        因为Redis处理命令是单线程,所以执行命令的过程是原子的。因此String数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。

      • 分布式锁:

      • 共享Session信息:

        • redis对用户的Session信息进行统一的存储和管理,无论请求发送到哪台服务器下,服务器都会从同一个redis获取相关的Session信息,这就解决了分布式系统下的Session存储问题
    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 功能
    set 键 值 添加或修改一个键和值,键不存在就是添加,存在就是修改
    get 键 获取值,如果存在就返回值,不存在返回nil(就是C语言中NULL)
    del 键 删除指定的键和值,返回删除的个数
    SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒。此处的value是指key对应的value值。等价于:SET key value ex seconds
    EXPIRE key seconds 如果一个key已经存在,要设置一个过期时间
    SETNX key value/set key value nx 保存键值对,如果key存在则不保存,不存在则保存
    MSET key1 value1 key2 value2 批量设置 key-value 类型的值
    MGET key1 key2 批量获取多个 key 对应的 value
    INCR/DECR number 将 key 中储存的数字值增一/减一
    INCRBY/DECRBY
    number 10
    将key中的数字增加10/减少10
  2. List(列表)
    • 存储类型:链表、链表上每个节点都包含一个字符串,列表最大长度为2^32-1,每个列表超过40亿个元素

      • List类型的底层数据结构是由双向链表或压缩列表实现的

        • 如果列表中元素个数<512个(默认值,list-max-ziplist-entries配置),每个元素的值小于64字节(默认值,list-max-ziplist-value配置),此时Redis就会使用压缩列表[缺点:1.查询时需要遍历查询,2.插入或修改元素时,由于内存连续会导致元素后移(类似页分裂)] 作为底层数据结构

        • 反之就会使用`作为底层数据结构(Redis3.2之后,用quicklist取代了双向链表和压缩列表

    • 读写能力:对链表两端进行push和pop操作,读取单个或多个元素,根据值查找或删除元素

    • 实际应用:微信朋友圈点赞,要求按照点赞顺序显示点赞好友信息。如果取消点赞,移除对应好友信息。

    • 场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)

    • 消息对列:需要满足:1.消息保序、2.处理重复消息、3.消息的可靠性

      • 1.List本身就是先进后出的顺序存取,Lpush+Rpop 或Rpush+pop实现消息对列,实现了消息对列保序

        • 缺点:新消息写入不会通知,需要一直执行接收程序,消耗cpu资源,使用BRpop(阻塞式读取)——没有读到新消息自动阻塞,直到有新消息写入对列,再重新读取数据
      • 2.需要自行给一个全局唯一ID,List插入消息的时候,包含上这个唯一ID

      • 3.List消息被读取后就会自动删除,所以使用BRPOPLPUSH保证读取过的消息再插入另一个备份List中留存

    • List的缺陷:不支持多个消费者消费同一条数据,不支持消费组的实现;

    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    lpush 键 元素 元素... left 从左边向指定的键中添加1个或多个元素,返回列表中元素的个数
    rpush 键 元素 元素... right 从右边向指定的键中添加1个或多个元素
    lpop 键 从左边删除一个元素,返回被删除的元素
    rpop 键 从右边删除一个元素,返回被删除的元素
    lrange 键 开始 结束 得到键中指定范围的元素的数据 每个元素都有一个索引号,从左向右0~n 从右向左索引号:-1~-(n+1),每个元素有2个索引号 如果要取出整个列表中所有的元素,索引号应该是:0~-1
    lindex 键 索引值 查询指定索引的元素
    llen 键 获取列表的长度
    BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止,超时时间单位默认是秒
    LREM key 删除元素个数 value值 从表头删除指定个数的元素
  3. Hash(哈希)
    • 存储类型:包含键值对的无序散列表,类似于Java中Map

      • 底层数据结构是压缩列表或哈希表实现的

        • 如果列表中元素个数<512个(默认值,list-max-ziplist-entries配置),每个元素的值小于64字节(默认值,list-max-ziplist-value配置),此时Redis就会使用压缩列表作为底层数据结构(redis7.0之后,由listpack替代压缩链表)

        • 反之就会使用哈希表作为底层数据结构

    • 读写能力:包含方法有添加、获取、删除单个元素

    • 场景:缓存对象、购物车等

      • 场景与String+Json的形式一致,但是如果对象中的属性频繁变化就需要使用Hash来存储例如购物车

      常用命令

      td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
      命令 功能
      hset 键 字段 值 添加键,字段,值
      hget 键 字段 通过键,字段得到值
      hmset 键 字段 值 字段 值 multiply多个,一次添加多个字段和值
      hmget 键 字段 字段 通过键,获取多个字段和值
      hdel 键 字段 字段 删除一个或多个字段的值
      hgetall 键 得到这个键下所有的字段和值
      HKEYS 键 获取哈希表中所有字段
      HVALS 键 获取哈希表中所有值
  4. Set(集合)
    • 存储类型:包含字符串的无序并且唯一的集合,集合最多可存储2^32-1个元素,超过40亿个元素

      • 底层数据结构是由哈希表或整数集合实现的

        • 如果集合中的元素都是整数且元素个数小于512个(默认值,set-maxintset-entries配置),redis使用整数集合作为set的底层结构

        • 不满足上面条件则选择哈希表作为底层数据结构

    • 读写能力:字符串的集合,包含方法有添加、获取、删除单个元素,包含计算交集、并集、差集(数据量较大时,不宜使用计算,会导致redis实例阻塞)

    • 场景:聚合计算(并交差),点赞,共同关注,抽奖

      • 用来缓存点赞的场景,一个用户只能点赞一次,

      • 还有朋友圈的共同好友点赞记录,以及视频号的共同好友推荐视频

      • 抽奖,可以保证一个用户只中一次奖

    常用命令

    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    sadd 键 元素 元素... 向一个键中添加1个或多个元素
    smembers 键 得到这个集合中所有的元素
    sismember 键 元素 判断指定的元素在集合中是否存在,存在返回1,不存在返回0
    srem 键 元素 元素... 通过键删除一个或多个元素
    sinter key1 key2 返回给定所有集合的交集(集合中都共有的部分)
    sdiff key1 [key2] 差集运算
    srandmember key1 count 从key1中随机选出count个元素,元素不从key1中删除
    spop key1 count 从key1中随机选出count个元素,元素从key1中删除
  5. Zset(有序集合)
    • 存储类型:和散列一样,用于存储键值对,相当于set类型多了个排序属性

      • 底层数据结构是由压缩列表或跳表实现的

        • 如果集合元素的个数小于128个,并且每个元素值都小于64字节,则使用压缩列表作为底层数据结构

        • 反之使用跳表作为底层数据结构

          • 基于单链表的高级数据结构(二分查找的思想),最大32级

          • 伪头结点作为所有层级的头结点,他的指针数组长度就是跳表的最大层级数

          • 每个跳表节点都有一个指针数组,指针数组中的第L个指针指向下一个≥他的指针元素

          • 插入和删除根据概率随机到某一层,生成一个以层级为基础的数组,插入进原链表中,删除时只需要找到这个索引就可以直接定位到数据进行删除

          • 查找插入删除的时间复杂度都为O(logn)

          • 空间复杂度为O(n),可以通过调整抽取节点的间隔来平衡时间和空间的复杂度

    • 读写能力:对整个字符串或一部分进行操作;对整数或浮点数进行自增或自减

    • 场景:排序场景;

      • 排行榜、电话、姓名排序
    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    zadd 键 分数 值 分数 值 添加1个或多个元素,每个元素都有一个分数
    zrange 键 开始索引 结束索引 获取指定范围的元素,得到所有的元素,索引是0到-1
    zrange 键 开始索引 结束索引 withscores 查询指定的元素和对应的分数
    zrevrange 键 开始索引 结束索引 withscores 按照分数倒叙获取指定的元素和对应的分数
    zrem 键 值 值 删除一个或多个值
    zcard 键 得到元素个数
    zrank 键 值 得到元素的索引号
    zscore 键 值 得到元素的分数
    zrangebyscore key min max withscore limit offset count 返回指定集合范围的元素,分数有高到低排序
    ZUNIONSTORE destkey numberkeys key1 [key2...] weight number1 number2 将key1和key2的值乘上number1和number2,结果集相加存放在destkey中
  6. Bitmap
    • 是由String类型作为底层数据结构实现的一种统计二值状态的数据类型(bit数组)

    • 场景:签到打卡,判断用户登录状态,布隆过滤器

      • 签到:通过用户唯一ID加上年份月份,统计这个月份的签到情况

        setbit id:sign💯202206 0 1(6月1日签到成功),可以使用

        bitpos id:sign💯202206 1统计6月第一次签到的时间

    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    setbit key offset value 设置值,value只能是0和1
    getbit key offset 得到元素
    bitcount key start end 获取指定范围内值为1的个数
    AND 与运算 &
    OR 或运算 |
    XOR 异或 ^
    NOT 取反 ~
    operations位移操作符
    bitop operations result key 运算,运算的结果result存在key中
    bitpos key value key中第一次出现value的位置
  7. HyperLogLog
    • redis2.8.9新增的数据类型,用于统计基数——统计一个集合中不重复的元素个数(hyperloglog统计规则不是很准确,标准误差为0.81%)

    • 只需要12kb内存就可以计算2^64个不同元素的基数

    • 应用场景:百万级网页UV(unique visitor)计数

      • 百万级网页UV:使用pfadd将访问页面的每个用户都添加到hyperloglog中,再使用pfcount直接获取page1中的UV值
    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    pfadd key element 添加1个或多个元素
    pfcount key 返回给定的hyperloglog的基数估算值
    pemerge destkey sourcekey 将多个hyperloglog合并为一个hyperloglog
  8. GEO
    • 是redis3.2新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作,GeoHash编码将经纬度进行权重分数的转换到Zset元素(1.对二维地图做区间划分;2.对区间进行编码)

    • 底层数据结构为 Zset

    • 场景:滴滴打车,附近的人

      • 先geoadd保存车辆的经纬度集合,然后用户georadius就可以调用周围几公里内的车辆信息,返回数据给LBS应用(Location-Based Service)
    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    geoadd longitude latitude member 存储指定的地理空间位置,经度(longitude)纬度(latitude)位置名(member)
    geopos key member 从给定的key中返回指定名称的经纬度信息
    geodist key member1 member2 [m|km|ft|mi] 返回两个给定位置之间的距离
    georadius key longitude radius 根据用户给定的经纬度坐标来获取范围内的地理位置集合
  9. Stream
    • redis5.0新增功能,专为消息队列设计的数据类型使用listpack和redix tree

    • 为了解决消息的可靠性,消息读后删除不可重复读取以及全局唯一ID需要自行实现

    //*表示生成唯一ID
    Xadd mymq * name xiaolin age 12; 
    xread streams mymq (*生成的唯一ID)
    //可以实现消息队列
    
    xread block 10000 streams mymq $
    //可以实现BRpop的阻塞读取操作
    
    • 1.解决了多个消费者不能消费同一个消息的问题

      • Xgroup创建消费组,xreadgroup可以使消费组内的消费者读取信息
      //创建名为group1的消费组,0-0表示从第一条数据读取
      xgroup create mymq group1 0-0
      //创建group2
      xgroup create mymq group2 0-0
      //group1的消费者consumer1从mymq读取所有消息
      xreadgroup group group1 consumer1 streams mymq
      
      • 同一个消费组的成员不能读取同一条消息,但是其他组可以(前提是创建消费组时指定了相同的读取位置)
    • 2.消息的可靠性

      • stream会自动使用内部队列(pending list)留存消费组里每个消费者读取的消息,知道消费者使用XACK命令通知streams消息处理完成,

      • xACK mymq group2 1654256265584-0,通知后这条消息就会被streams删除

      • 消费者可以在重启后,用xpending查询已读取、但尚未确认处理完成的消息

      //查看为处理完成的消息
      XPENDING mymq group2
      //查看 group2 里 consumer2 已从 mymq 消息队列中读取了哪些消息
      XPENDING mymq group2 - + 10 consumer2
      
    • 场景:作为消息队列,专业的消息队列要做到1.消息不丢失,2.消息可堆积

      • redis在AOF持久化配置每秒写盘和主从复制都是异步的,出现意外都会有丢失数据的可能

      • redis本身数据存储在内存中,面对消息挤压内存空间会紧张,且会出现丢数据;

      • redis发布/订阅机制没有基于任何数据类型实现,所以不具备持久化能力,且发后既忘,不能消费历史数据,当消息挤压到32M或在60s内维持在8M以上,消费端就会强行断开

三 Redis的线程模型

为什么说redis是单线程的?

Rdis单线程=>

「接收客户端请求->解析请求->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的

在redis2.6,会启动两个线程,分别用来处理关闭文件和AOF刷盘在redis4.0之后,新增了一个后台线程(lazyfree)用来异步释放redis内存(flushdb async flushall async)

  • 这些任务都是放在各自的任务队列中的,由消费者轮询队列,对任务执行对应的方法

I/O多路复用

  • I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。

有select、poll和epoll三种实现

  • select和poll的通知方式有问题,只会通知用户进程有socket就绪,但是不确定具体哪个socket就绪

  • epoll它会在通知用户进程就绪的同时,将已就绪的socket写入用户空间,不需要遍历socket来判断是否就绪,提升了性能

  • I/O多路复用是指单个线程来同时监听多个socket,并在某个socket可读、可写时得到通知,从而避免无效等待,充分利用cpu资源,目前都是用epoll模式

在redis6.0之前,epoll+事件派发机制

  • 1.调用epoll_create()创建一个epoll对象和调用socket()创建一个服务端socket

  • 2.调用bind()绑定端口和调用listen()监听该socket;

  • 3.将调用epoll__ctl()将listen socket加入到epoll,同时注册「连接事件」处理函数。

  • 上述初始化完成,主程序进入事件循环函数

    • 调用处理发送队列函数,查看发送队列是否有任务,有则通过write函数将客户端缓冲区的数据发送出去,没有没有发送完则会注册epoll_wait发现可写后再处理

      • 调用epoll_wait函数等待事件到来

        • 如果是连接事件,则调用连接事件处理函数:调用accpet获取已连接的socket=》调用epoll_ctl将已连接的socket加入epoll=》注册读事件处理函数

        • 如果是读事件,则调用读事件处理函数:调用read获取客户端发送的数据=》解析命令=》处理命令(6.0之后在此处加入多线程)=》将客户端对象添加到发送队列=》将执行结果写到发送缓冲区等待发送

        • 如果是写事件,则调用写事件处理函数:(此处加入多线程)通过write函数将客户端发送缓存区里的数据发送出去,如果这一轮没有发送完,就会继续注册写事件处理函数,等待epoll_wait发现可写后再处理

为什么单线程的还那么快?

  • redis基本在内存中操作

  • redis的单线程模式可以避免多线程之间的竞争

  • redis采用了I/O多路复用机制处理大量的Socket请求,即一个线程处理多个IO流,允许存在多个监听Socket和已连接socket,内核监听这些socket连接请求和数据请求,一旦请求到达就会交给redis线程处理

为什么要使用单线程?

  • CPU 并不是制约 Redis 性能表现的瓶颈所在,更多的是受到内存大小和网络I/O的限制,所以redis可以使用单线程,想要多核cpu,可以在一台服务器上启动多个节点或分片集群

  • 使用单线程可维护性高,多线程会增加系统复杂度,也会存在线程切换,甚至死锁的可能

为什么又引入了多线程?

在redis6.0之后也采用了多个I/O线程处理网络请求,因为硬件的提示,网络I/O也会出现一些瓶颈问题。

所以为了提高网络I/O的并行度,Rdis6.0对于网络/O采用多线程来处理。但是对于命令的执行,Redis仍然使用单线程来处理,所以大家不要误解Redis有多线程同时执行命令。

Redis官方表示,Redis6.0版本引入的多线程/o特性对性能提升至少是一倍以上(需要io-threads-do-reads 设为yes)

redis6.0后,默认额外创建6个线程

  • redis-server:redis主线程,主要负责处理命令

  • bio_close_file(异步处理关闭文件任务)、bio_aof_fsync(AOF刷盘任务)、bio_lazy_free(释放内存任务)

  • io_thd1、io_thd2、io_thd3:三个l/O线程,io-threads默认是4,所以会启动3(4-1)个/O多线程,用来分担Redis网络/O的压力。

四 Redis的持久化

由于redis的读写都是在内存中的,所以一旦宕机,redis的数据就会消失

所以为了保证数据不丢失,redis会将数据存储到磁盘,保证redis重启后可以恢复数据

三种持久化方式:

1.AOF日志:

  • (增量数据)每执行完一条操作命令,就会把该命令以追加的方式写入一个文件中

    • 好处:

      • 避免额外的检查开销:如果先将操作写入AOF,那么不能确定是否有语法错误,还要进行语法检查

      • 不会阻塞当前写操作命令的执行:因为当写操作命令执行成功才会将命令记录到AOF日志

    • 坏处:

      • 数据可能丢失:在记录到AOF之前宕机了,就会丢失这个数据

      • 因为写入AOF也是在主线程上的,所以会阻塞后续的其他操作

1.1.AOF写回策略:

  • 执行完写操作将命令追加到server.aof_buf缓冲区,然后由系统调用write()将缓冲区数据写入AOF文件,此时数据在page cache中,由内核决定什么时候刷盘到硬盘(有三种策略:always:每次写操作执行完就同步AOF到硬盘;Everysec:等待一秒再将缓存写入硬盘;No:由操作系统自行决定何时写回硬盘

  • AOF日志过大,触发AOF重写机制:

    • 当AOF日志文件大小超过设定的阈值,就会读取当前数据库的所有键值对,每个键值对用一个命令记录到AOF文件中,记录完成后就可以压缩过的新AOF文件替换旧文件了

1.2.AOF的重写过程:

  • 是由父进程fork的子进程(bgrewriteaof)完成的,可以避免阻塞主进程,可以调高效率,不必加锁保证数据安全。

  • 在AOF过大时会触发主进程创建子进程重写AOF,子进程是共享物理内存的,子进程对内存只读,将所有数据转成命令记录到新的AOF;

  • 但是当父进程或子进程对内存发起了修改操作,就会触发写保护中断,就会将修改的物理内存复制,重新设置内存映射关系,再将父子进程设置为可读写,最后才对内存进行写操作————写时复制就是发生写操作时才会复制物理内存)

  • 因为重写过程中主进程仍可以执行命令,如果重写时主进程修改了键值,则redis会将这个写命令写入到AOF缓冲区和AOF重写缓冲区(在fork完子进程之后才开始使用)

  • 最后重写日志完成,子进程异步发送一个信号给主进程,主进程收到信号,就会调用函数将AOF重写缓冲区的内容追加进新的AOF文件中,然后将新的AOF改名,替换旧的AOF文件,完后文件压缩过程(重写AOF)

2.RDB快照(默认的方式):

  • (全量快照)将某一时刻的内存数据,以二进制的方式写入磁盘

  • 使用save和bgsave两个命令,save在主进程操作,会阻塞主进程,bgsave会创建一个子进程生成RDB文件,避免主进程阻塞

  • 获取通过配置文件设置RDB的触发条件save 900 1(900s内执行一次修改)

  • RDB也有写时复制,与AOF相同

3.混合持久化方式:

redis4.0新增的,集成AOF和RDB

  • 因为RDB是全量的,数据写入较慢,容易丢失,但是重启后redis读取很快,而AOF只追加写操作,记录写入较快,数据不容易丢失,但是redis重启读取较慢

  • 所以将AOF文件划分两部分,一部分记录时以RDB二进制形式写入AOF,另一部分重写操作以AOF形式记录,这样即保证了重启数据恢复速度,又尽量保证了数据的完整性

  • 降低了AOF文件的可读性,开启后不能兼容redis4.0之前的版本了(因为AOF文件格式不一致)

五 Redis集群

1.主从复制模式

是redis高可用服务的基础,采用读写分离与mysql相似

1.1第一次主从同步:

  • 要确定谁是主服务器,使用replicaof(redis5.0之后使用slaveof)命令
//服务器B执行这条命令
replicaof <服务器A的IP地址> <服务器A的Redis端口号>
//此时服务器B就会变成A的从服务器
  • 第一阶段:建立链接,协商同步

    • 从服务器发出psync命令,向主服务器申请runID[嗲一次为?](redis服务器唯一标识)和offset[第一次为-1](复制进度)

    • 主服务器接到命令,用fullresync命令响应,将本机runID和offset传递过去

    • 目的为了全量同步复制做准备;

  • 第二阶段:主服务器同步数据给从服务器

    • 主服务器执行bgsave命令来生成RDB文件,将文件发给从服务器

    • 从服务器接收到文件,就会将当前数据清空,执行RDB文件

    • 会在生成RDB文件期间、发送RDB文件期间、从服务器加载RDB文件期间;将新的写操作命令写入到replication buffer缓冲区里

  • 第三阶段:主服务器发送新的写操作命令给从服务器

    • 从服务器加载完RDB文件,会回复一个消息给主服务器

    • 然后主服务器就将replication buffer缓冲区所记录的写操作命令发送给从服务器,从服务器执行这些命令

    • 从而保证主从一致性

1.2后续通过长连接

  • 在第一次同步完成后,主从服务器之间就会维护一个TCP连长连接接,基于这个长连接进行命令传播,从而保证后续的主从一致性

  • 网络不稳定,如果长连接断开后恢复,redis2.8之后就会采用增量复制的方式继续同步数据

    • 从服务器发送psync命令给主服务器,此时的offset(不是-1)

      • 从服务器将自己的slave_repl_offset发送给主服务器
    • 主服务器接收到命令后,然后用continue响应命令告诉从服务器接下来采用增量复制方式

      • 主服务器根据master_repl_offset和slave_repl_offset之间的差距决定同步方式:

        • 如果判断从服务器要读取的数据还在rep_backlog_buffer缓冲区中,则采用增量同步的方式

        • 如果不在了,则采用全量同步的方式

    • 然后主服务器将断线期间,所执行的写命令发送给从服务器

      • 通过rep_backlog_buffer:是一个环形缓冲区,用于找到主从的差异性,在主服务器每次进行命令传播时写入

      • Replication offset:标记缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器采用master_repl_offset来记录自己写的位置,从服务器采用slave_repl_offset来记录自己读的位置

        • 可以尽量调大rep_backlog_buffer缓冲区的大小,防止出现主服务器写入速度大于从服务器读取速度,且缓冲区已满,从服务器想读的数据可能直接被覆盖了

          • 缓冲区大小至少:(主服务器写入的速度*主从重连的时间)
          repl-backlog-size 10mb
          
  • 可以使用从服务器作为副的主服务器,为他自己的从服务器提供RDB文件,从而分担主服务器的压力

2.哨兵模式

实现主从节点故障转移,监控主节点是否挂了,如果挂了就会选举一个从节点切换到主节点,并把新主节点的相关信息通知给从节点和客户端

2.1 哨兵是如何工作的?

1.哨兵会每间隔一秒给所有主从节点发送ping命令,当主从节点收到ping命令后,会发送一个响应命令给哨兵,这样就可以判断他是否正常运行,如果没收到响应,哨兵会认为主观下线

2.客观下线只针对于主节点:

多个哨兵对主节点是否下线进行判断,只要投赞成票的哨兵超过quorum配置([(哨兵数)/2]+1),就会判定主节点客观下线

2.2.选取故障转移的哨兵:

  • 首先对主节点做出主观下线判断的哨兵,会向其他实例发送is-master-down-by-addr命令,这个哨兵就是Leader的候选者

  • 候选者(们)就会接受其他哨兵投票,其他哨兵先接到谁的请求就把票投给谁,只要拿到quorum配置的值的票数,且票数大于其他候选者,则就成为Leader

  • 哨兵为什么至少要三个也是因为成为leader的条件,如果挂掉一个哨兵,哨兵还有机会成为leader

2.3.主从故障转移

主要分为四个步骤

1.选取从节点

先通过down-after-miliseconds*10的配置项,先把断连超过10次的节点排除

  • 第一轮考察:根据服务器性能配置为节点设置优先级

  • 第二轮考察:根据复制的进度,offset最接近旧主节点的靠前

  • 第三轮考察:比较ID号,ID号小的胜出

  • 胜出的节点,哨兵leader会发送slaveof no one命令,让其成为新的主节点(哨兵leader每秒一次发送info命令,知道发现角色信息转变)

2.修改从节点目标

哨兵leader向旧主节点的所有从节点发送slaveof,让他们成为新主节点的从节点

3.新主节点通知客户端

主从切换完成,要将消息通过发布者/订阅者机制通知给客户端

  • 哨兵提供很多消息订阅频道给客户端,客户端可以从提供的频道查看主从切换到哪一步了、新主节点的IP地址和端口
4.处理旧主节点

要继续监视主节点当主节点重新上线后,哨兵集群向它发送slaveof命令,让他成为新主节点的从节点,此时主从故障转移完成!

2.4.哨兵集群如何组成

配置哨兵信息:

sentinel monitor <master-name> <ip> <redis-port> <quorum> 

哨兵节点之间是通过发布者/订阅者机制相互发现的

  • 主节点上有个_sentinel_:hello的频道,哨兵会把ip地址和端口号发布到该频道上,不同的哨兵就是通过它相互发现,实现通信的,从而形成哨兵集群

  • 哨兵通过每10s向主节点发送info命令来获取所有从节点的信息,从而与每个从节点建立联系

3.切片集群

当redis缓存数据量达到一台服务器无法缓存时,就要使用切片集群(redis cluster)解决高并发读写问题

常见的分片算法:哈希分片、范围分片和一致性哈希分片

  • 集群有多个master,每个master保存不同的数据,每个master可以有多个slave节点,master之间可以通过ping检测彼此健康状态(类似哨兵)

  • 客户端可以访问任意的集群节点,最终会被转发到正确的节点上

  • 通过哈希槽(hash slot)将数据分配到不同的集群节点上,一个切片集群有16384个哈希槽(redis内置的),通过键值对的key按照CRC16算法计算一个16bit的值,再用16bit值对16384取模,每个模数代表一个相应编号的哈希槽

  • 平均分配:每个集群平均分配哈希槽

  • 手动分配:通过cluster meet命令手动建立节点间的连接,组成集群再使用cluster addlots命令,指定每个节点上的哈希槽个数

4.集群脑裂问题

4.1.什么是脑裂

由于网络问题导致哨兵认为主节点挂掉了,开始执行主从故障转移,但是在同步的过程中主节点网络恢复了,就出现了两个主节点,

这就是脑裂;

4.2.脑裂导致的数据丢失

因为出现了脑裂,客户端不知道旧的主节点出问题了,还在写数据,但是主从故障转移已经开始了,旧的主节点在此期间写入的数据会在故障转移的最后一步将数据清空,全量复制新主节点的数据成为从节点,所以旧主节点在转移期间写入的数据就会被清空丢失。

解决方案:

既然会写入丢失,那就禁止写入

  • Min-slaves-to-write x 主节点必须有x个从节点连接,小于x个,主节点就会禁止写数据

  • Min-slaves-max-lag x 主从数据复制和同步不能延迟超过x秒,超过主节点就会禁止写数据

六.Redis的缓存设计

缓存设计包括:

1.缓存穿透

  • 查询一个不存在的数据,redis没有缓存,mysql也查询不到数据也就不会写入缓存,所以每次都会请求查询mysql数据库

解决方案:

1.直接缓存空数据
  • Mysql查询返回数据为空,直接把这个空结果缓存到redis

  • 优点:简单

  • 缺点:消耗内存,如果后续插入了这个不存在的数据,但是redis缓存的还是空,可能会发生不一致问题

2.布隆过滤器
  • 通过Redis中bitmap数据类型实现,查询语句直接通过布隆过滤器,如果过滤器不存在则直接返回,存在才去查询redis

  • 缓存预热前要先预热布隆过滤器,将数据通过多个hash函数计算出hash值,然后把hash值对应位置改为1,查询的时候就是通过相同的hash函数判断对应位置是否为1

  • 数组越小误判率就越大,反之越小,但是会带来更多内存消耗(默认设置误判率5%)

  • 优点:占用内存少,没有多余的key

  • 缺点:实现比较复杂,会出现误判

2.缓存击穿

  • 给一个key设置了过期时间,当key过期时,大量的请求可能会瞬间把数据库击穿

解决方案:

1.互斥锁:
  • 一个线程去查询缓存,未命中就会获取互斥锁,获取锁成功后去查询数据库重建缓存数据,写入缓存完毕释放锁

  • 其他的线程需要等待锁释放后才能去获取缓存

  • 具有强一致性,但是性能较差

2.逻辑过期
  • 线程1去查询缓存,发现逻辑时间过期了,去获取互斥锁,成功后重新开启一个线程2去执行查询数据库重建缓存数据,重置过期时间,然后释放锁,而线程1在开启线程2后就直接返回过期数据了。

  • 其他线程获取锁失败就会直接返回过期数据,不需要等待锁释放

  • 具有高可用性,性能优良,不能保证数据的绝对一致性

3.缓存雪崩

两种情况:

1.大量的缓存key同时失效

解决方案:给不同key的TTL添加不同随机值

2.Redis服务宕机

解决方案:

  • 搭建redis集群提高服务高可用性(哨兵模式、集群模式)

  • 给业务添加多级缓存(caffeine或Guava)

  • 给缓存业务添加降级限流策略(ngxin或spring cloud gateway)(保底策略)

    2.1.redis怎么实现限流
    1. 使用分布式锁

    2. 使用滑动窗口算法

      其实限流涉及的最主要的就是滑动窗口,上面也提到1-10怎么变成2-11。其实也就是起始值和末端值都各+1即可。

      而我们如果用Redis的list数据结构可以轻而易举的实现该功能

      我们可以将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成,而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了range方法让我们可以很轻易的获取到2个时间戳内有多少请求

      public Response limitFlow(){
              Long currentTime = new Date().getTime();
              System.out.println(currentTime);
              if(redisTemplate.hasKey("limit")) {
                  Integer count = redisTemplate.opsForZSet().rangeByScore("limit", currentTime -  intervalTime, currentTime).size();        // intervalTime是限流的时间 
                  System.out.println(count);
                  if (count != null && count > 5) {
                      return Response.ok("每分钟最多只能访问5次");
                  }
              }
              redisTemplate.opsForZSet().add("limit",UUID.randomUUID().toString(),currentTime);
              return Response.ok("访问成功");
          }
      

    3.令牌桶算法

    令牌桶算法提及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了。也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。

    依靠上述的思想,我们可以结合Redis的List数据结构很轻易的做到这样的代码,只是简单实现依靠List的leftPop来获取令牌;

    再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成

    // 输出令牌
    public Response limitFlow2(Long id){
            Object result = redisTemplate.opsForList().leftPop("limit_list");
            if(result == null){
                return Response.ok("当前令牌桶中无令牌");
            }
            return Response.ok(articleDescription2);
        }
        
       // 10S的速率往令牌桶中添加UUID,只为保证唯一性
        @Scheduled(fixedDelay = 10_000,initialDelay = 0)
        public void setIntervalTimeTask(){
            redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
        } 
        
    

4.双写一致性

当修改了数据库的数据同时也要更新缓存的数据,缓存和数据库的数据要保持一致

1.延迟双删

因为无论是先删除缓存还是先修改数据库都会出现数据不一致

所以采用了双删,且主从同步需要时间,所以采用延时删除,尽可能的保证数据一致性,但是这个延时的时间不好控制,还是会出现脏数据的可能

  • 数据修改并发问题:如果多个请求同时尝试修改数据库中的数据,而缓存中的数据没有被删除,就有可能导致数据不一致

  • 某个请求可能会在缓存中获取旧数据并写入数据库,然后其他请求又读取了旧数据,从而导致数据不一致

2.分布式锁

因为加入缓存中的数据肯定是读多写少,所以可以采用读写锁来操作,读数据的时候加读锁(共享锁),写数据的时候加写锁(排它锁)

  • 强一致性,但是性能较差

3.异步通知

1.通过阿里的canal中间件,canal把自己伪装成MySQL的一个从节点,监听mysql的binlog文件,然后通知数据的变更情况给cache-service,再执行缓存的更新,可以保证数据一致性

2.通过MQ接收到数据修改的消息,缓存服务层监听MQ,进行缓存的更新,可以保证数据的最终一致性

  • 会有短暂的延时

5.持久化

6.数据过期

数据过期后,要按照不同的规则将数据删除,redis是惰性删除+定期删除配合使用的

1.惰性删除

设置完key的过期时间后,就不管了,当需要该key时,再检查是否过期,如果过期就删除,反之就返回数据

  • 优点:对cpu友好,不会浪费时间去检查过期的key

  • 缺点:对内存不友好,大量过期的key不删除会一致存在内存中

2.定期删除

如果遍历到的库中没有设置过期时间的key,则直接执行下一个库的遍历;如果遍历到的库中有设置过期时间的key,则检查是否过期,如果过期则删除key。 定期删除操作会持续执行,直到达到指定时长或者删除的过期key数量达到指定个数(比例少于25%)时停止

每隔一段时间,就对一些key进行检查,删除其中过期的key(从一定数量的数据库中取出一定数量的随机key检查)

  • slow模式:执行频率默认是10hz(每秒施行10次),每次不超过25ms,可以通过修改配置文件redis.conf的hz选项来调整这个次数

  • fast模式:频率不固定,但是两次间隔不低于2ms,每次耗时不超过1ms

  • 优点:可以通过限制删除的频率和时长来减少对cpu的影响,也能释放过期键占用的内存

  • 缺点:难以确定删除操作执行的时长和频率

3.定时删除

定时删除策略的做法是,在设置key的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行key的删除操作

  • 优点:可以保证过期key可以最快被删除,对内存友好

  • 缺点:过多过期key同时删除,会占用cpu的资源,对cpu不友好

7.淘汰策略

32 位操作系统,默认最大内存值为 3GB

64 位操作系统,不设置限制,直到耗尽机器中所有的内存为止

当Redis中的内存不够用,超过最大内存时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

Redis支持8种不同策略来选择要删除的key:

  • noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。

  • volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰

  • allkeys-random:对全体key ,随机进行淘汰

    • (如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰)
  • volatile-random:对设置了TTL的key ,随机进行淘汰。

  • allkeys-lru: 对全体key,基于LRU算法进行淘汰

    • (把最近最常访问的数据留在缓存中,如果业务有明显的冷热数据区分,建议使用)
  • volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰

    • 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据
  • allkeys-lfu: 对全体key,基于LFU算法进行淘汰

    • 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略
  • volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰

    • LRU:最近最少使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

    • LFU:最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高

    数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?

    使用allkeys-lRU,可以选出最近经常访问的热点数据

    Redis的内存用完了会发生什么?

    如果是noeviction,会直接报错,因为它不淘汰任何key

    如果是 allkeys-lru 策略。把最近最常访问的数据留在缓存中

8.如何设计一个缓存策略,可以动态缓存热点数据呢?

由于数据存储受限,系统并不是将所有数据都需要存放到缓存中的,而只是将其中一部分热点数据缓存起来,所以我们要设计一个热点数据动态缓存的策略。

热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据

缓存更新策略

  • Cache Aside (旁路缓存)

    • 适合读多写少的场景

    • 读策略:先更新数据库中的数据,再删除缓存

    • 写策略:

      • 如果读取数据命中了缓存则直接缓存数据

      • 如果没有命中,则从数据库读取,将数据写入缓存,并返回给用户

  • Read/Write Through(读穿/写穿)

    • ReadThrough先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。

    • Write through当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:

      • 如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成。

      • 如果缓存中数据不存在,直接更新数据库,然后返回;

  • Write Back(写回)

    • Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。

七.分布式锁

分布式锁要实现:

  • 互斥性

    • 锁的目的是获取资源的使用权,只让一个竞争者持有锁
  • 安全性

    • 避免死锁发生
  • 对称性

    • 同一个锁,加锁和释放锁要保证是同一个人
  • 可靠性

    • 要有一定程度的异常处理能力、容灾能力

基于redis实现分布式锁的优点:性能高效、实现方便、避免单点故障

  • 分布式锁:

    • set命令的NX参数可以实现【key值不存在才插入】,用来实现分布式锁

    • 如果key不存在则,显示插入成功(加锁成功),如果key存在则显示插入失败(加锁失败)

    /*
    lock_key 就是键值
    unique_value是客户端生成的唯一标识
    NX表示在lock_key不存在时才对锁操作
    PX 1000表示 lock_key的过期时间为 10s
    */
    Set lock_key unique_value NX PX 1000
    
    • 释放锁就是将lock_key键删除,但是要根据unique_value标识判断是否为加锁的客户端,是才释放锁,通过执行Lua脚本,以原子性的方式释放锁
    //释放锁是,先比较unique_value,避免锁的误释放
    if redis.call("get",KEYS[1])==ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    

如何控制有效时长?

set nx px 不好控制有效时长,所以采用redission框架实现:

1.在redission中可以手动加锁,并且可以控制锁的失效时间和等待时间

2.在redission中引入了一个看门狗机制(watch dog),就是当锁住了一个业务时,每隔一段时间就检查当前业务是否完成,是否还持有锁,如果还持有就续期锁的时长,业务执行完释放锁并通知watch dog不用监听了

3.在高并发业务下,一个业务可能执行的很快,客户1持有锁的时候,客户2来了之后并不是直接拒绝,而是while循环不断尝试获取锁,如果客户1释放,客户2可以马上持有锁,提升了性能

Redisson实现的分布式锁可以重入吗?

redisson实现的分布式锁可以重入,这样避免了死锁的产生,重入其实就是在内部判断是否当前线程持有的锁,如果是当前线程持有的锁就会计数+1,如果释放锁就会-1,在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数

redisson实现的分布式锁能解决主从一致性的问题吗?

这个还不行,比如:当线程1加锁成功后,master节点数据会异步复制给从节点,但是此时master节点宕机了,那么哨兵机制就会选取一个从节点成为新的主节点,新的主节点也会申请一个锁,这个时候就会出现两个节点同时持有一把锁的问题,有可能会出现脏数据的现象

可以使用redisson提供的redlock(红锁)来解决,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。

但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁

可以使用zookeeper实现的分布式锁

八.Redis实战

1.Redis如何实现延迟队列?

延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种:

  • 在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消;

  • 打车的时候,在规定时间没有车主接单,平台会取消你的单并提醒你暂时没有车主接单;

  • 点外卖的时候,如果商家在10分钟还没接单,就会自动取消订单:

在redis中可以使用有序集合(zset)方式来实现延迟消息队列,zset有一个score属性可以用来存储延迟执行的时间

使用zadd score1 value1可以往内存中生产消息,再利用zrangebyscore查询符合条件的所有待处理的任务,通过循环执行队列任务即可

2.Redis大key和热key怎么处理?

大key是指key对应的value值很大

  • String类型的值大于10kB

  • Hash、List、set、zset类型的元素个数超过5000个

热key是指某个key接收到的访问次数和带宽显著高于其他key

  • 某Redis实例的每秒总访问量为10000,而其中一个Key的每秒访问量达到了7000(访问次数显著高于其它Key)

  • 对一个拥有上千个成员且总大小为1MB的HASH Key每秒发送大量的HGETALL(带宽占用显著高于其它Key)

  • 对一个拥有数万个成员的ZSET Key每秒发送大量的ZRANGE(CPU时间占用显著高于其它Key)

2.1.大key和热key的影响

业务规划不足、redis的不正确使用、无效数据的堆积、访问突增都会产生大key和热key

大key

  1. 客户端超时

    1. 由于redis是单线程,操作大key会比较费时,会阻塞redis,客户端视角就会很久没有回应
  2. 引发网络阻塞

    1. 每次获取大key产生的网络流量很大
  3. 阻塞工作线程

    1. 如果del删除大key时,会阻塞工作线程
  4. 内存分布不均

    1. 集群模型在slot分片均匀的情况下,会出现数据和查询倾斜的情况,大key的redis节点占用内存多,QPS也会比较大

热key

  1. 占用大量cpu时间导致性能变差

  2. 节点流量分布不均衡,分布式集群优势弱化

    1. 一个分片负载很高,其他分片十分空闲,会产生读写热点问题
  3. Key的请求量过大超出Redis处理能力造成超卖

  4. 热key压力过大造成缓存击穿

2.2.如何找到大key和热

大key

  1. Redis-cli --bigkeys (查找大key)

    redis-cli -h 127.0.0.1 -p6379 -a "password" --bigkeys
    
    1. 最好在从节点上执行或在实例压力低峰时执行,因为会阻塞主节点和主进程

    2. 不足之处:

      • 只能返回每种类型中最大的那个bigkey,无法得到大小排在前N位的bigkey

      • 对于集合类型来说,这个方案只统计个数,不统计实际占用内存量

  2. 使用scan命令查找大key

    1. 使用scan命令扫描数据库扫描,然后用type命令获取每一个key的类型,对于string类型可以直接使用strlen命令获取字符串的长度

    2. List 类型:LLEN 命令Hash 类型:HLEN 命令;Set 类型:SCARD 命令;Sorted Set 类型:ZCARD 命令;

    3. 不知道业务数据类型的可以使用memory usage命令,查询每一个键值对占用的内存空间

  3. 使用RdbTools工具查找大key

    1. 通过解析RDB文件找到其中大key

热key

  1. Redis-cli --hotkeys

    可以返回所有key被访问的次数,将redis-server的maxmemory-policy参数设置为LFU

  2. 通过业务层定位热key

  3. 使用monitor命令紧急找出热key

2.3.如何解决大key和热key

大key:

  • 对大key进行拆分:变成value1、value2……valueN

  • 压缩value:用压缩算法将key的大小控制在合理范围

热key:

  • 读写分离:

    • 主节点处理写请求,从节点处理读请求
  • 使用本地缓存:

    • 在客户端使用本地缓存,从而降低redis集群对热key的访问量
  • 使用redis cluster:

    • 将热点数据分散存储在多个redis节点上

2.4.如何删除大key

删除操作的本质是要释放键值对占用的内存空间

如果一下释放了大量的内存,空闲内存块链表操作时间就会增加,相应的就会造成redis主线程的阻塞

  • 分批次删除

    • 删除大Hash,用hscan获取,用hdel每次删除一个字段

    • 删除大List,用ltrim每次删除少量元素

    • 删除大set,用sscan扫描元素,再用srem每次删除一个键

    • 删除大zset,用zremrangebyrank命令每次删除top100元素

  • 异步删除

    • redis4.0可以采用异步删除法,用 unlink 命令代替 del 来删除

    • 就是把key放入一个异步线程中进行删除,不会阻塞主进程

3.Redis不支持事务回滚

redis没有提供回滚机制,discard只能主动放弃事务机制,把暂存的命令队列清空,redis不一定保证原子性

  • 且redis在生产环境中很少会出现编译错误,没必要回滚功能

  • 回滚机制的复杂功能和redis追求的简单设计主旨不符合

redis事务:

  • multi:标记事务块的开始

  • exec:执行事务块中的所有命令

  • DISCARD:取消事务,放弃执行事务块中的所有命令;

  • UNWATCH:取消 WATCH 命令对所有 key 的监控;

4.Redis管道(pipeline)有什么用?

管道技术是客户端提供的一种批处理技术,用于一次处理多个Redis命令,从而提高整个交互的性能

  • 将多个命令整合到一起发送给服务端处理后统一返回给客户端,这样就解决了多个命令执行时的网路等待

5.怎么遍历所有key

1.keys

keys * 、keys id:* 分别是查询全部的key以及查询前缀为id:的key。

缺点:

1、没有 offset、limit 参数,一次返回所有满足条件的 key。

2.keys算法是遍历算法,复杂度是O(n),也就是数据越多,时间复杂度越高。

3.数据量达到几百万,keys这个指令就会导致 Redis 服务卡顿,因为 Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续

2.scan

  • 复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程

  • 提供 count 参数,不是结果数量,是redis单次遍历字典槽位数量(约等于)

  • 同 keys 一样,它也提供模式匹配功能;

  • 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;

  • 返回的结果可能会有重复,需要客户端去重复,这点非常重要;

  • 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零

1.redis具有高性能

redis可以缓存MySQL的数据到内存中,供下一次用户获取,在内存中获取相较于在磁盘中快了很多。

mysql数据改变时,同步reids缓存数据即可,但是存在双写一致性问题

2.redis具有高并发

单台设备的Redis的QPS(Query Per Second,每秒钟处理完请求的次数)是MySQL的10倍,Redis单机的QPS能轻松破10w,

MySQL单机的QPS很难破1w。

所以,直接访问Redis能够承受的请求是远远大于直接访问MySQL的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的部分请求会直接到缓存这里而不用经过数据库

2.Redis 和 Memcached 有什么区别?

相同点:

  • 都是基于内存的数据库,一般都用来做缓存

  • 都有过期策略

  • 两者性能都很高

区 别 :

  • redis的数据类型更丰富,memcached只有key-value数据类型

  • redis支持数据持久化,而memcached不支持

  • redis支持集群模式,memcached没有原生的集群模式

  • redis支持发布订阅模式、lua脚本、事务等,而memcached不支持

二.Redis定义

1.什么是Redis?

redis是一种基于内存的非关系型数据库Nosql(Not Only Sql),是一种高级的key-value的存储系统,读写都在内存中完成,读写速度很快,常用于缓存,消息对列,分布式锁等;

redis支持的数据类型较多,且将信息存储在内存上,并且会定期刷新到磁盘或记录文件上,实现主从同步功能;

1.1 Redis的数据类型

键(key):
  • 键值不能重复

  • 用来标识存储的数据

  • String字符串类型

  • 规则

    • 不能太长,影响查询效率

    • 不能太短,容易重复,同时可读性也比较差

    • 规范:Student_Name_List

值(value)

redis常见的五种数据类型:

  1. String(字符串)
    • 存储类型:字符串、整数、浮点数value最长可容纳512M

      • String类型的底层数据结构是int和SDS(简单动态字符串)

      • 不同于C语言字符,SDS不仅可以保存文本数据还可以保存二进制数据(图片、音频、视频、压缩文件)

      • SDS获取字符串长度的时间复杂度为O(1),通过len属性记录字符串的长度

      • Redis的SDS的Api是安全的,拼接字符串不会造成缓存区溢出(拼接字符串时会先检查SDS空间是否够,如果不够会自动扩容)

      • 有三种编码(encoding)格式:int、raw和embstr(只读,要修改先转为raw);(redis2.+是32字节,redis3.0-4.0是39字节,redis5.0是44字节)

        • 如果保存的是整数值,则将整数值保存在字符串对象的ptr属性里,并将编码设置为int

        • 如果保存的是字符串,且长度≤32字节,则使用SDS保存,并将编码格式设置为embstr,embstr通过一次内存分配函数来分配一块连续的内存空间保存redisObject和SDS(分配和释放都只需要调用一次函数,且所有的数据保存在一块连续的内存中,可以更好的利用CPU缓存提升性能)

        • 如果保存的是字符串,且长度>32字节,则使用SDS保存,并将编码格式设置为raw,raw通过2次内存分配函数来分配2块内存空间保存redisObject和SDS

    • 读写能力:对整个字符串或一部分进行操作;对整数或浮点数进行自增或自减

    • 场景:缓存对象、常规计数、分布式锁、共享session 信息等

      • 缓存对象:

        • 直接缓存整个对象的Json

        • 将key进行分离为user:ID:属性,采用MSet存储

      • 常规计数:

        因为Redis处理命令是单线程,所以执行命令的过程是原子的。因此String数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。

      • 分布式锁:

      • 共享Session信息:

        • redis对用户的Session信息进行统一的存储和管理,无论请求发送到哪台服务器下,服务器都会从同一个redis获取相关的Session信息,这就解决了分布式系统下的Session存储问题
    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 功能
    set 键 值 添加或修改一个键和值,键不存在就是添加,存在就是修改
    get 键 获取值,如果存在就返回值,不存在返回nil(就是C语言中NULL)
    del 键 删除指定的键和值,返回删除的个数
    SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒。此处的value是指key对应的value值。等价于:SET key value ex seconds
    EXPIRE key seconds 如果一个key已经存在,要设置一个过期时间
    SETNX key value/set key value nx 保存键值对,如果key存在则不保存,不存在则保存
    MSET key1 value1 key2 value2 批量设置 key-value 类型的值
    MGET key1 key2 批量获取多个 key 对应的 value
    INCR/DECR number 将 key 中储存的数字值增一/减一
    INCRBY/DECRBY
    number 10
    将key中的数字增加10/减少10
  2. List(列表)
    • 存储类型:链表、链表上每个节点都包含一个字符串,列表最大长度为2^32-1,每个列表超过40亿个元素

      • List类型的底层数据结构是由双向链表或压缩列表实现的

        • 如果列表中元素个数<512个(默认值,list-max-ziplist-entries配置),每个元素的值小于64字节(默认值,list-max-ziplist-value配置),此时Redis就会使用压缩列表[缺点:1.查询时需要遍历查询,2.插入或修改元素时,由于内存连续会导致元素后移(类似页分裂)] 作为底层数据结构

        • 反之就会使用`作为底层数据结构(Redis3.2之后,用quicklist取代了双向链表和压缩列表

    • 读写能力:对链表两端进行push和pop操作,读取单个或多个元素,根据值查找或删除元素

    • 实际应用:微信朋友圈点赞,要求按照点赞顺序显示点赞好友信息。如果取消点赞,移除对应好友信息。

    • 场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)

    • 消息对列:需要满足:1.消息保序、2.处理重复消息、3.消息的可靠性

      • 1.List本身就是先进后出的顺序存取,Lpush+Rpop 或Rpush+pop实现消息对列,实现了消息对列保序

        • 缺点:新消息写入不会通知,需要一直执行接收程序,消耗cpu资源,使用BRpop(阻塞式读取)——没有读到新消息自动阻塞,直到有新消息写入对列,再重新读取数据
      • 2.需要自行给一个全局唯一ID,List插入消息的时候,包含上这个唯一ID

      • 3.List消息被读取后就会自动删除,所以使用BRPOPLPUSH保证读取过的消息再插入另一个备份List中留存

    • List的缺陷:不支持多个消费者消费同一条数据,不支持消费组的实现;

    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    lpush 键 元素 元素... left 从左边向指定的键中添加1个或多个元素,返回列表中元素的个数
    rpush 键 元素 元素... right 从右边向指定的键中添加1个或多个元素
    lpop 键 从左边删除一个元素,返回被删除的元素
    rpop 键 从右边删除一个元素,返回被删除的元素
    lrange 键 开始 结束 得到键中指定范围的元素的数据 每个元素都有一个索引号,从左向右0~n 从右向左索引号:-1~-(n+1),每个元素有2个索引号 如果要取出整个列表中所有的元素,索引号应该是:0~-1
    lindex 键 索引值 查询指定索引的元素
    llen 键 获取列表的长度
    BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止,超时时间单位默认是秒
    LREM key 删除元素个数 value值 从表头删除指定个数的元素
  3. Hash(哈希)
    • 存储类型:包含键值对的无序散列表,类似于Java中Map

      • 底层数据结构是压缩列表或哈希表实现的

        • 如果列表中元素个数<512个(默认值,list-max-ziplist-entries配置),每个元素的值小于64字节(默认值,list-max-ziplist-value配置),此时Redis就会使用压缩列表作为底层数据结构(redis7.0之后,由listpack替代压缩链表)

        • 反之就会使用哈希表作为底层数据结构

    • 读写能力:包含方法有添加、获取、删除单个元素

    • 场景:缓存对象、购物车等

      • 场景与String+Json的形式一致,但是如果对象中的属性频繁变化就需要使用Hash来存储例如购物车

      常用命令

      td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
      命令 功能
      hset 键 字段 值 添加键,字段,值
      hget 键 字段 通过键,字段得到值
      hmset 键 字段 值 字段 值 multiply多个,一次添加多个字段和值
      hmget 键 字段 字段 通过键,获取多个字段和值
      hdel 键 字段 字段 删除一个或多个字段的值
      hgetall 键 得到这个键下所有的字段和值
      HKEYS 键 获取哈希表中所有字段
      HVALS 键 获取哈希表中所有值
  4. Set(集合)
    • 存储类型:包含字符串的无序并且唯一的集合,集合最多可存储2^32-1个元素,超过40亿个元素

      • 底层数据结构是由哈希表或整数集合实现的

        • 如果集合中的元素都是整数且元素个数小于512个(默认值,set-maxintset-entries配置),redis使用整数集合作为set的底层结构

        • 不满足上面条件则选择哈希表作为底层数据结构

    • 读写能力:字符串的集合,包含方法有添加、获取、删除单个元素,包含计算交集、并集、差集(数据量较大时,不宜使用计算,会导致redis实例阻塞)

    • 场景:聚合计算(并交差),点赞,共同关注,抽奖

      • 用来缓存点赞的场景,一个用户只能点赞一次,

      • 还有朋友圈的共同好友点赞记录,以及视频号的共同好友推荐视频

      • 抽奖,可以保证一个用户只中一次奖

    常用命令

    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    sadd 键 元素 元素... 向一个键中添加1个或多个元素
    smembers 键 得到这个集合中所有的元素
    sismember 键 元素 判断指定的元素在集合中是否存在,存在返回1,不存在返回0
    srem 键 元素 元素... 通过键删除一个或多个元素
    sinter key1 key2 返回给定所有集合的交集(集合中都共有的部分)
    sdiff key1 [key2] 差集运算
    srandmember key1 count 从key1中随机选出count个元素,元素不从key1中删除
    spop key1 count 从key1中随机选出count个元素,元素从key1中删除
  5. Zset(有序集合)
    • 存储类型:和散列一样,用于存储键值对,相当于set类型多了个排序属性

      • 底层数据结构是由压缩列表或跳表实现的

        • 如果集合元素的个数小于128个,并且每个元素值都小于64字节,则使用压缩列表作为底层数据结构

        • 反之使用跳表作为底层数据结构

          • 基于单链表的高级数据结构(二分查找的思想),最大32级

          • 伪头结点作为所有层级的头结点,他的指针数组长度就是跳表的最大层级数

          • 每个跳表节点都有一个指针数组,指针数组中的第L个指针指向下一个≥他的指针元素

          • 插入和删除根据概率随机到某一层,生成一个以层级为基础的数组,插入进原链表中,删除时只需要找到这个索引就可以直接定位到数据进行删除

          • 查找插入删除的时间复杂度都为O(logn)

          • 空间复杂度为O(n),可以通过调整抽取节点的间隔来平衡时间和空间的复杂度

    • 读写能力:对整个字符串或一部分进行操作;对整数或浮点数进行自增或自减

    • 场景:排序场景;

      • 排行榜、电话、姓名排序
    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    zadd 键 分数 值 分数 值 添加1个或多个元素,每个元素都有一个分数
    zrange 键 开始索引 结束索引 获取指定范围的元素,得到所有的元素,索引是0到-1
    zrange 键 开始索引 结束索引 withscores 查询指定的元素和对应的分数
    zrevrange 键 开始索引 结束索引 withscores 按照分数倒叙获取指定的元素和对应的分数
    zrem 键 值 值 删除一个或多个值
    zcard 键 得到元素个数
    zrank 键 值 得到元素的索引号
    zscore 键 值 得到元素的分数
    zrangebyscore key min max withscore limit offset count 返回指定集合范围的元素,分数有高到低排序
    ZUNIONSTORE destkey numberkeys key1 [key2...] weight number1 number2 将key1和key2的值乘上number1和number2,结果集相加存放在destkey中
  6. Bitmap
    • 是由String类型作为底层数据结构实现的一种统计二值状态的数据类型(bit数组)

    • 场景:签到打卡,判断用户登录状态,布隆过滤器

      • 签到:通过用户唯一ID加上年份月份,统计这个月份的签到情况

        setbit id:sign💯202206 0 1(6月1日签到成功),可以使用

        bitpos id:sign💯202206 1统计6月第一次签到的时间

    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    setbit key offset value 设置值,value只能是0和1
    getbit key offset 得到元素
    bitcount key start end 获取指定范围内值为1的个数
    AND 与运算 &
    OR 或运算 |
    XOR 异或 ^
    NOT 取反 ~
    operations位移操作符
    bitop operations result key 运算,运算的结果result存在key中
    bitpos key value key中第一次出现value的位置
  7. HyperLogLog
    • redis2.8.9新增的数据类型,用于统计基数——统计一个集合中不重复的元素个数(hyperloglog统计规则不是很准确,标准误差为0.81%)

    • 只需要12kb内存就可以计算2^64个不同元素的基数

    • 应用场景:百万级网页UV(unique visitor)计数

      • 百万级网页UV:使用pfadd将访问页面的每个用户都添加到hyperloglog中,再使用pfcount直接获取page1中的UV值
    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    pfadd key element 添加1个或多个元素
    pfcount key 返回给定的hyperloglog的基数估算值
    pemerge destkey sourcekey 将多个hyperloglog合并为一个hyperloglog
  8. GEO
    • 是redis3.2新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作,GeoHash编码将经纬度进行权重分数的转换到Zset元素(1.对二维地图做区间划分;2.对区间进行编码)

    • 底层数据结构为 Zset

    • 场景:滴滴打车,附近的人

      • 先geoadd保存车辆的经纬度集合,然后用户georadius就可以调用周围几公里内的车辆信息,返回数据给LBS应用(Location-Based Service)
    td {white-space:nowrap;border:0.5pt solid #dee0e3;font-size:10pt;font-style:normal;font-weight:normal;vertical-align:middle;word-break:normal;word-wrap:normal;}
    命令 行为
    geoadd longitude latitude member 存储指定的地理空间位置,经度(longitude)纬度(latitude)位置名(member)
    geopos key member 从给定的key中返回指定名称的经纬度信息
    geodist key member1 member2 [m|km|ft|mi] 返回两个给定位置之间的距离
    georadius key longitude radius 根据用户给定的经纬度坐标来获取范围内的地理位置集合
  9. Stream
    • redis5.0新增功能,专为消息队列设计的数据类型使用listpack和redix tree

    • 为了解决消息的可靠性,消息读后删除不可重复读取以及全局唯一ID需要自行实现

    //*表示生成唯一ID
    Xadd mymq * name xiaolin age 12; 
    xread streams mymq (*生成的唯一ID)
    //可以实现消息队列
    
    xread block 10000 streams mymq $
    //可以实现BRpop的阻塞读取操作
    
    • 1.解决了多个消费者不能消费同一个消息的问题

      • Xgroup创建消费组,xreadgroup可以使消费组内的消费者读取信息
      //创建名为group1的消费组,0-0表示从第一条数据读取
      xgroup create mymq group1 0-0
      //创建group2
      xgroup create mymq group2 0-0
      //group1的消费者consumer1从mymq读取所有消息
      xreadgroup group group1 consumer1 streams mymq
      
      • 同一个消费组的成员不能读取同一条消息,但是其他组可以(前提是创建消费组时指定了相同的读取位置)
    • 2.消息的可靠性

      • stream会自动使用内部队列(pending list)留存消费组里每个消费者读取的消息,知道消费者使用XACK命令通知streams消息处理完成,

      • xACK mymq group2 1654256265584-0,通知后这条消息就会被streams删除

      • 消费者可以在重启后,用xpending查询已读取、但尚未确认处理完成的消息

      //查看为处理完成的消息
      XPENDING mymq group2
      //查看 group2 里 consumer2 已从 mymq 消息队列中读取了哪些消息
      XPENDING mymq group2 - + 10 consumer2
      
    • 场景:作为消息队列,专业的消息队列要做到1.消息不丢失,2.消息可堆积

      • redis在AOF持久化配置每秒写盘和主从复制都是异步的,出现意外都会有丢失数据的可能

      • redis本身数据存储在内存中,面对消息挤压内存空间会紧张,且会出现丢数据;

      • redis发布/订阅机制没有基于任何数据类型实现,所以不具备持久化能力,且发后既忘,不能消费历史数据,当消息挤压到32M或在60s内维持在8M以上,消费端就会强行断开

三 Redis的线程模型

为什么说redis是单线程的?

Rdis单线程=>

「接收客户端请求->解析请求->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的

在redis2.6,会启动两个线程,分别用来处理关闭文件和AOF刷盘在redis4.0之后,新增了一个后台线程(lazyfree)用来异步释放redis内存(flushdb async flushall async)

  • 这些任务都是放在各自的任务队列中的,由消费者轮询队列,对任务执行对应的方法

I/O多路复用

  • I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。

有select、poll和epoll三种实现

  • select和poll的通知方式有问题,只会通知用户进程有socket就绪,但是不确定具体哪个socket就绪

  • epoll它会在通知用户进程就绪的同时,将已就绪的socket写入用户空间,不需要遍历socket来判断是否就绪,提升了性能

  • I/O多路复用是指单个线程来同时监听多个socket,并在某个socket可读、可写时得到通知,从而避免无效等待,充分利用cpu资源,目前都是用epoll模式

在redis6.0之前,epoll+事件派发机制

  • 1.调用epoll_create()创建一个epoll对象和调用socket()创建一个服务端socket

  • 2.调用bind()绑定端口和调用listen()监听该socket;

  • 3.将调用epoll__ctl()将listen socket加入到epoll,同时注册「连接事件」处理函数。

  • 上述初始化完成,主程序进入事件循环函数

    • 调用处理发送队列函数,查看发送队列是否有任务,有则通过write函数将客户端缓冲区的数据发送出去,没有没有发送完则会注册epoll_wait发现可写后再处理

      • 调用epoll_wait函数等待事件到来

        • 如果是连接事件,则调用连接事件处理函数:调用accpet获取已连接的socket=》调用epoll_ctl将已连接的socket加入epoll=》注册读事件处理函数

        • 如果是读事件,则调用读事件处理函数:调用read获取客户端发送的数据=》解析命令=》处理命令(6.0之后在此处加入多线程)=》将客户端对象添加到发送队列=》将执行结果写到发送缓冲区等待发送

        • 如果是写事件,则调用写事件处理函数:(此处加入多线程)通过write函数将客户端发送缓存区里的数据发送出去,如果这一轮没有发送完,就会继续注册写事件处理函数,等待epoll_wait发现可写后再处理

为什么单线程的还那么快?

  • redis基本在内存中操作

  • redis的单线程模式可以避免多线程之间的竞争

  • redis采用了I/O多路复用机制处理大量的Socket请求,即一个线程处理多个IO流,允许存在多个监听Socket和已连接socket,内核监听这些socket连接请求和数据请求,一旦请求到达就会交给redis线程处理

为什么要使用单线程?

  • CPU 并不是制约 Redis 性能表现的瓶颈所在,更多的是受到内存大小和网络I/O的限制,所以redis可以使用单线程,想要多核cpu,可以在一台服务器上启动多个节点或分片集群

  • 使用单线程可维护性高,多线程会增加系统复杂度,也会存在线程切换,甚至死锁的可能

为什么又引入了多线程?

在redis6.0之后也采用了多个I/O线程处理网络请求,因为硬件的提示,网络I/O也会出现一些瓶颈问题。

所以为了提高网络I/O的并行度,Rdis6.0对于网络/O采用多线程来处理。但是对于命令的执行,Redis仍然使用单线程来处理,所以大家不要误解Redis有多线程同时执行命令。

Redis官方表示,Redis6.0版本引入的多线程/o特性对性能提升至少是一倍以上(需要io-threads-do-reads 设为yes)

redis6.0后,默认额外创建6个线程

  • redis-server:redis主线程,主要负责处理命令

  • bio_close_file(异步处理关闭文件任务)、bio_aof_fsync(AOF刷盘任务)、bio_lazy_free(释放内存任务)

  • io_thd1、io_thd2、io_thd3:三个l/O线程,io-threads默认是4,所以会启动3(4-1)个/O多线程,用来分担Redis网络/O的压力。

四 Redis的持久化

由于redis的读写都是在内存中的,所以一旦宕机,redis的数据就会消失

所以为了保证数据不丢失,redis会将数据存储到磁盘,保证redis重启后可以恢复数据

三种持久化方式:

1.AOF日志:

  • (增量数据)每执行完一条操作命令,就会把该命令以追加的方式写入一个文件中

    • 好处:

      • 避免额外的检查开销:如果先将操作写入AOF,那么不能确定是否有语法错误,还要进行语法检查

      • 不会阻塞当前写操作命令的执行:因为当写操作命令执行成功才会将命令记录到AOF日志

    • 坏处:

      • 数据可能丢失:在记录到AOF之前宕机了,就会丢失这个数据

      • 因为写入AOF也是在主线程上的,所以会阻塞后续的其他操作

1.1.AOF写回策略:

  • 执行完写操作将命令追加到server.aof_buf缓冲区,然后由系统调用write()将缓冲区数据写入AOF文件,此时数据在page cache中,由内核决定什么时候刷盘到硬盘(有三种策略:always:每次写操作执行完就同步AOF到硬盘;Everysec:等待一秒再将缓存写入硬盘;No:由操作系统自行决定何时写回硬盘

  • AOF日志过大,触发AOF重写机制:

    • 当AOF日志文件大小超过设定的阈值,就会读取当前数据库的所有键值对,每个键值对用一个命令记录到AOF文件中,记录完成后就可以压缩过的新AOF文件替换旧文件了

1.2.AOF的重写过程:

  • 是由父进程fork的子进程(bgrewriteaof)完成的,可以避免阻塞主进程,可以调高效率,不必加锁保证数据安全。

  • 在AOF过大时会触发主进程创建子进程重写AOF,子进程是共享物理内存的,子进程对内存只读,将所有数据转成命令记录到新的AOF;

  • 但是当父进程或子进程对内存发起了修改操作,就会触发写保护中断,就会将修改的物理内存复制,重新设置内存映射关系,再将父子进程设置为可读写,最后才对内存进行写操作————写时复制就是发生写操作时才会复制物理内存)

  • 因为重写过程中主进程仍可以执行命令,如果重写时主进程修改了键值,则redis会将这个写命令写入到AOF缓冲区和AOF重写缓冲区(在fork完子进程之后才开始使用)

  • 最后重写日志完成,子进程异步发送一个信号给主进程,主进程收到信号,就会调用函数将AOF重写缓冲区的内容追加进新的AOF文件中,然后将新的AOF改名,替换旧的AOF文件,完后文件压缩过程(重写AOF)

2.RDB快照(默认的方式):

  • (全量快照)将某一时刻的内存数据,以二进制的方式写入磁盘

  • 使用save和bgsave两个命令,save在主进程操作,会阻塞主进程,bgsave会创建一个子进程生成RDB文件,避免主进程阻塞

  • 获取通过配置文件设置RDB的触发条件save 900 1(900s内执行一次修改)

  • RDB也有写时复制,与AOF相同

3.混合持久化方式:

redis4.0新增的,集成AOF和RDB

  • 因为RDB是全量的,数据写入较慢,容易丢失,但是重启后redis读取很快,而AOF只追加写操作,记录写入较快,数据不容易丢失,但是redis重启读取较慢

  • 所以将AOF文件划分两部分,一部分记录时以RDB二进制形式写入AOF,另一部分重写操作以AOF形式记录,这样即保证了重启数据恢复速度,又尽量保证了数据的完整性

  • 降低了AOF文件的可读性,开启后不能兼容redis4.0之前的版本了(因为AOF文件格式不一致)

五 Redis集群

1.主从复制模式

是redis高可用服务的基础,采用读写分离与mysql相似

1.1第一次主从同步:

  • 要确定谁是主服务器,使用replicaof(redis5.0之后使用slaveof)命令
//服务器B执行这条命令
replicaof <服务器A的IP地址> <服务器A的Redis端口号>
//此时服务器B就会变成A的从服务器
  • 第一阶段:建立链接,协商同步

    • 从服务器发出psync命令,向主服务器申请runID[嗲一次为?](redis服务器唯一标识)和offset[第一次为-1](复制进度)

    • 主服务器接到命令,用fullresync命令响应,将本机runID和offset传递过去

    • 目的为了全量同步复制做准备;

  • 第二阶段:主服务器同步数据给从服务器

    • 主服务器执行bgsave命令来生成RDB文件,将文件发给从服务器

    • 从服务器接收到文件,就会将当前数据清空,执行RDB文件

    • 会在生成RDB文件期间、发送RDB文件期间、从服务器加载RDB文件期间;将新的写操作命令写入到replication buffer缓冲区里

  • 第三阶段:主服务器发送新的写操作命令给从服务器

    • 从服务器加载完RDB文件,会回复一个消息给主服务器

    • 然后主服务器就将replication buffer缓冲区所记录的写操作命令发送给从服务器,从服务器执行这些命令

    • 从而保证主从一致性

1.2后续通过长连接

  • 在第一次同步完成后,主从服务器之间就会维护一个TCP连长连接接,基于这个长连接进行命令传播,从而保证后续的主从一致性

  • 网络不稳定,如果长连接断开后恢复,redis2.8之后就会采用增量复制的方式继续同步数据

    • 从服务器发送psync命令给主服务器,此时的offset(不是-1)

      • 从服务器将自己的slave_repl_offset发送给主服务器
    • 主服务器接收到命令后,然后用continue响应命令告诉从服务器接下来采用增量复制方式

      • 主服务器根据master_repl_offset和slave_repl_offset之间的差距决定同步方式:

        • 如果判断从服务器要读取的数据还在rep_backlog_buffer缓冲区中,则采用增量同步的方式

        • 如果不在了,则采用全量同步的方式

    • 然后主服务器将断线期间,所执行的写命令发送给从服务器

      • 通过rep_backlog_buffer:是一个环形缓冲区,用于找到主从的差异性,在主服务器每次进行命令传播时写入

      • Replication offset:标记缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器采用master_repl_offset来记录自己写的位置,从服务器采用slave_repl_offset来记录自己读的位置

        • 可以尽量调大rep_backlog_buffer缓冲区的大小,防止出现主服务器写入速度大于从服务器读取速度,且缓冲区已满,从服务器想读的数据可能直接被覆盖了

          • 缓冲区大小至少:(主服务器写入的速度*主从重连的时间)
          repl-backlog-size 10mb
          
  • 可以使用从服务器作为副的主服务器,为他自己的从服务器提供RDB文件,从而分担主服务器的压力

2.哨兵模式

实现主从节点故障转移,监控主节点是否挂了,如果挂了就会选举一个从节点切换到主节点,并把新主节点的相关信息通知给从节点和客户端

2.1 哨兵是如何工作的?

1.哨兵会每间隔一秒给所有主从节点发送ping命令,当主从节点收到ping命令后,会发送一个响应命令给哨兵,这样就可以判断他是否正常运行,如果没收到响应,哨兵会认为主观下线

2.客观下线只针对于主节点:

多个哨兵对主节点是否下线进行判断,只要投赞成票的哨兵超过quorum配置([(哨兵数)/2]+1),就会判定主节点客观下线

2.2.选取故障转移的哨兵:

  • 首先对主节点做出主观下线判断的哨兵,会向其他实例发送is-master-down-by-addr命令,这个哨兵就是Leader的候选者

  • 候选者(们)就会接受其他哨兵投票,其他哨兵先接到谁的请求就把票投给谁,只要拿到quorum配置的值的票数,且票数大于其他候选者,则就成为Leader

  • 哨兵为什么至少要三个也是因为成为leader的条件,如果挂掉一个哨兵,哨兵还有机会成为leader

2.3.主从故障转移

主要分为四个步骤

1.选取从节点

先通过down-after-miliseconds*10的配置项,先把断连超过10次的节点排除

  • 第一轮考察:根据服务器性能配置为节点设置优先级

  • 第二轮考察:根据复制的进度,offset最接近旧主节点的靠前

  • 第三轮考察:比较ID号,ID号小的胜出

  • 胜出的节点,哨兵leader会发送slaveof no one命令,让其成为新的主节点(哨兵leader每秒一次发送info命令,知道发现角色信息转变)

2.修改从节点目标

哨兵leader向旧主节点的所有从节点发送slaveof,让他们成为新主节点的从节点

3.新主节点通知客户端

主从切换完成,要将消息通过发布者/订阅者机制通知给客户端

  • 哨兵提供很多消息订阅频道给客户端,客户端可以从提供的频道查看主从切换到哪一步了、新主节点的IP地址和端口
4.处理旧主节点

要继续监视主节点当主节点重新上线后,哨兵集群向它发送slaveof命令,让他成为新主节点的从节点,此时主从故障转移完成!

2.4.哨兵集群如何组成

配置哨兵信息:

sentinel monitor <master-name> <ip> <redis-port> <quorum> 

哨兵节点之间是通过发布者/订阅者机制相互发现的

  • 主节点上有个_sentinel_:hello的频道,哨兵会把ip地址和端口号发布到该频道上,不同的哨兵就是通过它相互发现,实现通信的,从而形成哨兵集群

  • 哨兵通过每10s向主节点发送info命令来获取所有从节点的信息,从而与每个从节点建立联系

3.切片集群

当redis缓存数据量达到一台服务器无法缓存时,就要使用切片集群(redis cluster)解决高并发读写问题

常见的分片算法:哈希分片、范围分片和一致性哈希分片

  • 集群有多个master,每个master保存不同的数据,每个master可以有多个slave节点,master之间可以通过ping检测彼此健康状态(类似哨兵)

  • 客户端可以访问任意的集群节点,最终会被转发到正确的节点上

  • 通过哈希槽(hash slot)将数据分配到不同的集群节点上,一个切片集群有16384个哈希槽(redis内置的),通过键值对的key按照CRC16算法计算一个16bit的值,再用16bit值对16384取模,每个模数代表一个相应编号的哈希槽

  • 平均分配:每个集群平均分配哈希槽

  • 手动分配:通过cluster meet命令手动建立节点间的连接,组成集群再使用cluster addlots命令,指定每个节点上的哈希槽个数

4.集群脑裂问题

4.1.什么是脑裂

由于网络问题导致哨兵认为主节点挂掉了,开始执行主从故障转移,但是在同步的过程中主节点网络恢复了,就出现了两个主节点,

这就是脑裂;

4.2.脑裂导致的数据丢失

因为出现了脑裂,客户端不知道旧的主节点出问题了,还在写数据,但是主从故障转移已经开始了,旧的主节点在此期间写入的数据会在故障转移的最后一步将数据清空,全量复制新主节点的数据成为从节点,所以旧主节点在转移期间写入的数据就会被清空丢失。

解决方案:

既然会写入丢失,那就禁止写入

  • Min-slaves-to-write x 主节点必须有x个从节点连接,小于x个,主节点就会禁止写数据

  • Min-slaves-max-lag x 主从数据复制和同步不能延迟超过x秒,超过主节点就会禁止写数据

六.Redis的缓存设计

缓存设计包括:

1.缓存穿透

  • 查询一个不存在的数据,redis没有缓存,mysql也查询不到数据也就不会写入缓存,所以每次都会请求查询mysql数据库

解决方案:

1.直接缓存空数据
  • Mysql查询返回数据为空,直接把这个空结果缓存到redis

  • 优点:简单

  • 缺点:消耗内存,如果后续插入了这个不存在的数据,但是redis缓存的还是空,可能会发生不一致问题

2.布隆过滤器
  • 通过Redis中bitmap数据类型实现,查询语句直接通过布隆过滤器,如果过滤器不存在则直接返回,存在才去查询redis

  • 缓存预热前要先预热布隆过滤器,将数据通过多个hash函数计算出hash值,然后把hash值对应位置改为1,查询的时候就是通过相同的hash函数判断对应位置是否为1

  • 数组越小误判率就越大,反之越小,但是会带来更多内存消耗(默认设置误判率5%)

  • 优点:占用内存少,没有多余的key

  • 缺点:实现比较复杂,会出现误判

2.缓存击穿

  • 给一个key设置了过期时间,当key过期时,大量的请求可能会瞬间把数据库击穿

解决方案:

1.互斥锁:
  • 一个线程去查询缓存,未命中就会获取互斥锁,获取锁成功后去查询数据库重建缓存数据,写入缓存完毕释放锁

  • 其他的线程需要等待锁释放后才能去获取缓存

  • 具有强一致性,但是性能较差

2.逻辑过期
  • 线程1去查询缓存,发现逻辑时间过期了,去获取互斥锁,成功后重新开启一个线程2去执行查询数据库重建缓存数据,重置过期时间,然后释放锁,而线程1在开启线程2后就直接返回过期数据了。

  • 其他线程获取锁失败就会直接返回过期数据,不需要等待锁释放

  • 具有高可用性,性能优良,不能保证数据的绝对一致性

3.缓存雪崩

两种情况:

1.大量的缓存key同时失效

解决方案:给不同key的TTL添加不同随机值

2.Redis服务宕机

解决方案:

  • 搭建redis集群提高服务高可用性(哨兵模式、集群模式)

  • 给业务添加多级缓存(caffeine或Guava)

  • 给缓存业务添加降级限流策略(ngxin或spring cloud gateway)(保底策略)

    2.1.redis怎么实现限流
    1. 使用分布式锁

    2. 使用滑动窗口算法

      其实限流涉及的最主要的就是滑动窗口,上面也提到1-10怎么变成2-11。其实也就是起始值和末端值都各+1即可。

      而我们如果用Redis的list数据结构可以轻而易举的实现该功能

      我们可以将请求打造成一个zset数组,当每一次请求进来的时候,value保持唯一,可以用UUID生成,而score可以用当前时间戳表示,因为score我们可以用来计算当前时间戳之内有多少的请求数量。而zset数据结构也提供了range方法让我们可以很轻易的获取到2个时间戳内有多少请求

      public Response limitFlow(){
              Long currentTime = new Date().getTime();
              System.out.println(currentTime);
              if(redisTemplate.hasKey("limit")) {
                  Integer count = redisTemplate.opsForZSet().rangeByScore("limit", currentTime -  intervalTime, currentTime).size();        // intervalTime是限流的时间 
                  System.out.println(count);
                  if (count != null && count > 5) {
                      return Response.ok("每分钟最多只能访问5次");
                  }
              }
              redisTemplate.opsForZSet().add("limit",UUID.randomUUID().toString(),currentTime);
              return Response.ok("访问成功");
          }
      

    3.令牌桶算法

    令牌桶算法提及到输入速率和输出速率,当输出速率大于输入速率,那么就是超出流量限制了。也就是说我们每访问一次请求的时候,可以从Redis中获取一个令牌,如果拿到令牌了,那就说明没超出限制,而如果拿不到,则结果相反。

    依靠上述的思想,我们可以结合Redis的List数据结构很轻易的做到这样的代码,只是简单实现依靠List的leftPop来获取令牌;

    再依靠Java的定时任务,定时往List中rightPush令牌,当然令牌也需要唯一性,所以我这里还是用UUID进行了生成

    // 输出令牌
    public Response limitFlow2(Long id){
            Object result = redisTemplate.opsForList().leftPop("limit_list");
            if(result == null){
                return Response.ok("当前令牌桶中无令牌");
            }
            return Response.ok(articleDescription2);
        }
        
       // 10S的速率往令牌桶中添加UUID,只为保证唯一性
        @Scheduled(fixedDelay = 10_000,initialDelay = 0)
        public void setIntervalTimeTask(){
            redisTemplate.opsForList().rightPush("limit_list",UUID.randomUUID().toString());
        } 
        
    

4.双写一致性

当修改了数据库的数据同时也要更新缓存的数据,缓存和数据库的数据要保持一致

1.延迟双删

因为无论是先删除缓存还是先修改数据库都会出现数据不一致

所以采用了双删,且主从同步需要时间,所以采用延时删除,尽可能的保证数据一致性,但是这个延时的时间不好控制,还是会出现脏数据的可能

  • 数据修改并发问题:如果多个请求同时尝试修改数据库中的数据,而缓存中的数据没有被删除,就有可能导致数据不一致

  • 某个请求可能会在缓存中获取旧数据并写入数据库,然后其他请求又读取了旧数据,从而导致数据不一致

2.分布式锁

因为加入缓存中的数据肯定是读多写少,所以可以采用读写锁来操作,读数据的时候加读锁(共享锁),写数据的时候加写锁(排它锁)

  • 强一致性,但是性能较差

3.异步通知

1.通过阿里的canal中间件,canal把自己伪装成MySQL的一个从节点,监听mysql的binlog文件,然后通知数据的变更情况给cache-service,再执行缓存的更新,可以保证数据一致性

2.通过MQ接收到数据修改的消息,缓存服务层监听MQ,进行缓存的更新,可以保证数据的最终一致性

  • 会有短暂的延时

5.持久化

6.数据过期

数据过期后,要按照不同的规则将数据删除,redis是惰性删除+定期删除配合使用的

1.惰性删除

设置完key的过期时间后,就不管了,当需要该key时,再检查是否过期,如果过期就删除,反之就返回数据

  • 优点:对cpu友好,不会浪费时间去检查过期的key

  • 缺点:对内存不友好,大量过期的key不删除会一致存在内存中

2.定期删除

如果遍历到的库中没有设置过期时间的key,则直接执行下一个库的遍历;如果遍历到的库中有设置过期时间的key,则检查是否过期,如果过期则删除key。 定期删除操作会持续执行,直到达到指定时长或者删除的过期key数量达到指定个数(比例少于25%)时停止

每隔一段时间,就对一些key进行检查,删除其中过期的key(从一定数量的数据库中取出一定数量的随机key检查)

  • slow模式:执行频率默认是10hz(每秒施行10次),每次不超过25ms,可以通过修改配置文件redis.conf的hz选项来调整这个次数

  • fast模式:频率不固定,但是两次间隔不低于2ms,每次耗时不超过1ms

  • 优点:可以通过限制删除的频率和时长来减少对cpu的影响,也能释放过期键占用的内存

  • 缺点:难以确定删除操作执行的时长和频率

3.定时删除

定时删除策略的做法是,在设置key的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行key的删除操作

  • 优点:可以保证过期key可以最快被删除,对内存友好

  • 缺点:过多过期key同时删除,会占用cpu的资源,对cpu不友好

7.淘汰策略

32 位操作系统,默认最大内存值为 3GB

64 位操作系统,不设置限制,直到耗尽机器中所有的内存为止

当Redis中的内存不够用,超过最大内存时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

Redis支持8种不同策略来选择要删除的key:

  • noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。

  • volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰

  • allkeys-random:对全体key ,随机进行淘汰

    • (如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰)
  • volatile-random:对设置了TTL的key ,随机进行淘汰。

  • allkeys-lru: 对全体key,基于LRU算法进行淘汰

    • (把最近最常访问的数据留在缓存中,如果业务有明显的冷热数据区分,建议使用)
  • volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰

    • 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据
  • allkeys-lfu: 对全体key,基于LFU算法进行淘汰

    • 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略
  • volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰

    • LRU:最近最少使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

    • LFU:最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高

    数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?

    使用allkeys-lRU,可以选出最近经常访问的热点数据

    Redis的内存用完了会发生什么?

    如果是noeviction,会直接报错,因为它不淘汰任何key

    如果是 allkeys-lru 策略。把最近最常访问的数据留在缓存中

8.如何设计一个缓存策略,可以动态缓存热点数据呢?

由于数据存储受限,系统并不是将所有数据都需要存放到缓存中的,而只是将其中一部分热点数据缓存起来,所以我们要设计一个热点数据动态缓存的策略。

热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据

缓存更新策略

  • Cache Aside (旁路缓存)

    • 适合读多写少的场景

    • 读策略:先更新数据库中的数据,再删除缓存

    • 写策略:

      • 如果读取数据命中了缓存则直接缓存数据

      • 如果没有命中,则从数据库读取,将数据写入缓存,并返回给用户

  • Read/Write Through(读穿/写穿)

    • ReadThrough先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。

    • Write through当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:

      • 如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成。

      • 如果缓存中数据不存在,直接更新数据库,然后返回;

  • Write Back(写回)

    • Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。

七.分布式锁

分布式锁要实现:

  • 互斥性

    • 锁的目的是获取资源的使用权,只让一个竞争者持有锁
  • 安全性

    • 避免死锁发生
  • 对称性

    • 同一个锁,加锁和释放锁要保证是同一个人
  • 可靠性

    • 要有一定程度的异常处理能力、容灾能力

基于redis实现分布式锁的优点:性能高效、实现方便、避免单点故障

  • 分布式锁:

    • set命令的NX参数可以实现【key值不存在才插入】,用来实现分布式锁

    • 如果key不存在则,显示插入成功(加锁成功),如果key存在则显示插入失败(加锁失败)

    /*
    lock_key 就是键值
    unique_value是客户端生成的唯一标识
    NX表示在lock_key不存在时才对锁操作
    PX 1000表示 lock_key的过期时间为 10s
    */
    Set lock_key unique_value NX PX 1000
    
    • 释放锁就是将lock_key键删除,但是要根据unique_value标识判断是否为加锁的客户端,是才释放锁,通过执行Lua脚本,以原子性的方式释放锁
    //释放锁是,先比较unique_value,避免锁的误释放
    if redis.call("get",KEYS[1])==ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    

如何控制有效时长?

set nx px 不好控制有效时长,所以采用redission框架实现:

1.在redission中可以手动加锁,并且可以控制锁的失效时间和等待时间

2.在redission中引入了一个看门狗机制(watch dog),就是当锁住了一个业务时,每隔一段时间就检查当前业务是否完成,是否还持有锁,如果还持有就续期锁的时长,业务执行完释放锁并通知watch dog不用监听了

3.在高并发业务下,一个业务可能执行的很快,客户1持有锁的时候,客户2来了之后并不是直接拒绝,而是while循环不断尝试获取锁,如果客户1释放,客户2可以马上持有锁,提升了性能

Redisson实现的分布式锁可以重入吗?

redisson实现的分布式锁可以重入,这样避免了死锁的产生,重入其实就是在内部判断是否当前线程持有的锁,如果是当前线程持有的锁就会计数+1,如果释放锁就会-1,在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数

redisson实现的分布式锁能解决主从一致性的问题吗?

这个还不行,比如:当线程1加锁成功后,master节点数据会异步复制给从节点,但是此时master节点宕机了,那么哨兵机制就会选取一个从节点成为新的主节点,新的主节点也会申请一个锁,这个时候就会出现两个节点同时持有一把锁的问题,有可能会出现脏数据的现象

可以使用redisson提供的redlock(红锁)来解决,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。

但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁

可以使用zookeeper实现的分布式锁

八.Redis实战

1.Redis如何实现延迟队列?

延迟队列是指把当前要做的事情,往后推迟一段时间再做。延迟队列的常见使用场景有以下几种:

  • 在淘宝、京东等购物平台上下单,超过一定时间未付款,订单会自动取消;

  • 打车的时候,在规定时间没有车主接单,平台会取消你的单并提醒你暂时没有车主接单;

  • 点外卖的时候,如果商家在10分钟还没接单,就会自动取消订单:

在redis中可以使用有序集合(zset)方式来实现延迟消息队列,zset有一个score属性可以用来存储延迟执行的时间

使用zadd score1 value1可以往内存中生产消息,再利用zrangebyscore查询符合条件的所有待处理的任务,通过循环执行队列任务即可

2.Redis大key和热key怎么处理?

大key是指key对应的value值很大

  • String类型的值大于10kB

  • Hash、List、set、zset类型的元素个数超过5000个

热key是指某个key接收到的访问次数和带宽显著高于其他key

  • 某Redis实例的每秒总访问量为10000,而其中一个Key的每秒访问量达到了7000(访问次数显著高于其它Key)

  • 对一个拥有上千个成员且总大小为1MB的HASH Key每秒发送大量的HGETALL(带宽占用显著高于其它Key)

  • 对一个拥有数万个成员的ZSET Key每秒发送大量的ZRANGE(CPU时间占用显著高于其它Key)

2.1.大key和热key的影响

业务规划不足、redis的不正确使用、无效数据的堆积、访问突增都会产生大key和热key

大key

  1. 客户端超时

    1. 由于redis是单线程,操作大key会比较费时,会阻塞redis,客户端视角就会很久没有回应
  2. 引发网络阻塞

    1. 每次获取大key产生的网络流量很大
  3. 阻塞工作线程

    1. 如果del删除大key时,会阻塞工作线程
  4. 内存分布不均

    1. 集群模型在slot分片均匀的情况下,会出现数据和查询倾斜的情况,大key的redis节点占用内存多,QPS也会比较大

热key

  1. 占用大量cpu时间导致性能变差

  2. 节点流量分布不均衡,分布式集群优势弱化

    1. 一个分片负载很高,其他分片十分空闲,会产生读写热点问题
  3. Key的请求量过大超出Redis处理能力造成超卖

  4. 热key压力过大造成缓存击穿

2.2.如何找到大key和热

大key

  1. Redis-cli --bigkeys (查找大key)

    redis-cli -h 127.0.0.1 -p6379 -a "password" --bigkeys
    
    1. 最好在从节点上执行或在实例压力低峰时执行,因为会阻塞主节点和主进程

    2. 不足之处:

      • 只能返回每种类型中最大的那个bigkey,无法得到大小排在前N位的bigkey

      • 对于集合类型来说,这个方案只统计个数,不统计实际占用内存量

  2. 使用scan命令查找大key

    1. 使用scan命令扫描数据库扫描,然后用type命令获取每一个key的类型,对于string类型可以直接使用strlen命令获取字符串的长度

    2. List 类型:LLEN 命令Hash 类型:HLEN 命令;Set 类型:SCARD 命令;Sorted Set 类型:ZCARD 命令;

    3. 不知道业务数据类型的可以使用memory usage命令,查询每一个键值对占用的内存空间

  3. 使用RdbTools工具查找大key

    1. 通过解析RDB文件找到其中大key

热key

  1. Redis-cli --hotkeys

    可以返回所有key被访问的次数,将redis-server的maxmemory-policy参数设置为LFU

  2. 通过业务层定位热key

  3. 使用monitor命令紧急找出热key

2.3.如何解决大key和热key

大key:

  • 对大key进行拆分:变成value1、value2……valueN

  • 压缩value:用压缩算法将key的大小控制在合理范围

热key:

  • 读写分离:

    • 主节点处理写请求,从节点处理读请求
  • 使用本地缓存:

    • 在客户端使用本地缓存,从而降低redis集群对热key的访问量
  • 使用redis cluster:

    • 将热点数据分散存储在多个redis节点上

2.4.如何删除大key

删除操作的本质是要释放键值对占用的内存空间

如果一下释放了大量的内存,空闲内存块链表操作时间就会增加,相应的就会造成redis主线程的阻塞

  • 分批次删除

    • 删除大Hash,用hscan获取,用hdel每次删除一个字段

    • 删除大List,用ltrim每次删除少量元素

    • 删除大set,用sscan扫描元素,再用srem每次删除一个键

    • 删除大zset,用zremrangebyrank命令每次删除top100元素

  • 异步删除

    • redis4.0可以采用异步删除法,用 unlink 命令代替 del 来删除

    • 就是把key放入一个异步线程中进行删除,不会阻塞主进程

3.Redis不支持事务回滚

redis没有提供回滚机制,discard只能主动放弃事务机制,把暂存的命令队列清空,redis不一定保证原子性

  • 且redis在生产环境中很少会出现编译错误,没必要回滚功能

  • 回滚机制的复杂功能和redis追求的简单设计主旨不符合

redis事务:

  • multi:标记事务块的开始

  • exec:执行事务块中的所有命令

  • DISCARD:取消事务,放弃执行事务块中的所有命令;

  • UNWATCH:取消 WATCH 命令对所有 key 的监控;

4.Redis管道(pipeline)有什么用?

管道技术是客户端提供的一种批处理技术,用于一次处理多个Redis命令,从而提高整个交互的性能

  • 将多个命令整合到一起发送给服务端处理后统一返回给客户端,这样就解决了多个命令执行时的网路等待

5.怎么遍历所有key

1.keys

keys * 、keys id:* 分别是查询全部的key以及查询前缀为id:的key。

缺点:

1、没有 offset、limit 参数,一次返回所有满足条件的 key。

2.keys算法是遍历算法,复杂度是O(n),也就是数据越多,时间复杂度越高。

3.数据量达到几百万,keys这个指令就会导致 Redis 服务卡顿,因为 Redis 是单线程程序,顺序执行所有指令,其它指令必须等到当前的 keys 指令执行完了才可以继续

2.scan

  • 复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程

  • 提供 count 参数,不是结果数量,是redis单次遍历字典槽位数量(约等于)

  • 同 keys 一样,它也提供模式匹配功能;

  • 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数;

  • 返回的结果可能会有重复,需要客户端去重复,这点非常重要;

  • 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零

posted on 2026-07-04 17:35  heyke  阅读(5)  评论(0)    收藏  举报