一、Redis 简介

Redis 是一个开源(BSD 许可)的内存数据结构存储库,可用作数据库、缓存、消息代理和流引擎。Redis 提供字符串、哈希值、列表、集合、带范围查询的排序集合、位图、hyperloglogs、地理空间索引和流等数据结构。Redis 内置复制、Lua 脚本、LRU 驱逐、事务和不同级别的磁盘持久性,并通过 Redis Sentinel 和 Redis Cluster 自动分区提供高可用性。
你可以在这些类型上运行原子操作,如追加字符串;递增哈希值;向列表添加元素;计算集合的交集、联合和差值;或获取排序集合中排名最靠前的成员。
为了达到最佳性能,Redis 使用内存数据集。根据使用情况,Redis 可以通过定期将数据集转储到磁盘或将每条命令附加到基于磁盘的日志来持久化数据。如果你只需要功能丰富的网络内存缓存,也可以禁用持久化功能。
Redis 支持异步复制,具有快速无阻塞同步和自动重新连接功能,并可在网络拆分时进行部分重新同步。

Redis 由 ANSI C 编写,可在大多数 POSIX 系统(如 Linux、*BSD 和 Mac OS X)上运行,无需外部依赖。Linux 和 OS X 是 Redis 开发和测试最多的两个操作系统,官方建议使用 Linux 进行部署。Redis 可在 Solaris 衍生系统(如 SmartOS)中运行,但只能尽力提供支持。Windows 版本没有官方支持。

Windows 版下载

官方没有提供 Windows 的安装包,推荐部署到 Linux 中的,但是有时候我们需要在客户的 Windows 中开发,本地的服务还是很有必要的。可以在 Github 中下载 Redis-Version-Windows-x64-msys2-with-Service.tar.gzwith-service 后缀的包可以注册服务自动运行。

注册 Redis 服务并启动

1
2
sc.exe create Redis binpath="D:/xxx/xxx/Redis-7.2.5-Windows-x64-msys2-with-Service/RedisService.exe" start= auto
net start Redis

binpath 声明的路径必须使用引号!

停止并卸载

1
2
3
4
# 停止 Redis 服务
net stop Redis
# 卸载
sc.exe delete Redis

Linux 安装

1
2
3
4
5
6
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg

echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list

sudo apt-get update
sudo apt-get install redis

Redis 各个版本的配置文件

https://redis.io/docs/management/config/

二、Redis 不同版本

Redis 4.0 中开始使用多线程,但是用户无法定义;Redis 的业务主要由主线程处理,后台线程只处理一些较慢的操作,例如:清理数据、释放无用连接。

Redis 6.0 中引入多线程,并且用户可以配置,多线程默认是 关闭 的,开启需要修改 io-threads-do-readsyes,然后修改使用的线程数:io-threads

在 Redis 7 及更高版本中,可以使用 Redis Functions 来管理和运行脚本。在 Redis 6.2 及更低版本中,可以使用带有 EVAL 命令的 Lua 脚本对服务器进行编程。

Functions 与数据本身一起存储。Functions 也会持久化到 AOF 文件中,并从主服务器复制到副本,因此它们与数据本身存储的时间一样。当 Redis 用作短暂缓存时,还需要额外的机制(如下所述)来提高函数的持久性。
与 Redis 中的所有其他操作一样,函数的执行是原子性的。函数的执行会在整个过程中阻塞所有服务器活动,这与事务的语义类似。这些语义意味着脚本的所有效果要么尚未发生,要么已经发生。已执行函数的阻塞语义始终适用于所有已连接的客户端。由于运行函数会阻塞 Redis 服务器,因此函数的目的是快速完成执行,因此应避免使用长时间运行的函数。
每个程序库至少需要包含一个注册函数才能成功加载。已注册函数会被命名,并作为库的入口点。目标执行引擎在处理 FUNCTION LOAD 命令时,会注册库的函数。

在 v7.0 中添加的 Redis 函数本质上是作为第一类数据库元素的脚本。因此,函数将脚本与应用程序逻辑分离,并支持脚本的独立开发、测试和部署。若要使用函数,需要先加载它们,然后它们可供所有连接的客户端使用。在这种情况下,将函数加载到数据库成为一项管理部署任务,这会将脚本与应用程序分开。

Redis 持久化方式

RDB(Redis Database):RDB 持久性可按指定时间间隔对数据集执行时间点快照。
AOF(Append Only File): AOF 持久化会记录服务器收到的每个写操作。这些操作可以在服务器启动时再次重放,重建原始数据集。命令记录格式与 Redis 协议本身相同。
无持久性 可以完全禁用持久化。这有时会在缓存时使用。
RDB + AOF:还可以在同一实例中结合使用 AOF 和 RDB。

1. RDB(时间间隔内的数据快照)

优势
  • RDB 是 Redis 数据非常紧凑的单文件时间点表示。RDB 文件非常适合备份。例如,你可能希望每小时归档最近 24 小时的 RDB 文件,每天保存一个 RDB 快照,持续 30 天。这样就可以在发生灾难时轻松恢复数据集的不同版本。
  • RDB 非常适合灾难恢复,因为它是一个紧凑的文件,可以传输到远处的数据中心或亚马逊 S3(可能已加密)等。
  • RDB 能最大限度地提高 Redis 的性能,因为 Redis 父进程为持久化所需做的唯一工作就是分叉一个子进程,由子进程完成其余所有工作。父进程永远不会执行磁盘 I/O 或类似操作。
  • 与 AOF 相比,RDB 允许更快地重启大数据集。
  • 在副本上,RDB 支持重启和故障切换后的部分重新同步
不足
  • 如果需要在 Redis 停止工作(例如停电后)时尽量减少数据丢失的几率,那么 RDB 就不太合适。你可以配置生成 RDB 的不同保存点(例如,数据集至少经过 5 分钟和 100 次写入后,你可以有多个保存点)。不过,你通常会每五分钟或更长时间创建一次 RDB 快照,因此,如果 Redis 因故停止工作而没有正确关机,你应该做好丢失最近几分钟数据的准备。
  • 如果数据集很大,fork() 可能会很耗时,如果数据集很大,CPU 性能又不高,可能会导致 Redis 在几毫秒甚至一秒内停止为客户端提供服务。AOF 也需要 fork(),但频率较低,你可以调整重写日志的频率,而无需牺牲可用性。

2. AOF(操作记录)

优势
  • 使用 AOF Redis 则更耐用:你可以使用不同的 fsync 策略:完全不执行 fsync、每秒执行 fsync、每次查询都执行 fsync。fsync 是使用后台线程执行的,主线程会在没有 fsync 的情况下努力执行写入操作,因此只会损失一秒钟的写入量。
  • AOF 日志是一种仅追加的日志,因此不会出现寻道问题,也不会在断电时出现损坏问题。即使由于某种原因(磁盘已满或其他原因),日志以写入一半的命令结束,redis-check-aof 工具也能轻松修复。
  • 当 AOF 过大时,Redis 会在后台自动重写。重写是完全安全的,因为 Redis 在继续追加旧文件的同时,会用创建当前数据集所需的最少操作集生成一个全新的文件,一旦第二个文件准备就绪,Redis 就会切换这两个文件,并开始追加新文件。
  • AOF 包含一个接一个操作的日志,格式简单易懂,易于解析。你甚至可以轻松导出 AOF 文件。例如,即使你不小心使用 FLUSHALL 命令刷新了所有数据,只要在此期间没有重写日志,你仍然可以通过停止服务器、删除最新命令并重新启动 Redis 来保存数据集。
不足
  • 对于相同的数据集,AOF 文件通常比相应的 RDB 文件大。
  • AOF 可能比 RDB 慢,这取决于具体的同步策略。一般来说,当 fsync 设置为每秒一次时,AOF 的性能仍然很高,而当 fsync 禁用时,即使在高负载情况下,AOF 的速度也应与 RDB 完全相同。不过,即使在写入负载很大的情况下,RDB 也能提供更多关于最大延迟的保证。

在 Redis 7.x 以下,还可能会有以下情况:

  • 如果在重写过程中向数据库写入内容(这些内容在内存中缓冲,并在最后写入新的 AOF),AOF 可能会占用大量内存。
  • 在重写过程中到达的所有写入命令都会写入磁盘两次。
  • Redis 可能会在重写结束时冻结向新的 AOF 文件写入和同步这些写入命令。

what should I use?

一般来说,如果你想获得与 PostgreSQL 相当的数据安全程度,就应该同时使用这两种持久化方法。

如果你非常关心你的数据,但仍然可以忍受灾难发生时几分钟的数据丢失,那么你可以只使用 RDB。

有许多用户只使用 AOF,但我们不鼓励他们这样做,因为偶尔或者说拥有一个某一时刻的 RDB 快照是一个很好的主意,这样可以进行数据库备份、更快地重新启动,以及在 AOF 引擎出现错误时使用。

以上内容机翻+自翻译官网内容

内存淘汰策略/驱逐策略

内存淘汰策略:Redis 的内存淘汰策略是指在Redis的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

三、Redis 的几种模式

  1. 主从模式
    redis 主从复制,从节点默认是只读的,当 master 服务挂掉之后,从节点不能代替主节点;主从复制架构只是一个数据的备份。
  2. 哨兵模式(建立在主从模式上)
    哨兵的作用,就是监控主、从数据库的状态,当主数据库宕机以后,哨兵会在一定的时间内去判断,然后在从数据库中选举出一个去顶替主数据库,从而实现redis数据的高可用。
  3. 集群模式

Redis 的集群模式使用了 CRC16 算法,该算法有以下特点:

  1. 对集群模式下的所有key进行 CRC16 计算,计算的结果始终在 0-16383 之间(Redis 有 16384 的 slot)
  2. 对客户端的 key 进行 CRC16 算法计算时,同一个 key 经过多次计算,计算结果始终一致。
  3. 对客户端的不同的 key 进行 CRC16 计算,会出现不同的 key 计算结果一致(类 Hash 碰撞)。

每一个节点存储 CRC16 计算后的数据范围。

伪集群搭建配置文件示例

Redis 集群的最大物理节点数不能超过 16384。动态添加节点后,分配的 slot 需要重新分配,具体如何分配可以自定义。slot 是存储 value 值的。

搭建 redis 伪集群实现 session 共享:
修改不同端口的配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 启动端口号
port 7000
# 允许远程连接
bind 0.0.0.0
# rdb 方式持久化数据的文件名
dbfilename dump-7000.rdb
# 开启守护进程(即后台运行)
daemonize yes
# 开启 aof 缓存并指定 aof 方式持久化数据的文件名
appendonly yes
appendonlyfilename "appendonly-7000.aof"
# 开启集群配置
cluster-enabled yes
# 配置集群节点名称
cluster-config-file nodes-7000.conf
# 集群超时时间 5s
cluster-node-timeout 5000

配置主从数据库后使用 redis-cli 进行连接后,使用 info replication 命令查看当前数据库的 role,“master” 是主节点,“slave” 是从节点。默认从库是只读的,不能进行写操作。

使用 ruby 脚本创建 Redis 集群,包括添加主节点、从节点。

四、缓存设计典型问题

1. 缓存穿透/击穿

缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中。通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。

造成缓存穿透的原因基本有两个:

第一,自身业务代码或者数据出现问题。第二,一些恶意攻击、爬虫等造成大量空命中。

解决方案:

  1. 缓存空对象;
  2. 布隆过滤器
    对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据,布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。

布隆过滤器

布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。 向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash,算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。
向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个 key 不存在。如果都是 1,这并不能说明这 个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组比较稀疏,这个概率就会很大,如果这个位数组比较拥挤,这个概率就会降低。
这种方法适用于数据命中不高、数据相对固定、实时性低(通常是数据集较大)的应用场景,代码维护较为 复杂,但是缓存空间占用很少。

2. 缓存雪崩

由于缓存层承载着大量请求,能够有效地保护存储层,但是如果缓存层由于某些原因不能提供服务(比如超大并发,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降),于是大量请求都会打到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。

预防和解决缓存雪崩问题,可以从以下三个方面进行着手。

  1. 保证缓存层服务高可用性,比如使用 Redis Sentinel 或 Redis Cluster。
  2. 依赖隔离组件为后端限流熔断并降级。比如使用 Sentinel 或 Hystrix 限流降级组件。比如服务降级,我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商 品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。
  3. 提前演练。在项目上线前,演练缓存层宕掉后,应用以及后端的负载情况以及可能出现的问题,在此基础上做一些预案设定。

3. 热点缓存key重建优化

开发人员使用“缓存+过期时间”的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现,可能就会对应用造成致命的危害:当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依赖等。在缓存失效的瞬间, 有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
要解决这个问题主要就是要避免大量线程同时重建缓存。我们可以利用互斥锁来解决,此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。

伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
String get(String key) { 
// 从Redis中获取数据
String value = redis.get(key);
// 如果value为空,则开始重构缓存
if (value == null) {
String mutexKey = "mutext:key:" + key;
// 只允许一个线程重建缓存,使用nx,并设置过期时间ex
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
// 从数据源获取数据
value = db.get(key);
// 回写Redis,并设置过期时间
redis.setex(key, timeout, value);
// 删除key_mutex
redis.delete(mutexKey);
} else {
// 其他线程休息50毫秒后重试
Thread.sleep(50);
get(key);
}
}
return value;
}

五、规范建议

1. key 设计

  1. 若需要可读性和可管理性:以业务名(或数据库名)为前缀(防止 key 冲突),用冒号分隔,比如 业务名:表名:id,不要包含特殊字符。
  2. 若不需要可读:使用 md5 进行处理作为 key。
  3. 简洁性:控制 key 的长度,当 key 较长时,内存占用也不容忽视。

2. value 设计

  1. 拒绝bigkey(防止网卡流量、慢查询) 在Redis中,一个字符串最大512MB,一个二级数据结构(例如hash、list、set、zset)可以存 储大约40亿个(2^32-1)个元素,但实际中如果下面两种情况,就可以认为它是bigkey。
    1. 字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey。
    2. 非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多。一般来说,string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。反例:一个包含200万个元素的list。非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞)。

bigkey的危害:

  1. 导致redis阻塞
  2. 网络拥塞
    bigkey也就意味着每次获取要产生的网络流量较大,假设一个 bigkey 为 1 MB,客户端每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个 bigkey 可能会对其他实例也造成影响,其后果不堪设想。
  3. 过期删除
    有个bigkey,它安分守己(只执行简单的命令,例如hget、lpop、zscore等),但它设置了过期时间,当它过期后,会被删除,如果没有使用 Redis4.0 的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞Redis的可能性。

一般来说,bigkey 的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的,来看几个例子:
(1) 社交类:粉丝列表,如果某些明星或者大v不精心设计下,必是bigkey。
(2) 统计类:例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是bigkey。
(3) 缓存类:将数据从数据库 load 出来序列化放到 Redis 里,这个方式非常常用,但有两个地方需要注意。第一,是不是有必要把所有字段都缓存;第二,有没有相关关联的数据,有的同学为了图方便把相关数据都存一个 key 下,产生 bigkey。

如何优化bigkey?


  1. big list: list1、list2、…listN;
    big hash:可以讲数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成 200 个key,每个key下面存放5000个用户数据
  2. 如果 bigkey 不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要 hmget,而不是 hgetall),删除也是一样,尽量使用优雅的方式来处理。
  3. 选择适合的数据类型。 例如:实体类型(要合理控制和使用数据结构,但也要注意节省内存和性能之间的平衡)
1
hmset user:1 name tom age 19 favor football
1
2
3
set user:1:age 19 3 
set user:1:favor football
set user:1:name tom

本站由 江湖浪子 使用 Stellar 1.29.1 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。