Redis性能调优及缓存常见问题
Redis性能调优
禁用长耗时的查询命令
-
不知道大家踩过这个坑没有,在生产环境中,用keys * 去查看Redis里面的所有数据
-
然后Redis就卡死了,卡了很长时间,期间不接受任何操作命令
-
-
Redis只用一个线程来做数据查询,如果某个查询指令耗时太长,就会阻塞Redis的其他查询操作,
-
想要避免耗时的操作指令,可以从以下几个方面入手改造
-
禁止使用keys命令,避免一次查询所有成员
-
可以使用scan命令,分批、游标式的遍历
-
比如我们要查询Redis中以key1开头的所有键(目前Redis中含有key1 ~ key20)
-
-
scan cursor [match pattern] [count]
-
cursor :当前游标位置,为0表示第一次
-
match :匹配 key1*
-
count 5:一次只遍历五个数据,这五个数据中如果有符合要求的,则返回
-
-
然后我们使用该命令每次都返回了一个数字,比如第一次使用时返回的26,第二次返回的9
-
这是当前遍历返回的游标
-
如果你还想遍历,就以此游标作为开始,继续遍历
-
直到返回0,,第一轮遍历结束,所有结果全部查询出来完毕
-
-
通过机制严格控制hash、set zset等结构的数据大小
-
因为这些数据的底层实现是使用了多种编码的
-
在数据越加庞大的时候会转换,复杂度就会蹭蹭蹭的往上涨
将不必要的业务脱离出Redis
-
比如Redis提供了排序,交集、并集等等功能实现
-
但是我们可以不用Redis来实现这些功能,而是客户端拿到数据,自己处理想要的业务
删除大体积数据,推荐使用unlink
-
删除一个体积较大的数据的时候,可能会需要很长的时间,这个时候建议使用异步的方式删除:unlink
-
Rdis主线程会启动一个新的线程来删除目标数据,主线程继续对外提供服务
-
这里就说到了Redis的多线程,也顺带说明依稀a
Redis的多线程(6.0)
为什么之前的Redis一直使用的是单线程?
-
在上图中是Redis的顶层设计架构
-
就涉及到一个知识:IO多路复用
-
FD是一个文件描述符,意思是表示当前文件处于可读、可写还是异常状态
-
使用 I/O 多路复用机制同时监听多个文件描述符的可读和可写状态 ,可以理解为具有了多线程的特点
-
一旦受到网络请求就会在内存中快速处理,由于绝大多数的操作都是纯内存的,所以处理的速度会非常地快
-
单线程模式下,即使连接的网络处理很多,因为有IO多路复用,依然可以在高速的内存处理中得到忽略
-
-
可维护性高
-
多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题。
-
单线程模式下,可以方便地进行调试和测试
-
-
基于内存,单线程状态下效率依然高
-
多线程能够充分利用CPU的资源 ,但是redis是使用内存作为介质,可接受10W/s的并发
-
Redis还有分片技术,交给不同的Redis服务器,相当于也是多线程效果了
-
-
总结
-
基于内存而且使用多路复用技术,单线程速度很快,又保证了多线程的特点。因为没有必要使用多线程
-
为什么又在6.0之后引入了多线程呢?
-
因为读写网络的read/write系统调用在Redis执行期间占用了大部分CPU时间
-
如果把网络读写做成多线程的方式会对性能会有很大提升
-
Redis可以使用del命令删除一个元素,如果这个元素占据了几十兆或几百兆,那么在短时间内是不能完成的
-
这样一来就需要多线程的异步支持,提升性能【多线程删除工作可以在后台 】
-
总结
-
Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈
-
所以使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本
-
系统的性能瓶颈也主要在网络 I/O 操作上
-
而 Redis 引入多线程操作也是出于性能上的考虑
-
对于一些大键值对的删除操作
-
通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率
使用slowlog优化耗时命令
-
这个和MySQL设计一致,都有慢查询日志记录
-
我们可以通过使用 slowlog get n 的方式来获取相关的慢查询日志
-
然后找到该操作命令,对其进行优化
-
这个slowlog在配置文件中有两个配置项
-
-
slowlog-log-slower-than :
-
用于设置慢查询的评定时间 ,超过该时间就会被记录在慢查询日志中,单位微妙(1 秒等于 1000000 微秒 )
-
-
slowlog-max-len
-
配置慢查询日志的最大记录数,生产环境一般配置为1024
-
-
避免大量数据同时失效
-
Redis 过期键值删除使用的是贪心策略
-
它每秒会进行 10 次过期扫描,此配置可在 redis.conf 进行配置,默认值是 hz 10
-
-
Redis 会从设置过期字典中随机抽取 20 个值,删除这 20 个键中过期的键
-
如果过期 key 的比例超过 25% ,重复执行此流程
-
如果在大型系统中有大量缓存在同一时间同时过期
-
那么会导致 Redis 循环多次持续扫描删除过期字典
-
直到过期字典中过期键值被删除的比较稀疏为止
-
而在整个执行过程会导致 Redis 的读写出现明显的卡顿
-
卡顿的另一种原因是内存管理器需要频繁回收内存页,因此也会消耗一定的 CPU
-
-
为了避免这种卡顿现象的产生,我们需要预防大量的缓存在同一时刻一起过期
-
就简单的解决方案就是在过期时间的基础上添加一个指定范围的随机数
使用pipeline批量操作数据
-
Pipeline (管道技术) 是客户端提供的一种批处理技术
-
可以批量执行一组指令,一次性返回全部结果 ,可以减少频繁的请求应答
客户端使用优化
-
客户端除了使用上面说到的pipeline技术
-
还需要注意要尽量使用 Redis 连接池 (连接池的作用就不用我赘述了吧)
使用分布式架构来增加读写速度
-
Redis 分布式架构有重要的手段:
-
主从同步 (读写分离)
-
把写入放到主库上执行,把读功能转移到从服务上,因此就可以在单位时间内处理更多的请求
-
-
哨兵模式
-
Redis Cluster 集群 (最佳方案)
-
Redis 集群是通过将数据库分散存储到多个节点上来平衡各个节点的负载压力
-
Redis Cluster 应该是首选的实现方案,它可以把读写压力自动的分担给更多的服务器,并且拥有自动 容灾的能力
-
-
使用物理机而非虚拟机
-
Linux kernel 在 2.6.38 内核增加了 Transparent Huge Pages (THP) 特性 ,支持大内存页 2MB 分配,默认开启
-
当开启了 THP 时,fork 的速度会变慢,fork 之后每个内存页从原来 4KB 变为 2MB ,会大幅增加重写期间父进程内存消耗
-
同时每次写命令引起的复制内存页单位放大了 512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询
-
例如简单的 incr 命令也会出现在慢查询中,因此 Redis 建议将此特性进行禁用,禁用方法如下
-
echo never > /sys/kernel/mm/transparent_hugepage/enabled
-
-
为了使机器重启后 THP 配置依然生效,可以在 /etc/rc.local 中追加 echo never >
-
/sys/kernel/mm/transparent_hugepage/enabled
-
缓存常见问题
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的期间,也会给后端系统数据库带来很大压力。
-
在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量
-
比如对某个key只允许一个线程查询数据和写缓存,其他线程等待 (双重检测锁)
-
-
不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀
-
做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期
缓存击穿
某个key在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来
这些请求发现缓存过期,一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮
-
使用redis的setnx互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大并发操作去操作数据库
缓存穿透
对不存在的key进行高并发访问,导致数据库压力瞬间增大,这就叫做【缓存穿透】
-
对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存
-
对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤
缓存预热
直接写个缓存刷新页面,上线前手工操作一下
数据量不大的时候,可以在项目启动的时候自动加载
定时刷新缓存
缓存更新
-
自定义的缓存淘汰策略
-
定期去清理过期的缓存
-
当有用户请求过来时,先判断这个请求用到的缓存是否过期,过期的话就去底层系统得到新数据进行缓存更新
-
缓存双写一致性
一般来说,在读取缓存方面,我们都是先读取缓存,再读取数据库的
但在更新缓存方面,我们到底应该先更新谁(Redi还是MySQL)?
-
先更新数据库再更新缓存(不建议使用【脏读】)
操作步骤(线程A和线程B都对同一数据进行更新操作):
线程A更新了数据库
线程B更新了数据库
线程B更新了缓存
线程A更新了缓存
此时缓存中的数据并不是最新的,最新的应该是B更新的数据
-
先更新数据库再删除缓存
操作步骤(线程A更新、线程B读)
请求A进行写操作,删除缓存,此时A的写操作还没有执行完
请求B查询发现缓存不存在
请求B去数据库查询得到旧值
请求B将旧值写入缓存
请求A将新值写入数据库
此时缓存中的数据也不是最新的,最新的应该是请求A更新的数据
延时双删策略,伪代码如下
public void write( String key, Object data){ db.updateData(data); redis.delKey(key); Thread.sleep(1000); redis.deleKey(key); }
-
先删除缓存在更新数据库
操作步骤
用户A删除缓存失败
用户A成功更新了数据
或者
用户A删除了缓存
用户B读取缓存,缓存不存在
用户B从数据库拿到旧数据
用户B更新了缓存
用户A更新了数据
问题:脏数据
解决方案一:
设置缓存有效时间(最简单)
解决方案二:
使用消息队列 :通过数据库的binlog来异步淘汰key,利用工具(canal)将binlog日志采集发送到MQ中,然后通过ACK机制确认处理删除缓存
解决方案三:
要求“缓存+数据库”必须保持一致,读请求和写请求进行串行化,串到一个内存队列中 最好不要做这种方案