Redis 杂七杂八
基本概念
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)等,都可以指定使用懒删除的方式。
安装与开机启动
包下载完成后,解压文件
#当前目录为/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.7make PREFIX=/usr/local/redis install#添加运行用户groupadd redisuseradd redis -g redis -p 密码#授权文件给用户chown -R redis:redis /usr/local/redissu 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(详细配置参考)
tcp-keepalive 300timeout 0
daemonize yes
save 900 1save 300 10save 60 10000
appendonly yes
maxmemory-policy allkeys-lru
lazyfree-lazy-expire yeslazyfree-lazy-server-del yes
bind 0.0.0.0port 6379#启停脚本shell也需要pidpidfile /usr/local/redis/pid/node.pid
logfile "/usr/local/redis/log/node.log"
dir /usr/local/redis/data
dbfilename node.rdbappendfilename "node.aof" 在根目录执行 redis-server
su redis
# 按配置文件redis.conf启动一个单节点实例/usr/local/redis/bin/redis-server /usr/local/redis/redif.conf2 连接服务节点
su redis/usr/local/redis/bin/redis-cli -h 127.0.0.1 -p 6379....#连接成功后,如果需要密码,输入后即可执行其它命令auth 密码3 自定义服务
注册为系统服务开机启动,方便管理
3.1 chkconfig 方式,
# 新建服务配置文件vim /etc/init.d/redis
#编辑内容如下:
#!/bin/sh# chkconfig: 2345 10 90# start/stop/restart anything
#待添加到PATH环境变量的路径,如Java应用依赖的$JAVA_HOME/bin:$JRE_HOME/binADDPATH=/usr/local/redis/bin
#新 PATH生效PATH=$PATH:$ADDPATHexport PATHsource /etc/profile
#运行应用的用户EXEC_USER=redis
#程序运行文件EXEC_FILE=/usr/local/redis/bin/redis-server#运行时PID,停止服务时,先按运行文件查找pid进行kill,如果失败,则按此pid文件的进程号killEXEC_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 1esac#保存服务配置文件后,赋权chmod +x /etc/init.d/redis
#开机启动服务chkconfig --add redischkconfig redis on3.2 systemd 方式,CentOS7 及以上
#新建运行脚本,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=redisAfter=network.target
[Service]Type=forkingUser=redisGroup=redisExecStart=/usr/local/redis/mananger.sh startExecReload=/usr/local/redis/mananger.sh restartExecStop=/usr/local/redis/mananger.sh stopPrivateTmp=true
[Install]WantedBy=multi-user.target#保存systemd服务脚本文件后,赋权chmod +x /lib/systemd/system/redis.service
#绑定服务开机启动systemctl enable redis4 性能测试工具
跟目录下的 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 |
示例:
执行如下
# 对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响应结果:
====== 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: no0.00% <= 0.4 milliseconds0.00% <= 0.5 milliseconds0.38% <= 0.6 milliseconds4.93% <= 0.7 milliseconds14.52% <= 0.8 milliseconds16.92% <= 0.9 milliseconds19.59% <= 1.0 milliseconds # 1秒完成19.59%的请求写入31.63% <= 1.1 milliseconds51.21% <= 1.2 milliseconds69.20% <= 1.3 milliseconds77.85% <= 1.4 milliseconds81.71% <= 1.5 milliseconds84.54% <= 1.6 milliseconds90.08% <= 1.7 milliseconds95.66% <= 1.8 milliseconds98.64% <= 1.9 milliseconds99.51% <= 2 milliseconds100.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 milliseconds100.00% <= 2 milliseconds # 2秒完成100% 10万个请求28011.21 requests per second5 事务
传统数据库像 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 执行生效,如下所示:
127.0.0.1:6379> set k1 "str" #设置 k1 值为 字符串strOK127.0.0.1:6379> get k1 # 查询到 k1 值为 "str""str"127.0.0.1:6379> multi #开启事务OK127.0.0.1:6379> incr k1 #事务指令1, 对k1的值"str"进行自增1,一定错误QUEUED127.0.0.1:6379> set k2 "val2" #事务指令2,设置k2的值为"val2"QUEUED127.0.0.1:6379> exec #提交事务1) (error) ERR value is not an integer or out of range #事务指令1错误信息2) OK127.0.0.1:6379> get k2 #查询k2成功, k2是事务指令2设置的,即使指令1错误,它也执行了"val2"127.0.0.1:6379>5.2 乐观锁
Redis 中乐观锁是使用指令 watch 声明,与事务组合使用,带乐观锁的事务,如果操作乐观锁 key 的事务指令失败,那么整个事务撤销。示范如下:
127.0.0.1:6379> set lockKey 100 #设置lockKey的值为100OK127.0.0.1:6379> watch lockKey #对lockKey使用乐观锁,接下来的事务按乐观锁组合处理OK127.0.0.1:6379> multi #开启事务OK127.0.0.1:6379> incr lockKey #对lockKey的值自增1,若其他连接修改为任意值,又改回100,一定错误QUEUED127.0.0.1:6379> set key2 123 #指令2.. 如果乐观锁的指令错误,该指令也不生效QUEUED127.0.0.1:6379> exec6 缓存问题
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 个请求,超过数量的请求走限流逻辑排队逻辑。
四,热点数据过期时间尽量分散设置,比如设置过期时间,随机增加几秒、几分钟;在重大活动前,也尽量能访问到的数据访问一遍,查漏补缺,缓存预热。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!