Redis[快问快答系列]

dafenqi
2023-08-12 / 0 评论 / 13 阅读 / 正在检测是否收录...

[Redis[快问快答系列]](https://learnku.com/articles/75382)

什么是 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 为 5、6、7、8、9

SADD uid:1 5 6 7 8 9

uid:2 用户关注公众号 id 为 7、8、9、10、11

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:1uid: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
电话和姓名排序
使用有序集合的 ZRANGEBYLEX 或 ZREVRANGEBYLEX 可以帮助我们实现电话号码或姓名的排序

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)

0

Deprecated: strtolower(): Passing null to parameter #1 ($string) of type string is deprecated in /www/wwwroot/testblog.58heshihu.com/var/Widget/Archive.php on line 1032

评论 (0)

取消