redis专题

  1. Redis数据结构

    • 字符串(String):底层是redis自定义的一种简单动态字符串SDS;在Redis中,包含字符串值的键值对底层都是用SDS实现的

      • SDS是二进制安全的数据结构:SDS API是以处理二进制的方式来处理SDS存放在buf数组里的数据,可以存储任意二进制数据;不像C语言字符串那样以‘\0’来标识字符串结束,因为传统C字符串符合ASCII编码,这种编码的操作的特点就是:遇零则止

        因此,如果传统字符串保存图片,视频等二进制文件,操作文件时就被截断了;

      • SDS提供了内存预分配机制,避免了频繁的内存分配

    • 列表(List):底层实现就是压缩列表、双向链表,压缩列表比双向链表更节省空间

    • 散列(Hash):底层存储有两种数据结构,ziplist + hashtable

    • 集合(Set):整数集合、哈希表

    • 有序集合(Sorted Set):每个元素有权重,可以用作排行榜,zset底层在数据量少的时候使用压缩表,数据量大的时候转成跳表

    • image

    1. String操作和适用场景:key和value大小最大512M

      • 常用操作

        • set [key] [value] 给指定key设置值(set 可覆盖老的值)
        • get [key] 获取指定key 的值
        • del [key] 删除指定key
        • exists [key] 判断是否存在指定key
        • expire [key] [time] 给指定key 设置过期时间 单位秒
        • setex [key] [time] [value] 等价于 set + expire 命令组合
        • setnx [key] [value] 如果key不存在则set 创建,否则返回0
        • incr [key] 如果value为整数 可用 incr命令每次自增1
        • incrby [key] [number] 使用incrby命令对整数值 进行增加 number
        • mset [key1] [value1] [key2] [value2] … 批量存键值对
        • mget [key1] [key2] … 批量取key
      • 适用场景

        • 计数器:incr和decr命令的作用是将key中储存的数字值加一/减一,这两个操作具有原子性;如微博的评论数、点赞数、分享数,抖音作品的收藏数,京东商品的销售量、评价数;
        • 存储对象:将对象转换为JSON字符串,再存储在string类型中;如用户信息、商品信息等。
  2. list操作和适用场景

    • 常用命令

      • rpush [key] [value1] [value2] … 链表右侧插入
      • rpop [key] 移除右侧列表头元素,并返回该元素
      • lpop [key] 移除左侧列表头元素,并返回该元素
      • llen [key] 返回该列表的元素个数
      • lrem [key] [count] [value] 删除列表中与value相等的元素,count是删除的个数。 count>0 表示从左侧开始查找,删除count个元素,count<0 表示从右侧开始查找,删除count个相同元素,count=0 表示删除全部相同的元素
      • lindex [key] [index] 获取list指定下标的元素 (需要遍历,时间复杂度为O(n))
      • lrange [key] [start_index] [end_index] 获取list 区间内的所有元素 (时间复杂度为 O(n))
      • ltrim [key] [start_index] [end_index] 保留区间内的元素,其他元素删除(时间复杂度为 O(n))
    • 应用场景

      • 消息队列:Redis的lpush + brpop命令组合即可实现阻塞队列,生产者客户端使用lpush从列表左侧插入元素,多个消费者客户端使用brpop命令阻塞式的争抢列表尾部的元素,多个客户端保证了消费的负载均衡和高可用
      • 最新列表:list类型的lpush命令和lrange命令能实现最新列表的功能,每次通过lpush命令往列表里插入新的元素,然后通过lrange命令读取最新的元素列表,如朋友圈的点赞列表、评论列表
  3. set操作和适用场景

    • 常用操作

      • sadd [key] [value] 向指定key的set中添加元素
      • smembers [key] 获取指定key 集合中的所有元素
      • sismember [key] [value] 判断集合中是否存在某个value
      • scard [key] 获取集合的长度
      • spop [key] 弹出一个元素
      • srem [key] [value] 删除指定元素
    • 适用场景

      • 去重场景:好友、关注、粉丝、感兴趣的人集合;每日签到用户ID池
  4. hash操作和适用场景

    • 常用操作

      • hset [key] [field] [value] 新建字段信息
      • hget [key] [field] 获取字段信息
      • hdel [key] [field] 删除字段
      • hlen [key] 保存的字段个数
      • hgetall [key] 获取指定key 字典里的所有字段和值 (字段信息过多,会导致慢查询 慎用:亲身经历 曾经用过这个这个指令导致线上服务故障)
      • hmset [key] [field1] [value1] [field2] [value2] … 批量创建
      • hincr [key] [field] 对字段值自增
      • hincrby [key] [field] [number] 对字段值增加number
    • 使用场景

      • 结构型数据存储:用户购物车、存储对象
  5. zset操作和适用场景

    • 常用操作

      • zadd [key] [score] [value] 向指定key的集合中增加元素
      • zrange [key] [start_index] [end_index] 获取下标范围内的元素列表,按score 排序输出
      • zrevrange [key] [start_index] [end_index] 获取范围内的元素列表 ,按score排序 逆序输出
      • zcard [key] 获取集合列表的元素个数
      • zrank [key] [value] 获取元素在集合中的排名
      • zrangebyscore [key] [score1] [score2] 输出score范围内的元素列表
      • zrem [key] [value] 删除元素
      • zscore [key] [value] 获取元素的score
    • 适用场景

      • 动态排行榜:比如说粉丝关注列表,score为关注时间;用户成绩,score为成绩;小说热度排行表,score为热度值
  6. Redis是单线程到多线程的演化

    • Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程;其中执行命令阶段,由于Redis是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,会进入一个队列中,然后逐个被执行,所以多个客户端发送的命令的执行顺序是不确定的
    • 演化主要经历两个阶段,redis4.0之前、redis4.0之后、redis6.0之后
    • redis4.0之前执行原理:通过IO多路复用器监听来自客户端的socket网络连接,然后由主线程进行IO请求的处理以及命令的处理,所有操作都是线性的
    • redis4.0之后引入Lazy Free机制:由于Redis单线程执行命令,若客户端向Redis发送一条耗时较长的命令,比如删除一个含有上百万对象的Set键,或者执行flushdb,flushall操作,Redis服务器需要回收大量的内存空间,这事就会导致Redis服务阻塞;所以Redis4.0引入Lazy Free,目的是将慢操作异步化,将慢操作放入异步任务队列中,有单独的线程处理
    • Redis6.0引入多线程,将网络I/O异步化单线程的性能瓶颈主要在网络IO操作上,即在读写网络 read/write 命令期间会占用大部分 CPU 时间,这对于单线程来说,会阻塞后边操作;Redis6使用多线程,将网络数据读写和协议解析通过多线程的方式来处理,对于命令执行来说,仍然使用单线程操作
  7. redis为什么会这么快?

    • 完全基于内存
    • 数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的,其中哈希表和压缩列表是复用比较多的数据结构
    • 单线程:利用队列技术并发访问变为串行访问,避免了上下文切换竞争锁等而消耗CPU
    • 使用多路I/O复用模型,非阻塞IO
  8. redis内存回收机制:过期策略(删除过期时间的key)+淘汰策略(内存使用达到maxmemony上限时触发内存淘汰机制)

    • 定时过期:为每个设置过期时间的key设置一个定时器,到期立即清除堆内存友好,但会占用大量的CPU资源
    • 惰性过期:只有当访问key时,去判断key是否过期,过期则删除;对内存不友好,但可以最大化节省CPU资源
    • 定期过期:每个一段时间(默认每秒运行10次)扫描一定数量的key,并清除已过期key
    • 折中方案:Redis中同时使用了惰性过期和定期过期两种过期策略;
    • volatile-lru:从已设置过期的数据集中挑选最近最少使用的淘汰
    • volatile-ttl:从已设置过期的数据集中挑选将要过期的数据淘汰
    • volatile-random:从已设置过期的数据集中任意挑选数据淘汰
    • allkeys-lru:从数据集中挑选最近最少使用的数据淘汰(最常用的)
    • allkeys-random:从数据集中任意挑选数据淘汰
    • noenviction:禁止驱逐数据;可以保证数据不被丢失,这也是系统默认的一种淘汰策略
  9. Redis持久化方式

    • RDB持久化(默认):定时保存到磁盘上的RDB文件;RDB文件紧凑、体积小,恢复速度快,对性能影响小;但无法做到实时持久化
    • AOF持久化:将Redis的操作日志以追加的方式写入文件;以日志的形式记录服务器处理的每一个写、删除操作,当服务器重启时会重新执行这些命令恢复原始数据;支持秒级持久化、兼容性好;文件大,恢复速度慢,对性能影响大
  10. 数据库和redis缓存一致性解决方案

    • 一般情况下,先删缓存,再更新数据库;如果缓存删除失败,则直接返回;如果缓存删除成功,数据库更新失败,仍能保持数据一致
    • 高并发情况下,删除缓存成功,这时去更新数据库,但数据库还没更新完,有一个查询请求过来,发现缓存里没有,就去数据库里查,此时查到的是旧数据,而且又被写入缓存,这样等数据库更新操作完成后,缓存和数据库数据不一致;此时可以用队列的去解决这个问题,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同数据在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成
  11. 缓存穿透、缓存血崩、缓存击穿

    • 缓存穿透

      • 定义:查询不存在数据 ——不断请求缓存和数据库中都没有的数据,导致数据库压力过大,严重会击垮数据库

      • 避免:

        • 对查询结果为空的情况也进行缓存
        • 接口层增加key的简单校验
        • BloomFilter:利用高效的数据结构位数组和算法(K的hash散列函数)快速判断出你这个Key是否在数据库中存在,不存在你return就好了,存在你就去查了DB刷新KV再return
    • 缓存血崩

      • 定义:某一时刻发生大规模的缓存失效、导致系统崩溃的情况

      • 避免:

        • 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
        • 使用 Hystrix进行限流 & 降级(二级缓存)
        • 采用集群,降低服务宕机的概率
    • 缓存击穿(缓存并发)

      • 定义:在高并发的情况下,大量的请求同时查询同一个key时,此时这个key正好失效了,就会导致同一时间,这些请求都会去查询数据库

      • 避免:

        • 设置热点数据永远不过期(比如首页数据)
        • 分布式互斥锁:只允许一个线程查询DB,然后回写到缓存
  12. redis热点问题

    • 问题:Redis中某个Key的访问频次远大于其他剩余的Key,导致该key的请求打到同一个节点上,压垮缓存服务,热点问题其实是局部性问题

    • 一般Redis单节点的查询性能在2W的QPS,MySQL单机的查询性能一般在4K的QPS

    • 排查:依赖监控数据,监控Redis各个实例的CPU使用率、QPS 数据,如果你看到Redis集群中某些实例负载和QPS特别高,但其他实例负载很低,不用问肯定是出现热点问题了,接下来你需要做的就是找出具体的热点key,并且找出数据访问的来源

    • 解决:

      • 增加应用层Cache:比如GuavaCache,对于热点数据,采用本地缓存,这样热点数据请求在应用层内部就能消化,从而减少对redis的压力
      • 增加数据副本:将热点key的请求做下拆分,在数据写入的时候,可以用不同的Key重复写10份,比如 XXX_KEY_01, XXX_KEY_02……XXX_KEY_10, 访问的时候在原始Key上随机拼接一个1-10之间的后缀即可,这样就能实现数据请求的分散,如果想让请求更分散,可以存储更多的副本
  13. 基于缓存(redis、memcached)的分布式锁

    • 利用Redis的SETNX key value这个命令实现分布式锁
    • 加锁:stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
    • 解锁:判断当前解锁的竞争者id是否为锁的持有者,如果不是直接返回失败,如果是则删除key,如果删除成功,返回解锁成功,否则解锁失败;这里使用Lua脚本实现原子性,防止解锁不属于自己线程的锁:if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
    • 是否可重入:以上实现的锁是不可重入的,如果需要实现可重入,在SET_IF_NOT_EXIST之后,再判断key对应的value是否为当前竞争者id,如果是返回加锁成功,否则失败
    • 死锁问题:加锁时我们设置了key的超时,当超时后,如果还未解锁,则自动删除key达到解锁的目的。如果一个竞争者获取锁之后挂了,我们的锁服务最多也就在超时时间的这段时间之内不可用
    • 自动续期:锁超时时间的设置,设大设小都不合适,redisson的看门狗 Watch Dog机制,其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到一个 RedissonLock.EXPIRATION_RENEWAL_MAP里面,然后每隔 10 秒 (internalLockLeaseTime / 3) 检查一下,如果客户端 还持有锁 key(判断客户端是否还持有 key,其实就是遍历 EXPIRATION_RENEWAL_MAP 里面线程 id 然后根据线程 id 去 Redis 中查,如果存在就会延长 key 的时间),那么就会不断的延长锁 key 的生存时间
  14. Redis扩容方案

    • 主从复制: 通过在集群中添加新的从服务器来扩容集群
    • 哨兵机制: 通过在集群中添加新的哨兵服务器来扩容集群
    • 集群模式: 通过在集群中添加新的节点来扩容集群
    • 分片: 通过将数据分散到多个节点上来扩容集群
    • 哈希槽: 通过将数据分布到多个节点上来扩容集群
  15. lua脚本如何实现原子性的?

    • Lua脚本优势:

      • 支持原子性操作:整体执行,不会被其他他请求插入
      • 降低网络开销:将多个请求通过脚本形式一次发送到服务端,减少网络延时
      • 脚本复用:脚本可以保存在服务端,被复用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值