什么是 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: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
- 电话和姓名排序
使用有序集合的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)