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)等,都可以指定使用懒删除的方式。
安装与开机启动
包下载完成后,解压文件
1 | #当前目录为/usr/local/ |
主要文件包括为:
文件 | 说明 |
---|---|
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 | tcp-keepalive 300 |
在根目录执行 redis-server
1 | su redis |
2 连接服务节点
1 | su redis |
3 自定义服务
注册为系统服务开机启动,方便管理
3.1 chkconfig 方式,
1 | # 新建服务配置文件 |
1 | #保存服务配置文件后,赋权 |
3.2 systemd 方式,CentOS7 及以上
1 | #新建运行脚本, |
1 | #保存systemd服务脚本文件后,赋权 |
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 | # 对127.0.0.1 6379这个节点,并发50执行10w个请求,只测试set和get |
响应结果:
1 | ====== SET ====== #写入测试 |
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 | 127.0.0.1:6379> set k1 "str" #设置 k1 值为 字符串str |
5.2 乐观锁
Redis 中乐观锁是使用指令 watch 声明,与事务组合使用,带乐观锁的事务,如果操作乐观锁 key 的事务指令失败,那么整个事务撤销。示范如下:
1 | 127.0.0.1:6379> set lockKey 100 #设置lockKey的值为100 |
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 个请求,超过数量的请求走限流逻辑排队逻辑。
四,热点数据过期时间尽量分散设置,比如设置过期时间,随机增加几秒、几分钟;在重大活动前,也尽量能访问到的数据访问一遍,查漏补缺,缓存预热。