单机下的redis
# 1. redis为什么速度快
Redis 基于内存,内存的访问速度是磁盘的上千倍;
Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用(Redis 线程模式后面会详细介绍到);
Redis 内置了多种优化过后的数据结构实现,性能非常高。
# 2. redis作为缓存的高性能与高并发
高性能
假如用户第一次访问数据库中的某些数据的话,这个过程是比较慢,毕竟是从硬盘中读取的。但是,如果说,用户访问的数据属于高频数据并且不会经常改变的话,那么我们就可以很放心地将该用户访问的数据存在缓存中。
这样有什么好处呢? 那就是保证用户下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
高并发
一般像 MySQL 这类的数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(就单机 Redis 的情况,Redis 集群的话会更高)。
QPS(Query Per Second):服务器每秒可以执行的查询次数;
由此可见,直接操作缓存能够承受的数据库请求数量是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。进而,我们也就提高了系统整体的并发。
# 3.redis能做什么事情
缓存、分布式锁、复杂业务逻辑、消息队列、限流
# 4.redis中的数据结构
# 字符串
- 常用命令:
设置指定的key值
设只有在key不存在时设置的key的值
获取指定key的值
设置一个或多个执行key的值
获取一个或多个指定 key 的值
返回 key 所储存的字符串值的长度
将 key 中储存的数字值增一
将 key 中储存的数字值减一
判断指定 key 是否存在
删除指定的 key
给指定 key 设置过期时间
- 应用场景
需要存储常规数据的场景
需要计数的场景
分布式锁
# 列表
- 常用命令
在指定列表的尾部(右边)添加一个或多个元素
在指定列表的头部(左边)添加一个或多个元素
将指定列表索引 index 位置的值设置为 value
移除并获取指定列表的第一个元素(最左边)
移除并获取指定列表的最后一个元素(最右边)
获取列表元素数量
获取列表 start 和 end 之间 的元素
- 应用场景
消息流展示:最新文章、最新推文
消息队列:Redis List 数据结构可以用来做消息队列,只是功能过于简单且存在很多缺陷,不建议这样做。
相对来说,Redis 5.0 新增加的一个数据结构 Stream
更适合做消息队列一些,只是功能依然非常简陋。和专业的消息队列相比,还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。
# 哈希
- 常用命令
设置指定哈希表中指定字段的值
只有指定字段不存在时设置指定字段的值
同时将一个或多个 field-value (域-值)对设置到指定哈希表中
获取指定哈希表中指定字段的值
获取指定哈希表中一个或者多个指定字段的值
获取指定哈希表中所有的键值对
查看指定哈希表中指定的字段是否存在
删除一个或多个哈希表字段
获取指定哈希表中字段的数量
对指定哈希中的指定字段做运算操作(正数为加,负数为减)
适用场景
对象数据存储场景
- 举例 :用户信息、商品信息、文章信息、购物车信息。
- 相关命令 :
HSET
(设置单个字段的值)、HMSET
(设置多个字段的值)、HGET
(获取单个字段的值)、HMGET
(获取多个字段的值)
# 集合
- 常用命令
向指定集合添加一个或多个元素
获取指定集合中的所有元素
获取指定集合的元素数量
判断指定元素是否在指定集合中
获取给定所有集合的交集
将给定所有集合的交集存储在 destination 中
获取给定所有集合的并集
将给定所有集合的并集存储在 destination 中
获取给定所有集合的差集
将给定所有集合的差集存储在 destination 中
随机移除并获取指定集合中一个或多个元素
随机获取指定集合中指定数量的元素
- 应用场景
需要存放的数据不能重复的场景:
- 举例:网站 UV 统计(数据量巨大的场景还是
HyperLogLog
更适合一些)、文章点赞、动态点赞等场景。 - 相关命令:
SCARD
(获取集合数量) 。
需要获取多个数据源交集、并集和差集的场景
举例 :共同好友(交集)、共同粉丝(交集)、共同关注(交集)、好友推荐(差集)、音乐推荐(差集) 、订阅号推荐(差集+交集) 等场景。
相关命令:SINTER
(交集)、SINTERSTORE
(交集)、SUNION
(并集)、SUNIONSTORE
(并集)、SDIFF
(差集)、SDIFFSTORE
(差集)。
需要随机获取数据源中的元素的场景
- 举例 :抽奖系统、随机。
- 相关命令:
SPOP
(随机获取集合中的元素并移除,适合不允许重复中奖的场景)、SRANDMEMBER
(随机获取集合中的元素,适合允许重复中奖的场景)。
# 有序集合
向指定有序集合添加一个或多个元素
获取指定有序集合的元素数量
获取指定有序集合中指定元素的 score 值
将给定所有有序集合的交集存储在 destination 中,对相同元素对应的 score 值进行 SUM 聚合操作,numkeys 为集合数量
求并集,其它和 ZINTERSTORE 类似
求差集,其它和 ZINTERSTORE 类似
获取指定有序集合 start 和 end 之间的元素(score 从低到高)
获取指定有序集合 start 和 end 之间的元素(score 从高到底)
获取指定有序集合中指定元素的排名(score 从大到小排序)
- 常用场景
需要随机获取数据源中的元素根据某个权重进行排序的场景
举例 :各种排行榜比如直播间送礼物的排行榜、朋友圈的微信步数排行榜、王者荣耀中的段位排行榜、话题热度排行榜等等。
相关命令 :ZRANGE
(从小到大排序) 、 ZREVRANGE
(从大到小排序)、ZREVRANK
(指定元素排名)。
需要存储的数据有优先级或者重要程度的场景 比如优先级任务队列。
- 举例 :优先级任务队列。
- 相关命令 :
ZRANGE
(从小到大排序) 、ZREVRANGE
(从大到小排序)、ZREVRANK
(指定元素排名)。
# 5.redis多路复用机制
redis是一个单线程的模型,并才用了多路复用的的技术
而多路复用也是与Reactor 时间驱动相关的。
redis服务器是一个事件驱动的程序,处理的事件包括时间事件和文件事件
时间事件: Redis 将所有时间事件都放在一个无序链表中,每次 Redis 会遍历整个链表,查找所有已经到达的时间事件,并且调用相应的事件处理器。
而文件事件又包括4个部分
- socket套接字:用于建立与客户端的连接
- IO多路复用程序:用于单个线程监听过个socket套接字,底层会使用select、epoll等机制进行监听。
- 文件事件分配器:将socket关联的相应的事件处理器
- 事件处理器:根据不同的socket进行事件的处理(连接应答处理器、命令请求处理器、命令回复处理器)
客户端向服务端发起建立 socket 连接的请求,那么监听套接字将产生 AE_READABLE 事件,触发连接应答处理器执行。处理器会对客户端的连接请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_READABLE 事件与命令请求处理器关联。 客户端建立连接后,向服务器发送命令,那么客户端套接字将产生 AE_READABLE 事件,触发命令请求处理器执行,处理器读取客户端命令,然后传递给相关程序去执行。 执行命令获得相应的命令回复,为了将命令回复传递给客户端,服务器将客户端套接字的 AE_WRITEABLE 事件与命令回复处理器关联。当客户端试图读取命令回复时,客户端套接字产生 AE_WRITEABLE 事件,触发命令回复处理器将命令回复全部写入到套接字中。
优点:这样的好处非常明显: I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector
组件很像)。
# 6.redis的内存管理
# ①Redis 给缓存数据设置过期时间有啥用?
一般情况下,我们设置保存的缓存数据的时候都会设置一个过期时间。为什么呢?
因为内存是有限的,如果缓存中的所有数据都是一直保存的话,分分钟直接 Out of memory。
Redis 自带了给缓存数据设置过期时间的功能,比如:
127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
(integer) 1
127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
OK
127.0.0.1:6379> ttl key # 查看数据还有多久过期
(integer) 56
2
3
4
5
6
注意:Redis 中除了字符串类型有自己独有设置过期时间的命令 setex
外,其他方法都需要依靠 expire
命令来设置过期时间 。另外, persist
命令可以移除一个键的过期时间。
过期时间除了有助于缓解内存的消耗,还有什么其他用么?
很多时候,我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效。
如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多。
# ②redis怎么判断key是否过期
Redis 通过一个叫做过期字典(可以看作是 hash 表)来保存数据过期的时间。过期字典的键指向 Redis 数据库中的某个 key(键),过期字典的值是一个 long long 类型的整数,这个整数保存了 key 所指向的数据库键的过期时间(毫秒精度的 UNIX 时间戳)。
typedef struct redisDb {
...
dict *dict; //数据库键空间,保存着数据库中所有键值对
dict *expires // 过期字典,保存着键的过期时间
...
} redisDb;
2
3
4
5
6
7
8
# ③过期数据的删除策略
**①定时删除:**在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。
优点:定时删除对内存是最友好的,能够保存内存的key一旦过期就能立即从内存中删除。
缺点:对CPU最不友好,在过期键比较多的时候,删除过期键会占用一部分 CPU 时间,对服务器的响应时间和吞吐量造成影响。
②惰性删除:设置该key 过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
优点:对 CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。
缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,从而造成内存泄漏。
③定期删除: 每隔一段时间,我们就对一些key进行检查,删除里面过期的key。每隔一段时间,我们就对一些key进行检查,删除里面过期的key。
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。
如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。
如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。
另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。
Redis默认采用的策略:定期删除+惰性删除
所有键读写命令执行之前都会调用 expireIfNeeded 函数对其进行检查,如果过期,则删除该键,然后执行键不存在的操作;未过期则不作操作,继续执行原有的命令。
函数以一定的频率运行,每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
另外默认删除策略还存在一个问题:大量key集中过期问题 缓存雪崩
key的删除是主线程来执行的。
定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。
如何解决呢?下面是两种常见的方法:
给 key 设置随机过期时间。
开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间。
2
3
4
5
6
7
我们看到,通过过期删除策略,对于某些永远使用不到的键,并且多次定期删除也没选定到并删除,那么这些键同样会一直驻留在内存中,又或者在Redis中存入了大量的键,这些操作可能会导致Redis内存不够用,这时候就需要Redis的内存淘汰策略了。(引出淘汰策略)
# ④内存淘汰策略
配置文件中可通过maxmemery_policy进行设置。当数据内存达到redis设置的最大内存maxmemory 后,会启动内存淘汰策略。
- volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的) 具体实现:局部LRU。redis会随机选择一部分数据进行LRU。这一部分数据的大小是可以配置的。
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
4.0 版本后增加以下两种:
- volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
- allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
通过淘汰机制定位热点数据,实现预估预估缓存热点数据可能占用的大小,然后设置redis的最大memery,经过淘汰策略后剩下的数据就是热点数据。
# 7.redis持久化机制
# ①rdb
Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。
通过配置文件中save或者使用bgsave命令可以触发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命令创建快照。
2
3
4
5
rdb持久化模式实际上是将某一时刻内存的快照存放进rdb文件中。
具体逻辑:
客户端发送的修改命令达到阈值后,触发rdb。首先服务端进程会fork一个子进程用户创建rdb文件(因为创建rdb文件比较耗时间,如果使用主进程,会阻塞,影响接收客户端的命令),并才用写时复制技术,加速创建子进程的效率。在子进程创建rdb的过程中,主进程仍然可以接收来自客户端的请求并正常进行。子进程创建文件成功后,替换掉以前的rdb文件
RDB 创建快照时会阻塞主线程吗?
Redis 提供了两个命令来生成 RDB 快照文件:
save
: 主线程执行,会阻塞主线程;bgsave
: 子线程执行,不会阻塞主线程,默认选项
RDB作用
- 主从复制
- 快速恢复数据
# 什么是写时复制技术
- Copy-on-write 简介
写时复制(Copy-on-write,COW),有时也称为隐式共享(implicit sharing)。COW 将复制操作推迟到第一次写入时进行:在创建一个新副本时,不会立即复制资源,而是共享原始副本的资源;当修改时再执行复制操作。通过这种方式共享资源,可以显著减少创建副本时的开销,以及节省资源;同时,资源修改操作会增加少量开销。
- 为什么需要 Copy-on-write?
当通过 fork()
来创建一个子进程时,操作系统需要将父进程虚拟内存空间中的大部分内容全部复制到子进程中(主要是数据段、堆、栈;代码段共享)。这个操作不仅非常耗时,而且会浪费大量物理内存。特别是如果程序在进程复制后立刻使用 exec
加载新程序,那么负面效应会更严重,相当于之前进行的复制操作是完全多余的。
因此引入了写时复制技术。内核不会复制进程的整个地址空间,而是只复制其页表,fork
之后的父子进程的地址空间指向同样的物理内存页。
但是不同进程的内存空间应当是私有的。假如所有进程都只读取其内存页,那么就可以继续共享物理内存中的同一个副本;然而只要有一个进程试图写入共享区域的某个页面,那么就会为这个进程创建该页面的一个新副本。
写时复制技术将内存页的复制延迟到第一次写入时,更重要的是,在很多情况下不需要复制。这节省了大量时间,充分使用了稀有的物理内存。
- Copy-on-write 实现原理
fork()
之后,内核会把父进程的所有内存页都标记为只读。一旦其中一个进程尝试写入某个内存页,就会触发一个保护故障(缺页异常),此时会陷入内核。
内核将拦截写入,并为尝试写入的进程创建这个页面的一个新副本,恢复这个页面的可写权限,然后重新执行这个写操作,这时就可以正常执行了。
内核会保留每个内存页面的引用数。每次复制某个页面后,该页面的引用数减少一;如果该页面只有一个引用,就可以跳过分配,直接修改。
这种分配过程对于进程来说是透明的,能够确保一个进程的内存更改在另一进程中不可见。
- 优缺点
优点:减少不必要的资源分配,节省宝贵的物理内存。
缺点:如果在子进程存在期间发生了大量写操作,那么会频繁地产生页面错误,不断陷入内核,复制页面。这反而会降低效率。
- 实际应用
Redis 的持久化机制中,如果采用 bgsave
或者 bgrewriteaof
命令,那么会 fork 一个子进程来将数据存到磁盘中。Redis 的读取操作多,因此这种情况下使用 COW 可以减少 fork()
操作的阻塞时间。
写时复制的思想在很多语言中也有应用,相比于传统的深层复制,能带来很大性能提升。比如 C++ 98 标准下的 std::string
就采用了写时复制的实现:
std::string x("Hello");
std::string y = x; // x、y 共享相同的 buffer
y += ", World!"; // 写时复制,此时 y 使用一个新的 buffer
// x 依然使用旧的 buffer
2
3
4
Golang、PHP 中的 string、array 也是写时复制。在修改这些类型时,如果其引用计数非零,则会复制一个副本。因此我们在 golang、php 中可以将字符串、数组当作值类型(values type)进行传递,即不会有传值复制的开销,也能保证其 immutable 的特性。
# ②AOF
与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:
appendonly yes
开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入到内存缓存 server.aof_buf
中,然后再根据 appendfsync
配置来决定何时将其同步到硬盘中的 AOF 文件。
AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof
。
在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步
2
3
为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec
选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
# 什么是先写命令后写日志
关系型数据库(如 MySQL)通常都是执行命令之前记录日志(方便故障恢复),而 Redis AOF 持久化机制是在执行完命令之后再记录日志。
为什么是在执行完命令之后记录日志呢?
- 避免额外的检查开销,AOF 记录日志不会对命令进行语法检查;
- 在命令执行完之后再记录,不会阻塞当前的命令执行。
这样也带来了风险(我在前面介绍 AOF 持久化的时候也提到过):
- 如果刚执行完命令 Redis 就宕机会导致对应的修改丢失;
- 可能会阻塞后续其他命令的执行(AOF 记录日志是在 Redis 主线程中进行的)
# ③aof重写
注意:aof重写不会读历史aof文件,而是基于当前内存数据重新写一个aof文件
AOF 重写了解吗?会fork子进程,写时复制
当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。
AOF 重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有 AOF 文件进行任何读入、分析或者写入操作。
在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新 AOF 文件期间,记录服务器执行的所有写命令。当子进程完成创建新 AOF 文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新 AOF 文件的末尾,使得新的 AOF 文件保存的数据库状态与现有的数据库状态一致。最后,服务器用新的 AOF 文件替换旧的 AOF 文件,以此来完成 AOF 文件重写操作。
Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。
# ④Redis 4.0 对于持久化机制做了什么优化?
由于 RDB 和 AOF 各有优势,于是,Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble
开启)。
如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
# ⑤aof与rdb选择
RDB 比 AOF 优秀的地方 :
- RDB 文件存储的内容是经过压缩的二进制数据, 保存着某个时间点的数据集,文件很小,适合做数据的备份,灾难恢复。AOF 文件存储的是每一次写命令,类似于 MySQL 的 binlog 日志,通常会必 RDB 文件大很多。当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。不过, Redis 7.0 版本之前,如果在重写期间有写入命令,AOF 可能会使用大量内存,重写期间到达的所有写入命令都会写入磁盘两次。
- 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。而 AOF 则需要依次执行每个写命令,速度非常慢。也就是说,与 AOF 相比,恢复大数据集的时候,RDB 速度更快。
AOF 比 RDB 优秀的地方 :
- RDB 的数据安全性不如 AOF,没有办法实时或者秒级持久化数据。生成 RDB 文件的过程是比繁重的, 虽然 BGSAVE 子进程写入 RDB 文件的工作不会阻塞主线程,但会对机器的 CPU 资源和内存资源产生影响,严重的情况下甚至会直接把 Redis 服务干宕机。AOF 支持秒级数据丢失(取决 fsync 策略,如果是 everysec,最多丢失 1 秒的数据),仅仅是追加命令到 AOF 文件,操作轻量。
- RDB 文件是以特定的二进制格式保存的,并且在 Redis 版本演进中有多个版本的 RDB,所以存在老版本的 Redis 服务不兼容新版本的 RDB 格式的问题。
- AOF 以一种易于理解和解析的格式包含所有操作的日志。你可以轻松地导出 AOF 文件进行分析,你也可以直接操作 AOF 文件来解决一些问题。比如,如果执行FLUSHALL命令意外地刷新了所有内容后,只要 AOF 文件没有被重写,删除最新命令并重启即可恢复之前的状态。
# 8.redis事务
# 如何使用 Redis 事务?
Redis 可以通过 MULTI
,EXEC
,DISCARD
和 WATCH
等命令来实现事务(transaction)功能。
> MULTI
OK
> SET PROJECT "JavaGuide"
QUEUED
> GET PROJECT
QUEUED
> EXEC
1) OK
2) "JavaGuide"
2
3
4
5
6
7
8
9
MULTI
open in new window (opens new window) 命令后可以输入多个命令,Redis 不会立即执行这些命令,而是将它们放到队列,当调用了 EXEC
open in new window (opens new window) 命令后,再执行所有的命令。
这个过程是这样的:
- 开始事务(
MULTI
); - 命令入队(批量操作 Redis 的命令,先进先出(FIFO)的顺序执行);
- 执行事务(
EXEC
)。
你也可以通过 DISCARD
open in new window (opens new window) 命令取消一个事务,它会清空事务队列中保存的所有命令。
> MULTI
OK
> SET PROJECT "JavaGuide"
QUEUED
> GET PROJECT
QUEUED
> DISCARD
OK
2
3
4
5
6
7
8
你可以通过WATCH
open in new window (opens new window) 命令监听指定的 Key,当调用 EXEC
命令执行事务时,如果一个被 WATCH
命令监视的 Key 被 其他客户端/Session 修改的话,整个事务都不会被执行。
# 客户端 1
> SET PROJECT "RustGuide"
OK
> WATCH PROJECT
OK
> MULTI
OK
> SET PROJECT "JavaGuide"
QUEUED
# 客户端 2
# 在客户端 1 执行 EXEC 命令提交事务之前修改 PROJECT 的值
> SET PROJECT "GoGuide"
# 客户端 1
# 修改失败,因为 PROJECT 的值被客户端2修改了
> EXEC
(nil)
> GET PROJECT
"GoGuide"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
不过,如果 WATCH 与 事务 在同一个 Session 里,并且被 WATCH 监视的 Key 被修改的操作发生在事务内部,这个事务是可以被执行成功的(相关 issue :WATCH 命令碰到 MULTI 命令时的不同效果open in new window (opens new window))。
事务内部修改 WATCH 监视的 Key:
> SET PROJECT "JavaGuide"
OK
> WATCH PROJECT
OK
> MULTI
OK
> SET PROJECT "JavaGuide1"
QUEUED
> SET PROJECT "JavaGuide2"
QUEUED
> SET PROJECT "JavaGuide3"
QUEUED
> EXEC
1) OK
2) OK
3) OK
127.0.0.1:6379> GET PROJECT
"JavaGuide3"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
事务外部修改 WATCH 监视的 Key:
> SET PROJECT "JavaGuide"
OK
> WATCH PROJECT
OK
> SET PROJECT "JavaGuide2"
OK
> MULTI
OK
> GET USER
QUEUED
> EXEC
(nil)
2
3
4
5
6
7
8
9
10
11
12
# Redis 支持原子性吗?
Redis 的事务和我们平时理解的关系型数据库的事务不同。我们知道事务具有四大特性: 1. 原子性,2. 隔离性,3. 持久性,4. 一致性。
- 原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
- 隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
- 持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
- 一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
Redis 事务在运行错误的情况下,除了执行过程中出现错误的命令外,其他命令都能正常执行。并且,Redis 是不支持回滚(roll back)操作的。因此,Redis 事务其实是不满足原子性的(而且不满足持久性)。
Redis 官网也解释了自己为啥不支持回滚。简单来说就是 Redis 开发者们觉得没必要支持回滚,这样更简单便捷并且性能更好。Redis 开发者觉得即使命令执行错误也应该在开发过程中就被发现而不是生产过程中。
你可以将 Redis 中的事务就理解为 :Redis 事务提供了一种将多个命令请求打包的功能。然后,再按顺序执行打包的所有命令,并且不会被中途打断。
除了不满足原子性之外,事务中的每条命令都会与 Redis 服务器进行网络交互,这是比较浪费资源的行为。明明一次批量执行多个命令就可以了,这种操作实在是看不懂。
因此,Redis 事务是不建议在日常开发中使用的。
# Redis 事务支持持久性吗?
Redis 不同于 Memcached 的很重要一点就是,Redis 支持持久化,而且支持 3 种持久化方式:
- 快照(snapshotting,RDB)
- 只追加文件(append-only file, AOF)
- RDB 和 AOF 的混合持久化(Redis 4.0 新增)
与 RDB 持久化相比,AOF 持久化的实时性更好。在 Redis 的配置文件中存在三种不同的 AOF 持久化方式( fsync
策略),它们分别是:
appendfsync always #每次有数据修改发生时都会调用fsync函数同步AOF文件,fsync完成后线程返回,这样会严重降低Redis的速度
appendfsync everysec #每秒钟调用fsync函数同步一次AOF文件
appendfsync no #让操作系统决定何时进行同步,一般为30秒一次
2
3
AOF 持久化的fsync
策略为 no、everysec 时都会存在数据丢失的情况 。always 下可以基本是可以满足持久性要求的,但性能太差,实际开发过程中不会使用。
因此,Redis 事务的持久性也是没办法保证的
# 如何解决 Redis 事务的缺陷?
Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。
一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。
如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。(所以我们要保证原子性就一定要保证lua脚本是写的正确的)并且,出错之前执行的命令是无法被撤销的。因此,严格来说,通过 Lua 脚本来批量执行 Redis 命令也是不满足原子性的。
# 9.redis bigkey问题
简述-》产生问题-》定位问题2方案-》如何解决问题
# bigKey是什么简述:
简单来说,如果一个 key 对应的 value 所占用的内存比较大,那这个 key 就可以看作是 bigkey。具体多大才算大呢?有一个不是特别精确的参考标准:string 类型的 value 超过 10 kb,复合类型的 value 包含的元素超过 5000 个(对于复合类型的 value 来说,不一定包含的元素越多,占用的内存就越多)。
# bigKey危害
消耗更多的内存
影响性能,阻塞主线程处理其他请求
影响主从同步,主从切换
删除一个大key造成主库较长时间的阻塞并引起同步中断或主从切换
# 如何发现bigKey
# redis-cli -p 6379 --bigkeys
从这个命令的运行结果,我们可以看出:这个命令会扫描(Scan) Redis 中的所有 key ,会对 Redis 的性能有一点影响。并且,这种方式只能找出每种数据结构 top 1 bigkey(占用内存最大的 string 数据类型,包含元素最多的复合数据类型)。
通过分析RDB
通过分析 RDB 文件来找出 big key。这种方案的前提是你的 Redis 采用的是 RDB 持久化。
网上有现成的代码/工具可以直接拿来使用:
- redis-rdb-toolsopen in new window (opens new window) :Python 语言写的用来分析 Redis 的 RDB 快照文件用的工具
- rdb_bigkeysopen in new window (opens new window) : Go 语言写的用来分析 Redis 的 RDB 快照文件用的工具,性能更好
要解决Big Key问题,无非就是减小key对应的value值的大小,也就是对于String数据结构的话,减少存储的字符串的长度;对于List、Hash、Set、ZSet数据结构则是减少集合中元素的个数。
1、对大Key进行拆分
将一个Big Key拆分为多个key-value这样的小Key,并确保每个key的成员数量或者大小在合理范围内,然后再进行存储,通过get不同的key或者使用mget批量获取。
2、对大Key进行清理
对Redis中的大Key进行清理,从Redis中删除此类数据。Redis自4.0起提供了UNLINK命令,该命令能够以非阻塞的方式缓慢逐步的清理传入的Key,通过UNLINK,你可以安全的删除大Key甚至特大Key。
3、监控Redis的内存、网络带宽、超时等指标
通过监控系统并设置合理的Redis内存报警阈值来提醒我们此时可能有大Key正在产生,如:Redis内存使用率超过70%,Redis内存1小时内增长率超过20%等。
4、定期清理失效数据
如果某个Key有业务不断以增量方式写入大量的数据,并且忽略了其时效性,这样会导致大量的失效数据堆积。可以通过定时任务的方式,对失效数据进行清理。
5、压缩value
使用序列化、压缩算法将key的大小控制在合理范围内,但是需要注意序列化、反序列化都会带来一定的消耗。如果压缩后,value还是很大,那么可以进一步对key进行拆分。
# 10.redis内存碎片问题
# 什么是内存碎片?
你可以将内存碎片简单地理解为那些不可用的空闲内存。
举个例子:操作系统为你分配了 32 字节的连续内存空间,而你存储数据实际只需要使用 24 字节内存空间,那这多余出来的 8 字节内存空间如果后续没办法再被分配存储其他数据的话,就可以被称为内存碎片。
Redis 内存碎片虽然不会影响 Redis 性能,但是会增加内存消耗。
# 为什么会有 Redis 内存碎片?
Redis 内存碎片产生比较常见的 2 个原因:
1、Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
以下是这段 Redis 官方的原话:
To store user keys, Redis allocates at most as much memory as the
maxmemory
setting enables (however there are small extra allocations possible).
Redis 使用 zmalloc
方法(Redis 自己实现的内存分配方法)进行内存分配的时候,除了要分配 size
大小的内存之外,还会多分配 PREFIX_SIZE
大小的内存。
zmalloc
方法源码如下(源码地址:https://github.com/antirez/redis-tools/blob/master/zmalloc.c):
void *zmalloc(size_t size) {
// 分配指定大小的内存
void *ptr = malloc(size+PREFIX_SIZE);
if (!ptr) zmalloc_oom_handler(size);
#ifdef HAVE_MALLOC_SIZE
update_zmalloc_stat_alloc(zmalloc_size(ptr));
return ptr;
#else
*((size_t*)ptr) = size;
update_zmalloc_stat_alloc(size+PREFIX_SIZE);
return (char*)ptr+PREFIX_SIZE;
#endif
}
2
3
4
5
6
7
8
9
10
11
12
13
另外,Redis 可以使用多种内存分配器来分配内存( libc、jemalloc、tcmalloc),默认使用 jemallocopen in new window (opens new window),而 jemalloc 按照一系列固定的大小(8 字节、16 字节、32 字节......)来分配内存的。jemalloc 划分的内存单元如下图所示:
当程序申请的内存最接近某个固定值时,jemalloc 会给它分配相应大小的空间,就比如说程序需要申请 17 字节的内存,jemalloc 会直接给它分配 32 字节的内存,这样会导致有 15 字节内存的浪费。不过,jemalloc 专门针对内存碎片问题做了优化,一般不会存在过度碎片化的问题。
2、频繁修改 Redis 中的数据也会产生内存碎片。
当 Redis 中的某个数据删除时,Redis 通常不会轻易释放内存给操作系统。
这个在 Redis 官方文档中也有对应的原话:
文档地址:https://redis.io/topics/memory-optimization 。
# 如何查看 Redis 内存碎片的信息?
使用 info memory
命令即可查看 Redis 内存相关的信息。下图中每个参数具体的含义,Redis 官方文档有详细的介绍:https://redis.io/commands/INFO 。
Redis 内存碎片率的计算公式:mem_fragmentation_ratio
(内存碎片率)= used_memory_rss
(操作系统实际分配给 Redis 的物理内存空间大小)/ used_memory
(Redis 内存分配器为了存储数据实际申请使用的内存空间大小)
也就是说,mem_fragmentation_ratio
(内存碎片率)的值越大代表内存碎片率越严重。
一定不要误认为used_memory_rss
减去 used_memory
值就是内存碎片的大小!!!这不仅包括内存碎片,还包括其他进程开销,以及共享库、堆栈等的开销。
很多小伙伴可能要问了:“多大的内存碎片率才是需要清理呢?”。
通常情况下,我们认为 mem_fragmentation_ratio > 1.5
的话才需要清理内存碎片。 mem_fragmentation_ratio > 1.5
意味着你使用 Redis 存储实际大小 2G 的数据需要使用大于 3G 的内存。
如果想要快速查看内存碎片率的话,你还可以通过下面这个命令:
> redis-cli -p 6379 info | grep mem_fragmentation_ratio
另外,内存碎片率可能存在小于 1 的情况。这种情况我在日常使用中还没有遇到过,感兴趣的小伙伴可以看看这篇文章 故障分析 | Redis 内存碎片率太低该怎么办?- 爱可生开源社区open in new window (opens new window) 。
# 如何清理 Redis 内存碎片?
Redis4.0-RC3 版本以后自带了内存整理,可以避免内存碎片率过大的问题。
直接通过 config set
命令将 activedefrag
配置项设置为 yes
即可。
config set activedefrag yes
具体什么时候清理需要通过下面两个参数控制:
# 内存碎片占用空间达到 500mb 的时候开始清理
config set active-defrag-ignore-bytes 500mb
# 内存碎片率大于 1.5 的时候开始清理
config set active-defrag-threshold-lower 50
2
3
4
通过 Redis 自动内存碎片清理机制可能会对 Redis 的性能产生影响,我们可以通过下面两个参数来减少对 Redis 性能的影响:
# 内存碎片清理所占用 CPU 时间的比例不低于 20%
config set active-defrag-cycle-min 20
# 内存碎片清理所占用 CPU 时间的比例不高于 50%
config set active-defrag-cycle-max 50
2
3
4
另外,重启节点可以做到内存碎片重新整理。如果你采用的是高可用架构的 Redis 集群的话,你可以将碎片率过高的主节点转换为从节点,以便进行安全重启