Redis 是一个基于 C 语言开发的开源 NoSQL 数据库,存储的是键值对数据。Redis 的数据是保存在内存中的,因此读写速度非常快,被广泛应用于分布式缓存。
Redis 基于内存的,而内存的访问速度是磁盘的上千倍。
Redis 内置了多种优化过后的数据结构,性能非常高。
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用。
一般用 Memcached 和 Redis 作为分布式缓存。Memcached 是分布式缓存最开始兴起的时候,比较常用。后来,随着 Redis 的发展,大家慢慢都转而使用更加强大的 Redis 了。
高性能:Redis是基于内存的,而内存的访问速度是磁盘的上千倍。
高并发:同时使用了Redis和数据库的系统,比起仅使用数据库的系统,并发量很容易就能提升很多倍。
缓存是Redis最普遍地一个用法,除了缓存,Redis还可以做:
分布式锁:通过 Redis 来做分布式锁是一种比较常见的方式。通常情况下,我们都是基于 Redisson 来实现分布式锁。
消息队列:Redis 自带的 List 数据结构可以作为一个简单的队列使用。Redis 5.0 中增加的 Stream 类型的数据结构更加适合用来做消息队列。它比较类似于 Kafka,有主题和消费组的概念,支持消息持久化以及 ACK 机制。
分布式 Session :利用 String 或者 Hash 数据类型保存 Session 数据,所有的服务器都可以访问。
复杂业务场景:通过 Redis 以及 Redis 的扩展(比如 Redisson)提供的数据结构,我们可以很方便地完成很多复杂的业务场景比如通过 Bitmap 统计活跃用户、通过 Sorted Set 维护排行榜。
限流:一般是通过 Redis + Lua 脚本的方式来实现限流。
延时队列:Redisson 内置了基于 Sorted Set 实现的延时队列。
什么是分布式锁
分布式锁是指满足分布式系统或集群模式下多进程可见并且互斥的锁。
如何通过Redis实现
5 种基本数据类型:String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。
3 种特殊数据类型:HyperLogLog(基数统计)、Bitmap (位图)、Geospatial (地理位置)。
常规数据的缓存,比如序列化后的对象、图片的路径、Token、Session。
最简单的分布式锁。比如利用 SETNX key value 命令实现的分布式锁。
绝大部分情况,建议使用 String 来存储对象数据即可!
String 存储的是序列化后的对象数据,存放的是整个对象。Hash 是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,节省网络流量。如果对象中某些字段需要经常变动或者经常需要查询,Hash 就非常适合。
String 存储相对来说更加节省内存,缓存相同数量的对象数据,String 消耗的内存约是 Hash 的一半。如果系统对性能和资源消耗非常敏感的话,String 就非常适合。
Hash 可以用来存储购物车信息,因为购物车中的商品会频繁修改和变动:
用户 id 为 key
商品 id 为 field,商品数量为 value
用户购物车信息的维护,具体应该怎么操作呢?
Note:这里只是以业务比较简单的购物车场景举例,实际电商场景下,field 只保存一个商品 id 是没办法满足需求的。
List 的特点是存储的元素是有序、可重复,常用来:
存储数据有序的场景,比如实现朋友圈点赞列表、评论列表等。
在异步秒杀业务当中充当消息队列。
Set 的特点是存储的元素无序、不可重复,常用来:
存储数据不能重复的场景。比如网站 UV 统计、文章点赞、动态点赞等。
需要获取多个数据源的交集、并集和差集的场景:共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集)、订阅号推荐(差集+交集) 等等。
需要随机获取数据源中的元素的场景:抽奖系统、随机点名等等。
Sorted Set 的特点是存储的元素可排序、不可重复。因此,Sorted Set 经常被用在各种排行榜中,比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
GEO 存储的是某个位置及其地理坐标。在电商业务中,经常会根据某个位置的地理坐标到 GEO 中查询附近的商家。
Bitmap 存储的是连续的二进制数字,通过 Bitmap,只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身,所以 Bitmap 本身会极大的节省储存空间。可以用 BitMap 统计活跃用户。
可以将 Bitmap 看作是一个存储二进制数字的数组,数组中每个元素的下标叫做偏移量(offset)。
用 BitMap 统计活跃用户的具体步骤如下:
如果想要使用 Bitmap 统计活跃用户的话,可以使用精确到天的日期作为 key,然后用户 ID 为 offset,如果当日活跃过就设置为 1。例如:
初始化数据:
SETBIT 20210308 1 1
(integer) 0
SETBIT 20210308 2 1
(integer) 0
SETBIT 20210309 1 1
(integer) 0
统计 20210308~20210309 总活跃用户数:
BITOP and desk1 20210308 20210309
(integer) 1
BITCOUNT desk1
(integer) 1
统计 20210308~20210309 在线活跃用户数:
BITOP or desk2 20210308 20210309
(integer) 1
BITCOUNT desk2
(integer) 2
可以使用 HyperLogLog 进行 UV 统计。
RDB 持久化是指 Redis 可以通过创建快照,来获得存储在内存里面的数据在 某个时间点 上的副本。Redis 得到副本之后,可以将副本留在当前服务器,在重启服务器的时候使用,也可以将副本复制到其他服务器,从而创建具有相同数据的服务器副本,比如将副本从主库复制到从库一份。
RDB 持久化在四种情况下会执行:
执行 save 命令
执行 bgsave 命令
Redis 停机时,触发 save 命令
触发 RDB 条件时,Redis就会自动触发 bgsave 命令
RDB 是 Redis 默认采用的持久化方式,在 redis.conf 配置文件中默认有此下配置会触发 RDB 条件:
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发bgsave命令创建快照。
save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发bgsave命令创建快照。
save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发bgsave命令创建快照。
如果执行的是 SAVE 命令,则会使主进程执行 RDB 持久化。在持久化过程中,Redis 不能处理任何其他命令,这意味着整个 Redis 服务器会被阻塞,直到 RDB 持久化完成。因此,SAVE 命令通常在 Redis 停机时使用,以确保所有数据都被保存到磁盘。
如果执行的是 bgsave 命令,主进程会 fork 一个子进程,并复制主进程的页表给子进程,子进程可以通过页表来共享主进程的内存数据,从而将共享内存中的数据写到磁盘上的 RDB 文件。fork 采用的是 copy-on-write 技术:
当有读请求访问主进程时,访问共享内存;
当有写请求访问主进程时,则会拷贝一份共享内存中的数据,执行写操作。
redis 提供了两个命令来生成 RDB 快照文件,会根据命令来选择是否阻塞主进程
save:同步保存操作,会阻塞 Redis 主进程;
bgsave:fork 出一个子进程,通过子进程生成副本,不会阻塞 Redis 主线程,默认选项。
AOF 也是用来实现 Redis 中数据的持久化,AOF 默认是关闭的,开启 AOF 持久化后每执行一条更改数据的命令,Redis 就会将该命令写入到 AOF 缓冲区,然后再将 AOF 缓冲区的数据写入到系统内核缓冲区,最后再根据持久化方式的配置来决定何时将系统内核缓存区的数据同步到磁盘中。持久化方式的配置如下:
# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
Note:
Redis 的主进程会在适当的时机,例如处理完一批命令后,将 AOF 缓冲区的数据写入到系统内核缓冲区,这个过程是非阻塞的。也就是说,Redis 在将数据写入系统内核缓冲区的同时,还可以继续处理其他的命令。这是因为系统内核缓冲区通常比硬盘快得多,所以写入操作的延迟非常小。
在关系型数据库当中,如 MySQL 通常都是执行命令之前记录日志,方便故障恢复,而 Redis AOF 持久化机制是在执行完命令之后再记录日志。
因为是记录命令,AOF 文件会比 RDB 文件大的多。而且 AOF 会记录对同一个 key 的多次写操作,但只有最后一次写操作才有意义。通过执行 bgrewriteaof 命令,可以对磁盘上的 AOF 文件执行文件重写,用最少的命令达到相同效果。
Redis 也会在触发阈值时自动去重写 AOF 文件。阈值也可以在 redis.conf 中配置:
# AOF文件比上次文件 增长超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb
因为内存是有限的,如果缓存中的所有数据都是一直保存的话,不给缓存数据设置过期时间的话,内存很快就会溢出。
很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 Token 可能只在 1 天内有效。如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。
Redis 通过做过期字典来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key,过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间。
过期字典是存储在 redisDb 这个结构里的:
typedef struct redisDb {
...
dict *dict; // 数据库键空间,保存着数据库中所有键值对
dict *expires // 过期字典,保存着键的过期时间
...
} redisDb;
常用的过期数据的删除策略有两个
惰性删除:只会在取出 key 的时候才对数据进行过期检查。这样对 CPU 最友好,但是可能会造成太多过期 key 没有被删除。
定期删除:每隔一段时间抽取一批 key 执行删除过期 key 操作。并且,Redis 底层会通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响。
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者都有自己的优点,所以 Redis 采用的是 定期删除+惰性删除 。
仅仅通过给 key 设置过期时间还是有问题的。因为还是可能存在定期删除和惰性删除漏掉了很多过期 key 的情况。这样就导致大量过期 key 堆积在内存里,导致内存溢出。可以通过 Redis 内存淘汰机制解决这个问题,Redis 提供 6 种数据淘汰策略:
volatile-lru(least recently used):从已设置过期时间的数据集中挑选最近最少使用的数据淘汰。
volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰。
volatile-random:从已设置过期时间的数据集中任意选择数据淘汰。
allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
allkeys-random:从数据集中任意选择数据淘汰。
no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
4.0 版本后增加以下两种:
volatile-lfu(least frequently used):从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。
allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。
Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。Redis由于以下的缺点,是不建议在日常开发中使用的。
Redis事务的缺点
不满足原子性和持久性。
事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。
因为在日常开发中基本不使用,所以了解即可,如果面试被问到,直接回答 Redis 的缺点,所以没用过。
Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 事务是不支持回滚操作的。因此,Redis 事务其实是不满足原子性的。
Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。
Redis 支持 3 种持久化方式:
RDB(snapshotting,快照)
AOF(append-only file,只追加文件)
RDB 和 AOF 的混合持久化
与 RDB 持久化相比,AOF 持久化的实时性更好。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度
appendfsync everysec #每秒钟调用fsync函数同步一次AOF文件
appendfsync no #让操作系统决定何时进行同步,一般为30秒一次
AOF 持久化的策略为 no、everysec 时都会存在数据丢失的情况 。always 虽然基本可以满足持久性要求,但性能太差,实际开发过程中不会使用。因此,Redis 事务的持久性是没办法保证的。
Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。
一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。
不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, 严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。
如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。
问题:Redis 采用的是 定期删除+惰性删除 策略。定期删除执行过程中,如果突然遇到大量过期 key 的话,定期删除抽中的那批 key,可能大部分都是过期 key,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。
解决方法:
给 key 设置随机过期时间
开启 lazy-free(惰性删除/延迟释放)。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
什么是 bigkey:简单来说,如果一个 key 对应的 val
redis-cli -a 密码 --bigkeys
ue 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢,有一个不是特别精确的参考标准:
string 类型的 value 超过 10 kb
复合类型的 value 包含的元素超过 5000 个。当然,对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多。
bigkey 有什么危害:bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。因此,我们应该尽量避免 Redis 中存在 bigkey。
如何发现 bigkey:
使用 Redis 自带的 --bigkeys 参数来查找。
命令如下所示,从这个命令的运行结果,我们可以看出:这个命令会扫描 Redis 中的所有 key,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 string 数据类型,包含元素最多的复合数据类型)。然而,一个 key 的元素多并不代表占用内存也多,需要我们根据具体的业务情况来进一步判断。
redis-cli -a 密码 --bigkeys
# redis-cli -p 6379 --bigkeys
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
[00.00%] Biggest string found so far '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' with 4437 bytes
[00.00%] Biggest list found so far '"my-list"' with 17 items
-------- summary -------
Sampled 5 keys in the keyspace!
Total key length in bytes is 264 (avg len 52.80)
Biggest list found '"my-list"' has 17 items
Biggest string found '"ballcat:oauth:refresh_auth:f6cdb384-9a9d-4f2f-af01-dc3f28057c20"' has 4437 bytes
1 lists with 17 items (20.00% of keys, avg size 17.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
4 strings with 4831 bytes (80.00% of keys, avg size 1207.75)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00
借助开源工具分析 RDB 文件。
通过分析 RDB 文件来找出 big key。这种方案的前提是你的 Redis 采用的是 RDB 持久化。网上有现成的工具可以直接拿来使用:
redis-rdb-toolsopen in new window:Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具
rdb_bigkeysopen in new window : Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好。
借助公有云的 Redis 分析服务。
如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能
如何处理 bigkey?
bigkey 的常见处理以及优化办法如下,这些方法可以配合起来使用:
手动清理:Redis 4.0+ 可以使用 UNLINK 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 SCAN 命令结合 DEL 命令来分批次删除。
采用合适的数据结构:比如使用 HyperLogLog 统计页面 UV。
开启 lazy-free(惰性删除/延迟释放):lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
问题:缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库,如果这些请求过多,会给数据库带来巨大的压力。这些请求也可能是别有用心之人故意发到服务端的。
解决办法:
缓存无效 key:如果缓存和数据库都查不到某个 key 的数据,就写一个无效 key 并设置过期时间缓存到 Redis 中去。如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
使用布隆过滤器:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会放行。
但是布隆过滤器会出现误判的情况,这个要从布隆过滤器的原理来说!
我们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操作:
使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
根据得到的哈希值,在位数组中把对应下标的值置为 1。
我们再来看一下,当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:
对给定元素再次进行相同的哈希计算;
得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
由于哈希函数的特性,不同的元素值可能哈希出来的位置相同,这就会导致误判
Note:位数组是一个由 bit 组成的数组,每个 bit 只能存储 0 或 1。在布隆过滤器中,位数组用于表示元素的存在与否。
缓存击穿问题也叫热点 Key 问题,就是一个被高并发访问的 key 突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击
解决办法
设置热点数据永不过期
使用互斥锁:如果从缓存中没有查询到数据,则进行互斥锁的获取,然后判断是否获得了锁,如果获取到了锁,再去数据库查询,查询后将数据写入 redis,再释放锁,返回数据。如果没有获得,则休眠,过一会再尝试查询缓存和获取互斥锁,直到查询到缓存或获取到互斥锁为止。利用互斥锁能保证只有一个线程去操作数据库,防止缓存击穿。
使用逻辑过期方案:当用户查询 redis 时,将 value 取出,判断 value 中的过期时间是否满足,如果没有过期,则直接返回 redis 中的数据,如果过期,则先获取互斥锁并开启独立线程,在开启独立线程后,直接返回之前的数据,独立线程去重构数据,重构完成后释放互斥锁。
缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。
解决方法:给不同的 key 设置不同的过期时间。