什么是 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**
签到
第一步,执行下面的命令,记录该用户 63 号已签到。

SETBIT uid:sign:100:202206 2 1
第二步,检查该用户 63 日是否签到。

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)


原文作者:竖横山

转自链接:https://learnku.com/articles/75382

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请保留以上作者信息和原文链接。