Redis[快问快答系列]

什么是 Redis?

Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景
Redis 提供了多种数据类型来支持不同的业务场景,比如 String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流),并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。
除此之外,Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。

Redis 和 Memcached 有什么区别?

  • Memcached 只支持最简单的 key-value 数据类型
  • Redis 支持数据的持久化,Memcached 重启或者挂掉后,数据就没了
  • Redis 原生支持集群模式,Memcached 没有原生的集群模式
  • Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持

为什么用 Redis 作为 MySQL 的缓存?

Redis具备高性能,高并发,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。

Redis 数据类型以及使用场景分别是什么?

String:
  • 缓存对象:
    SET user:1 '{"name":"xiaolin", "age":18}'
  • 计数器:
    INCR count:1001
  • 分布式锁:
    SET lock_key unique_value NX PX 10000
  • 共享session:适用分布式系统
List:
  • 消息队列:
    消息保序:使用 LPUSH + RPOP;
    阻塞读取:使用 BRPOP;
    重复消息处理:生产者自行实现全局唯一 ID;
    消息的可靠性:使用 BRPOPLPUSH
Hash:
  • 缓存对象
    一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。
    # 存储一个哈希表uid:1的键值
    > HMSET uid:1 name Tom age 15
    2
    # 存储一个哈希表uid:2的键值
    > HMSET uid:2 name Jerry age 13
    2
    # 获取哈希表用户id为1中所有的键值
    > HGETALL uid:1
    1) "name"
    2) "Tom"
    3) "age"
    4) "15"
  • 购物车
    添加商品:HSET cart:{用户id} {商品id} 1
    添加数量:HINCRBY cart:{用户id} {商品id} 1
    商品总数:HLEN cart:{用户id}
    删除商品:HDEL cart:{用户id} {商品id}
    获取购物车所有商品:HGETALL cart:{用户id}
Set:
  • 聚合计算场景
    主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,我们可以选择一个从库完成聚合统计

  • 点赞

    #`uid:1` 用户对文章 article:1 点赞
    SADD article:1 uid:1
    #`uid:1` 取消了对 article:1 文章点赞。
    SREM article:1 uid:1
    # 获取 article:1 文章所有点赞用户 :
    SMEMBERS article:1
    1) "uid:3"
    2) "uid:2"
    获取 article:1 文章的点赞用户数量:
    SCARD article:1
    (integer) 2
    #判断用户 uid:1 是否对文章 article:1 点赞了:
    SISMEMBER article:1 uid:1
    (integer) 0  # 返回0说明没点赞,返回1则说明点赞了
  • 共同关注
    Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。

    # uid:1 用户关注公众号 id 为 56789
    > SADD uid:1 5 6 7 8 9
    # uid:2 用户关注公众号 id 为 7891011
    > SADD uid:2 7 8 9 10 11
    # 获取共同关注
    > SINTER uid:1 uid:2
    1) "7"
    2) "8"
    3) "9"
    # 给 `uid:2` 推荐 `uid:1` 关注的公众号:
    > SDIFF uid:1 uid:2
    1) "5"
    2) "6"
    # 验证某个公众号是否同时被 `uid:1``uid:2` 关注:
    > SISMEMBER uid:1 5
    (integer) 1 # 返回0,说明关注了
    > SISMEMBER uid:2 5
    (integer) 0 # 返回0,说明没关注
  • 抽奖活动
    存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。
    key为抽奖活动名,value为员工名称,把所有员工名称放入抽奖箱 :

    >SADD lucky Tom Jerry John Sean Marry Lindy Sary Mark
    (integer) 5

    如果允许重复中奖,可以使用 SRANDMEMBER 命令。

    # 抽取 1 个一等奖:
    > SRANDMEMBER lucky 1
    1) "Tom"
    # 抽取 2 个二等奖:
    > SRANDMEMBER lucky 2
    1) "Mark"
    2) "Jerry"
    # 抽取 3 个三等奖:
    > SRANDMEMBER lucky 3
    1) "Sary"
    2) "Tom"
    3) "Jerry"

    如果不允许重复中奖,可以使用 SPOP 命令。

    # 抽取一等奖1> SPOP lucky 1
    1) "Sary"
    # 抽取二等奖2> SPOP lucky 2
    1) "Jerry"
    2) "Mark"
    # 抽取三等奖3> SPOP lucky 3
    1) "John"
    2) "Sean"
    3) "Lindy"
Zset:
  • 排行榜

    # arcticle:1 文章获得了200个赞
    ZADD user:xiaolin:ranking 200 arcticle:1
    # 文章 arcticle:1 新增一个赞
    ZINCRBY user:xiaolin:ranking 1 arcticle:1
    # 查看某篇文章的赞数
    ZSCORE user:xiaolin:ranking arcticle:4
    # 获取文章赞数最多的 3 篇文章
    ZREVRANGE user:xiaolin:ranking 0 2 WITHSCORES
    # 获取100赞到200 赞的文章
    ZRANGEBYSCORE user:xiaolin:ranking 100 200 WITHSCORES
  • 电话和姓名排序
    使用有序集合的 ZRANGEBYLEXZREVRANGEBYLEX 可以帮助我们实现电话号码或姓名的排序

BitMap:
  • 签到
    第一步,执行下面的命令,记录该用户 6 月 3 号已签到。

    SETBIT uid:sign:100:202206 2 1

    第二步,检查该用户 6 月 3 日是否签到。

    GETBIT uid:sign:100:202206 2 

    第三步,统计该用户在 6 月份的签到次数。

    BITCOUNT uid:sign:100:202206
  • 用户登录状态
    第一步,执行以下指令,表示用户已登录。

    SETBIT login_status 10086 1

    第二步,检查该用户是否登陆,返回值 1 表示已登录。

    GETBIT login_status 10086

    第三步,登出,将 offset 对应的 value 设置成 0。

    SETBIT login_status 10086 0
  • 布隆过滤器

HyperLogLog:

只需要花费 12 KB 内存,就可以计算接近 2^64 个元素的基数,统计结果是有一定误差的,标准误算率是 0.81%。

  • 百万计网页UV计数
    在统计 UV 时,你可以用 PFADD 命令把访问页面的每个用户都添加到 HyperLogLog 中。
    PFADD page1:uv user1 
    PFADD page1:uv user2
    用 PFCOUNT 命令直接获得 page1 的 UV 值了
    PFCOUNT page1:uv
GEO:

GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。

  • 查找用户附近的网约车
    把 ID 号为 33 的车辆的当前经纬度位置存入 GEO 集合中:
    GEOADD cars:locations 116.034579 39.030452 33
    当用户想要寻找自己经纬度(116.054579,39.030452 )为中心的5公里内的车辆信息
    GEORADIUS cars:locations 116.054579 39.030452 5 km ASC COUNT 10
Stream:
  • 消息队列 比list高级

Redis 线程模型

Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。
Redis程序不是单线程的,后台还会有三个线程处理关闭文件,AOF刷盘,释放内存

Redis 采用单线程为什么还这么快?

  • Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。
  • Redis 采用了 I/O Epoll多路复用机制处理大量的客户端 Socket 请求

Redis 6.0 之后为什么引入了多线程?

在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,**但是对于命令的执行,Redis 仍然使用单线程来处理,Redis 6.0 版本引入的多线程 I/O 特性对性能提升至少是一倍以上。

Redis 如何实现数据不丢失?

  • AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
  • RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
  • 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点;

AOF 日志是如何实现的?

Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。
Redis 提供了 3 种AOF写回硬盘的策略,在 Redis.conf 配置文件中的 appendfsync 配置项

  • Always,每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
  • Everysec,每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
  • No,意味着不由 Redis 控制写回硬盘的时机,由操作系统决定何时将缓冲区内容写回硬盘。

AOF 日志过大,会触发压缩机制 bgrewriteaof

RDB 做快照时会阻塞线程吗?

  • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,会阻塞主线程
  • 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞

Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令

save 900 1 //900 秒之内,对数据库进行了至少 1 次修改;
save 300 10    //300 秒之内,对数据库进行了至少 10 次修改;
save 60 10000 // 60 秒之内,对数据库进行了至少 10000 次修改。

RDB 在执行快照的时候,数据能修改吗?

可以的,执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于多进程的写时复制技术(Copy-On-Write, COW)。

为什么会有混合持久化?

Redis 4.0 提出了混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。

Redis 如何实现服务高可用?

  • 主从复制:一主多从的模式,且主从服务器之间采用的是「读写分离」的方式。
  • 哨兵模式:主从服务器出现故障宕机时,需要手动进行恢复。所以Redis 增加了哨兵模式,因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。
  • Redis Cluster:分布式集群,采用哈希槽,来处理数据和节点之间的映射关系

Redis 使用的过期删除策略是什么?

Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。

  • 惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
  • 定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。

Redis 主从模式中,对过期键会如何处理?

主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库

Redis 内存满了,会发生什么?

在 Redis 的运行内存达到了配置项设置的 maxmemory,就会触发内存淘汰机制

Redis 内存淘汰策略有哪些?

  • noeviction:默认的内存淘汰策略,不淘汰任何数据,而是不再提供服务,直接返回错误。

在设置了过期时间的数据中进行淘汰

  • volatile-random:随机淘汰设置了过期时间的任意键值;
  • volatile-ttl:优先淘汰更早过期的键值。
  • volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值;
  • volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;

在所有数据范围内进行淘汰:

  • allkeys-random:随机淘汰任意键值;
  • allkeys-lru:淘汰整个键值中最久未使用的键值;
  • allkeys-lfu:淘汰整个键值中最少使用的键值。

如何避免缓存雪崩?

  • 将缓存失效时间随机打散,在原有的失效时间基础上增加一个随机值
  • 设置缓存不过期,通过业务逻辑来更新缓存数据

如何避免缓存击穿

  • 互斥锁方案(Redis 中使用 SET EX NX)
  • 不给热点数据设置过期时间,由后台异步更新缓存

如何避免缓存穿透

  • 判断求请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果判断出是恶意请求就直接返回错误
  • 可以针对查询的数据,在缓存中设置一个空值或者默认值返回给应用,而不会继续查询数据库。
  • 使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在

Redis 如何实现延迟队列?

在 Redis 可以使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。
zadd score1 value1 命令就可以一直往内存中生产消息。 zrangebyscore 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。

Redis 的大 key 如何处理?

一般而言,下面这两种情况被称为大 key:

  • String 类型的值大于 10 KB;
  • Hash、List、Set、ZSet 类型的元素的个数超过 5000个;
//最好选择在从节点上执行该命令。因为主节点上执行时,会阻塞主节点,只能返回每种类型中最大的那个 bigkey,无法得到大小排在前 N 位的 bigkey,对于集合类型来说,只统计集合元素个数的多少,而不是实际占用的内存量。
redis-cli -h 127.0.0.1 -p6379 -a "password" -- bigkeys
scan命令,配合key类型再用对应的命令计算内存
//使用 RdbTools 第三方开源工具,可以用来解析 Redis 快照(RDB)文件,找到其中的大 key。
rdb dump.rdb -c memory --bytes 10240 -f redis.csv

如何删除大 key?

  • 分批次删除
  • 异步删除(Redis 4.0版本以上)推荐使用
    从 Redis 4.0 版本开始,可以采用异步删除法,用 unlink 命令代替 del 来删除。这样 Redis 会将这个 key 放入到一个异步线程中进行删除,这样不会阻塞主线程。我们还可以通过配置参数,达到某些条件的时候自动进行异步删除。

Redis 管道有什么用?

把多条命令拼接到一起,当成一次请求发出去,结果也是拼接到一起发回来,免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。

Redis 事务支持回滚吗?

Redis 中并没有提供回滚机制

如何用 Redis 实现分布式锁的?

SET lock_key unique_value NX PX 10000 
  • lock_key 就是 key 键;
  • unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
  • NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
  • PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
    解锁需要Lua脚本保证原子性
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

基于 Redis 实现分布式锁有什么缺点?

  • 超时时间不好设置。
  • 集群情况下的不可靠性。

Redis 如何解决集群情况下分布式锁的可靠性?

为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)。它是基于多个 Redis 节点的分布式锁,官方推荐是至少部署 5 个 Redis 节点,而且都是主节点

为什么用跳表而不用平衡树?

  • 从内存占用上来比较,跳表比平衡树更灵活一些。
  • 在做范围查找的时候,跳表比平衡树操作要简单
  • 从算法实现难度上来比较,跳表比平衡树要简单得多

如何保证缓存和数据库数据的一致性?

  • 更新数据库 + 更新缓存
    如果我们的业务对缓存命中率有很高的要求,我们可以采用「更新数据库 + 更新缓存」的方案,但是在两个更新请求并发执行的时候,会出现数据不一致的问题
    所以我们得增加一些手段来解决这个问题,这里提供两种做法:

    • 在更新缓存前先加个分布式锁,保证同一时间只运行一个请求更新缓存,当然对于写入的性能就会带来影响。
    • 在更新完缓存时,给缓存加上较短的过期时间,缓存的数据也会很快过期,
  • 先删除缓存 + 更新数据库
    延迟双删

    #删除缓存
    redis.delKey(X)
    #更新数据库
    db.update(X)
    #睡眠
    Thread.sleep(N)
    #再删除缓存
    redis.delKey(X)
本作品采用《CC 协议》,转载必须注明作者和本文链接
遇强则强,太强另说
本帖由系统于 10个月前 自动加精
讨论数量: 3
Jyunwaa

应该是Redis单机100W,MySQL单机10W。

10个月前 评论
竖横山 (楼主) 10个月前

Redis 做排行榜时,大 Key 怎么处理? 元素超过 5000 个,那 5000 后的名次怎么获取?

8个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!