基本概念

​ Redis 是基于内存的数据库,读写内存数据是单线程完成的,官方提供测试数据,单线程支持 10w+ QPS,其主要原因以下几点:

  • 单线程避免了多线程切换、竞争阻塞、锁,一个指令通常 1-4 微秒,单核 CPU 足够处理数十万请求
  • 完全基于内存读写,数据存放于内存,查找和操作效率高于磁盘
  • 多路 I/O 复用模型,采用 epoll 实现,一定程度上提高网络 I/O 吞吐性能
  • 6.0 后支持多线程 I/O 模型,提高网络 I/O 瓶颈,内存数据操作仍然是单线程

多路 I/O 复用,多路复用机制是指在一个线程上处理多个网络 I/O 流,Redis 网络框架调用 epoll 机制,让 Linux 内核监听无数个套接字(Socket),每个监听套接字发生的不同事件,都进入事件队列,Redis 单线程对该事件队列进行处理并调相应的事件回调函数,Redis 本身无需一直轮询是否有连接请求。

1 单线程

​ 单线程读写内存的缺点是无法发挥多核心的性能,可以在多核部署多个 Redis 实例避免浪费,或者利用容器机制,一个 Redis 容器分配 2 个核心足够。

​ 对单线程 Redis 来说,性能瓶颈主要在于网络 I/O,因为不可能应用和 Redis 单独部署在同一系统中做进程通讯,通常都是多个应用与 Redis 做 TCP 通讯,理论上多路 I/O 复用可以支持并发数十万请求,但实践中 TCP 握手的开销、长连接的数量,以及读写的数据本身,都有着一定的影响,实践中通常 4-5 万左右 QPS。

2 多线程

​ 6.0 以后,Redis 开始引入多线程,因为即使多路 I/O 复用单线程,效率再高,也很难更进一步突破瓶颈。在 6.0 之后正式引入多线程模式优化网络 I/O 瓶颈,默认是非开启的。

​ 配置io-threads-do-reads yes 开启多线程 I/O 模式,默认 no 关闭

io-threads线程数官方推荐 4 核时 2-3 个线程,8 核时 6 个线程,线程数维持在核心数 - 1 或 核心数 -2,超过 8 个线程后的再进一步提升性能的作用不大。

3 惰性释放(懒删除)

​ 在 redis 4.0 之前,数据较大的 key(如数十万、上百万个元素的集合),对它们 del 或者清理时,效率较低易引起阻塞;针对这个问题,4.0 之后增加了惰性删除机制,清理数据时先摘除 key 索引,再异步线程去做实际的数据清除,避免引起阻塞,比如 unlink、flushdb、flushall 这些命令都是如此。

​ 同时也可以在配置文件中,针对内存上限清理策略、过期 key 的清理策略、隐式 del 指令(如 rename)等,都可以指定使用懒删除的方式。

安装与开机启动

包下载完成后,解压文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#当前目录为/usr/local/
#下载源码
wget https://download.redis.io/releases/redis-6.2.7.tar.gz
#解压文件
tar -zvxf redis-6.2.7.tar.gz
#安装相关依赖
yum -y install cpp glibc glibc-kernheaders glibc-devel gcc make
#编译
cd /usr/local/redis-6.2.7
make PREFIX=/usr/local/redis install
#添加运行用户
groupadd redis
useradd redis -g redis -p 密码
#授权文件给用户
chown -R redis:redis /usr/local/redis
su redis

主要文件包括为:

文件 说明
redis-server 服务实例
redis-cli 指令工具
redis-sentile 哨兵实例
redis.conf 默认配置文件
redis-benchmark 性能测试工具
redis-check-aof aof 持久化工具
redis-check-rdb rdb 持久化工具

1 启动一个服务

1.1 新建配置文件

vim /usr/local/redis/redis.conf(详细配置参考)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
tcp-keepalive 300
timeout 0

daemonize yes

save 900 1
save 300 10
save 60 10000

appendonly yes

maxmemory-policy allkeys-lru

lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes

bind 0.0.0.0
port 6379
#启停脚本shell也需要pid
pidfile /usr/local/redis/pid/node.pid

logfile "/usr/local/redis/log/node.log"

dir /usr/local/redis/data

dbfilename node.rdb
appendfilename "node.aof"

​ 在根目录执行 redis-server

1
2
3
4
su redis

# 按配置文件redis.conf启动一个单节点实例
/usr/local/redis/bin/redis-server /usr/local/redis/redif.conf

2 连接服务节点

1
2
3
4
5
su redis
/usr/local/redis/bin/redis-cli -h 127.0.0.1 -p 6379
....
#连接成功后,如果需要密码,输入后即可执行其它命令
auth 密码

3 自定义服务

注册为系统服务开机启动,方便管理

3.1 chkconfig 方式,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 新建服务配置文件
vim /etc/init.d/redis

#编辑内容如下:

#!/bin/sh
# chkconfig: 2345 10 90
# start/stop/restart anything

#待添加到PATH环境变量的路径,如Java应用依赖的$JAVA_HOME/bin:$JRE_HOME/bin
ADDPATH=/usr/local/redis/bin

#新 PATH生效
PATH=$PATH:$ADDPATH
export PATH
source /etc/profile

#运行应用的用户
EXEC_USER=redis

#程序运行文件
EXEC_FILE=/usr/local/redis/bin/redis-server
#运行时PID,停止服务时,先按运行文件查找pid进行kill,如果失败,则按此pid文件的进程号kill
EXEC_PID=/usr/local/redis/pid/node.pid

#运行参数
EXEC_OPTS="/usr/local/redis/conf/redis.conf"

start() {
echo "$EXEC_FILE starting ...... "
#chkconfig方式,切换用户
su - $EXEC_USER -c "$EXEC_FILE $EXEC_OPTS"
exr=$?
if [ $exr -ne 0 ]; then
echo "start error"
else
echo "started"
fi
}
stop() {
echo "stopping $EXEC_FILE ..."
kill -15 `ps -ef|grep $EXEC_FILE|grep -v grep|grep -v stop|awk '{print $2}'`
exr1=$?
if [ $exr1 -ne 0 ]; then
echo "stop error, retry......"
kill -15 `cat $EXEC_PID`
exr2=$?
if [ $exr2 -ne 0 ]; then
echo "stop error"
else
echo "stopped"
fi
else
echo "stopped"
fi
}
case "$1" in
start)
start
;;
stop)
stop
sleep 6
;;
restart)
stop
sleep 6
start
;;
*)
echo "Userage: $0 {start|stop|restart}"
exit 1
esac

1
2
3
4
5
6
#保存服务配置文件后,赋权
chmod +x /etc/init.d/redis

#开机启动服务
chkconfig --add redis
chkconfig redis on

3.2 systemd 方式,CentOS7 及以上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#新建运行脚本,
vim /usr/local/redis/mananger.sh
# .... 脚本内容和上述chkconfig方式的脚本内容大体一致 ...
# su - $EXEC_USER -c "$EXEC_FILE $EXEC_OPTS" 改为以下
$EXEC_FILE $EXEC_OPTS

#修改完成后,

#新建systemd服务,并指定mananger.sh脚本路径
vim /lib/systemd/system/redis.service

#编辑内容如下
[Unit]
Description=redis
After=network.target

[Service]
Type=forking
User=redis
Group=redis
ExecStart=/usr/local/redis/mananger.sh start
ExecReload=/usr/local/redis/mananger.sh restart
ExecStop=/usr/local/redis/mananger.sh stop
PrivateTmp=true

[Install]
WantedBy=multi-user.target

1
2
3
4
5
#保存systemd服务脚本文件后,赋权
chmod +x /lib/systemd/system/redis.service

#绑定服务开机启动
systemctl enable redis

4 性能测试工具

跟目录下的 redis-benchmark,不安装的话在 redis src 目录下

参数 说明
-h 服务节点主机,默认值 127.0.0.1
-p 服务节点端口,默认 6379
-s 指定服务器 socket
-c 指定并发的连接数,默认 50
-n 指定请求数,默认 10000
-d 以字节的形式指定 SET/GET 值的数据大小,默认 3
-k 1 保持连接,0 重连
-P 通过管道传输请求
-q 强制退出 redis
–csv CSV 格式输出
-l 循环测试
-t 指定测试的命令,如 set,get

示例:

执行如下

1
2
# 对127.0.0.1 6379这个节点,并发50执行10w个请求,只测试set和get
/usr/local/redis/bin/redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 100000 -t set,get

响应结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
====== SET ======    #写入测试
100000 requests completed in 3.73 seconds # 3.73秒写入10万个请求
50 parallel clients # 50个并发连接
3 bytes payload # 每次写入3个字节
keep alive: 1
host configuration "save": 900 1 300 10 60 10000
host configuration "appendonly": no
multi-thread: no
0.00% <= 0.4 milliseconds
0.00% <= 0.5 milliseconds
0.38% <= 0.6 milliseconds
4.93% <= 0.7 milliseconds
14.52% <= 0.8 milliseconds
16.92% <= 0.9 milliseconds
19.59% <= 1.0 milliseconds # 1秒完成19.59%的请求写入
31.63% <= 1.1 milliseconds
51.21% <= 1.2 milliseconds
69.20% <= 1.3 milliseconds
77.85% <= 1.4 milliseconds
81.71% <= 1.5 milliseconds
84.54% <= 1.6 milliseconds
90.08% <= 1.7 milliseconds
95.66% <= 1.8 milliseconds
98.64% <= 1.9 milliseconds
99.51% <= 2 milliseconds
100.00% <= 2 milliseconds # 2秒完成所有请求写入
26795.28 requests per second

====== GET ====== # get测试
100000 requests completed in 3.57 seconds # 3.57秒完成10万个请求
50 parallel clients #50个并发客户端
3 bytes payload # 每个get测试,3个字节大小
keep alive: 1
host configuration "save": 900 1 300 10 60 10000
host configuration "appendonly": no
multi-thread: no

22.19% <= 1 milliseconds # 1秒完成22.19%的请求
99.95% <= 2 milliseconds
100.00% <= 2 milliseconds # 2秒完成100% 10万个请求
28011.21 requests per second

5 事务

传统数据库像 MySQL InnoDB 和 OracleDB 考虑效率原因,都是日志先行策略,并不会实时将内存数据写入磁盘。

以 InnoDB 为例,应用端提交事务后,数据库服务仅仅在内存完成了修改,redo log 前滚日志记录事务,落地后便响应事务成功,此时内存中的数据并未写入磁盘,如果内存数据丢失,则可以通过 redo log 前滚恢复。

undo log 是对事务提交前的数据进行备份,该日志持续顺序落库(写磁盘),当有事务未最终 commit,而又有连接查询数据时,undo log 可以回滚保证数据一致性。

也就是前滚日志保证数据持久性,回滚日志保障原子性从而实现一致性。

Redis 因为是以内存为主的数据库,所以没有传统数据库复杂的机制。

对于 Redis 来说,事务就是将每一条指令当作 queue,放入队列,排队处理。

5.1 流程如下:

multi 开启事务队列

事务队列[

命令 1

命令 2

命令 n…

]

exec 执行事务队列 / discard 取消执行

Redis 事务是非原子性的,也就是 开启事务后,每一条指令都是一个 queue,其中一个 queue 有错误不影响其它指令 queue 执行生效,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> set k1 "str"	#设置 k1 值为 字符串str
OK
127.0.0.1:6379> get k1 # 查询到 k1 值为 "str"
"str"
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> incr k1 #事务指令1, 对k1的值"str"进行自增1,一定错误
QUEUED
127.0.0.1:6379> set k2 "val2" #事务指令2,设置k2的值为"val2"
QUEUED
127.0.0.1:6379> exec #提交事务
1) (error) ERR value is not an integer or out of range #事务指令1错误信息
2) OK
127.0.0.1:6379> get k2 #查询k2成功, k2是事务指令2设置的,即使指令1错误,它也执行了
"val2"
127.0.0.1:6379>

5.2 乐观锁

Redis 中乐观锁是使用指令 watch 声明,与事务组合使用,带乐观锁的事务,如果操作乐观锁 key 的事务指令失败,那么整个事务撤销。示范如下:

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> set lockKey 100		#设置lockKey的值为100
OK
127.0.0.1:6379> watch lockKey #对lockKey使用乐观锁,接下来的事务按乐观锁组合处理
OK
127.0.0.1:6379> multi #开启事务
OK
127.0.0.1:6379> incr lockKey #对lockKey的值自增1,若其他连接修改为任意值,又改回100,一定错误
QUEUED
127.0.0.1:6379> set key2 123 #指令2.. 如果乐观锁的指令错误,该指令也不生效
QUEUED
127.0.0.1:6379> exec

6 缓存问题

Redis 作为辅助的缓存数据库,很有可能出现缓存穿透,缓存击穿,缓存雪崩等问题。

6.1 缓存穿透

当一个请求进来,先到 Redis 中查询数据,如果 Redis 中没有,则到数据库中查询,如果突然大量请求查询不存在的数据,则会造成缓存穿透,造成数据库过大的压力。

常见的解决方式:

​ 一,校验不合理的查询参数和不符合规则的 key,能直接拦截就不再往 DB 查询;同时在 DB 层查出来为空的数据,可以做以 key-null 形式放到 Redis 中,即把空值放到 Redis,适当的设置过期时间,减轻数据库的压力。

​ 二、如果是恶意攻击,第一种方式将会造成大量空缓存,很有可能比 DB 实际存在的 key 都多,这种场景就可以使用布谷鸟过滤器结合 Redis,将 DB 实际存在的 key 放入 Redis,利用布谷鸟过滤器拦截,请求进来不存在的 key,布谷鸟过滤器在 redis 中查询不到,直接返回结果。

6.2 缓存击穿

大量请求查询同一个 key,持续查询,当这个 key 的缓存过期了,海量请求查询同一个 key 的数据,直接打到数据库,这种现象便是缓存击穿。比如一篇普通微博,突然越来越多的人关注,很短的时间内形成了巨大的查询请求,当这个缓存过期的时候,巨量请求直接打到数据库,对数据库层面来说压力不小。

常见解决方式:

​ 一,使用分布式锁,确保第一个到 DB 的请求,能将数据重新放回缓存。其它分布式服务和线程到达 DB 请求层面,等锁的同时,间断的查询缓存。等这一步骤完成后进来的请求都重新直接走缓存。

​ 二,定时任务监控热度,根据数据热度的增长,适当的增加缓存过期时间。

6.3 缓存雪崩

缓存雪崩与缓存击穿类似,区别在于,缓存雪崩是大量缓存数据同一时间过期或者 Redis 宕机,造成大量请求直接打到 DB,造成 DB 瞬间面量大量请求。

常见解决方式:

​ 一,Redis 采取主从+哨兵的高可用方式,或者 Redis Cluster 集群模式的高可用,来避免 Redis 宕机或崩溃的情况;同时开启 RDB、AOF 混合持久化模式,确保重启时能更快加载持久化数据。

​ 二,加锁或者阻塞队列排队处理,防止 DB 被打死,对用户来说体验很糟糕。

​ 三,限流,比如每一秒只处理 2000 个请求,超过数量的请求走限流逻辑排队逻辑。

​ 四,热点数据过期时间尽量分散设置,比如设置过期时间,随机增加几秒、几分钟;在重大活动前,也尽量能访问到的数据访问一遍,查漏补缺,缓存预热。