Redis不是说用单线程的吗?怎么6.0成了多线程的?
Redis6.0的多线程是用多线程来处理数据的读写和协议解析,但是Redis执行命令还是单线程的。
持久化分为rdb和aof两种。
RDB持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化过程分为手动触发和自动触发。分别使用命令save或者bgsave。
同时rdb是一个二进制的压缩文件,
以下几个场景会自动触发rdb持久化
以独立日志的方式记录每次写命令, 重启时再重新执行AOF文件中的命令达到恢复数据的目的,整体工作过程为:
rdb优点:
rdb的缺点:
aof优点:
aof缺点:
AOF
持久化开启且存在AOF
文件时,优先加载AOF
文件。AOF
关闭或者AOF
文件不存在时,加载RDB
文件。AOF/RDB
文件成功后,Redis
启动成功。AOF/RDB
文件存在错误时,Redis
启动失败并打印错误信息。将 rdb 文件的内容和增量的 AOF 日志文件存在一起。这里的 AOF 日志不再是全量的日志,而是 自持久化开始到持久化结束 的这段时间发生的增量 AOF 日志,通常这部分 AOF 日志很小。
尽管我们将数据库中某些数据换到到内存中,但是若有些攻击者使用一些数据库中不存在的key进行恶意攻击,这时候,所有的查询请求就像穿透了缓存中间件一样直接在数据库中进行查询操作,在高并发场景,这样的攻击就会使得数据压力过大,从而导致数据库性能瓶颈。
(在initialBean阶段将数据缓存到内存中)
,每次查询之前,先使用布隆过滤器过滤掉一定不存在的无效请求,从而避免了无效请求给数据库带来的查询压力。(解决长时间内查询不到任何信息的情况)
,我们可以将空结果的缓存时间设置得短一些,例如 3~5 分钟,但是有可能导致数据一致性问题,所以我们建议查询或者更新的时候要对这个类型的缓存上个锁进行进一步的操作。和上述问题情况一样,也是缓存中查不到用户数据,大量请求打到数据库上,但是这种情况的发生原因却非恶意攻击者所为,原因大抵如下:
1. 大量用户查询的某个数据,刚刚好在缓存中过期。
2. 大量用户查询的值都在数据中,缓存中没有。
大量缓存数据同一时间到期,所有查询一下子都打到数据库上。导致数据库压力过大进而直接宕机。
// 缓存 key
String cacheKey = "userlist";
// 查询缓存
String data = jedis.get(cacheKey);
if (StringUtils.isNotBlank(data)) {
// 查询到数据,直接返回结果
return data;
} else {
// 先排队查询数据库,再放入缓存
synchronized (cacheKey) {
data = jedis.get(cacheKey);
if (!StringUtils.isNotBlank(data)) { // 双重判断
// 查询数据库
data = findUserInfo();
// 放入缓存
jedis.set(cacheKey, data);
}
return data;
}
}
// 缓存原本的失效时间
int exTime = 10 * 60;
// 随机数生成类
Random random = new Random();
// 缓存设置
jedis.setex(cacheKey, exTime + random.nextInt(1000) , value);
某些数据查询一次就被缓存在数据库中,随着时间推移,缓存空间已经满了,这时候redis
就要根据缓存策略进行缓存置换。这就造成没意义的数据需要通过缓存置换策略来淘汰数据,而且还可能出现淘汰热点数据的情况。
解决方案
选定合适的缓存置换策略,而redis
缓存策略主要分三类
不淘汰的
lfu-log-factor
和lfu-decay-time
来用户访问次数增加的频率。具体可以查看redis配置文件描述
MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select one from the following behaviors:
#
# volatile-lru -> Evict using approximated LRU, only keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key having an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
使用 keys 指令可以扫出指定模式的 key 列表。但是要注意 keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。
加锁主要是考虑多个客户端对相同业务方法进行修改操作,我们可以使用加锁的方式保证原子性,大致的方式为:
这期间你可能会遇到两个问题:
SET key value [EX seconds | PX milliseconds] [NX]
SET lock_key unique_value NX PX 10000
local current current = redis.call("incr",KEYS[1])
if tonumber(current) == 1
then redis.call("expire",KEYS[1],60)
end
什么是热Key? 所谓的热key,就是访问频率比较的key。
比如,热门新闻事件或商品,这类key通常有大流量的访问,对存储这类信息的 Redis来说,是不小的压力。
假如Redis集群部署,热key可能会造成整体流量的不均衡,个别节点出现OPS过大的情况,极端情况下热点key甚至会超过 Redis本身能够承受的OPS。
怎么处理热key?
热key处理 对热key的处理,最关键的是对热点key的监控,可以从这些端来监控热点key:
客户端 客户端其实是距离key“最近”的地方,因为Redis命令就是从客户端发出的,例如在客户端设置全局字典(key和调用次数),每次调用Redis命令时,使用这个字典进行记录。
代理端 像Twemproxy、Codis这些基于代理的Redis分布式架构,所有客户端的请求都是通过代理端完成的,可以在代理端进行收集统计。
Redis服务端 使用monitor命令统计热点key是很多开发和运维人员首先想到,monitor命令可以监控到Redis执行的所有命令。
只要监控到了热key,对热key的处理就简单了:
把热key打散到不同的服务器,降低压⼒
加⼊⼆级缓存,提前加载热key数据到内存中,如果redis宕机,⾛内存查询
所谓缓存预热,就是提前把数据库里的数据刷到缓存里,通常有这些方法:
开发的时候一般使用“缓存+过期时间”的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。
但是有两个问题如果同时出现,可能就会出现比较大的问题:
怎么处理呢?
要解决这个问题也不是很复杂,解决问题的要点在于:
减少重建缓存的次数。
数据尽可能一致。
较少的潜在危险。
所以一般采用如下方式:
互斥锁(mutex key) 这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
永远不过期 “永远不过期”包含两层意思:
从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期,注意数据更新后要实时加锁更新。
从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
通常Redis执行命令速度非常快,但是不合理地使用命令,可能会导致执行速度很慢,导致阻塞,对于高并发的场景,应该尽量避免在大对象上执行算法复杂 度超过O(n)的命令。
发现慢查询: slowlog get{n}
命令可以获取最近 的n条慢查询命令;
发现慢查询后,可以从两个方向去优化慢查询: 1)修改为低算法复杂度的命令,如hgetall改为hmget等,禁用keys、sort等命 令 2)调整大对象:缩减大对象数据或把大对象拆分为多个小对象,防止一次命令操作过多的数据。
单线程的Redis处理命令时只能使用一个CPU。而CPU饱和是指Redis单核CPU使用率跑到接近100%。
针对这种情况,处理步骤一般如下:
判断当前Redis并发量是否已经达到极限,可以使用统计命令redis-cli-h{ip}-p{port}--stat
获取当前 Redis使用情况
如果Redis的请求几万+,那么大概就是Redis的OPS已经到了极限,应该做集群化水品扩展来分摊OPS压力
如果只有几百几千,那么就得排查命令和内存的使用
对于开启了持久化功能的Redis节点,需要排查是否是持久化导致的阻塞。
fork阻塞 fork操作发生在RDB和AOF重写时,Redis主线程调用fork操作产生共享 内存的子进程,由子进程完成持久化文件重写工作。如果fork操作本身耗时过长,必然会导致主线程的阻塞。
AOF刷盘阻塞 当我们开启AOF持久化功能时,文件刷盘的方式一般采用每秒一次,后台线程每秒对AOF文件做fsync操作。当硬盘压力过大时,fsync操作需要等 待,直到写入完成。如果主线程发现距离上一次的fsync成功超过2秒,为了 数据安全性它会阻塞直到后台线程执行fsync操作完成。
HugePage写操作阻塞 对于开启Transparent HugePages的 操作系统,每次写命令引起的复制内存页单位由4K变为2MB,放大了512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。
Redis使用过程中,有时候会出现大key的情况, 比如:
单个简单的key存储的value很大,size超过10KB
hash, set,zset,list 中存储过多的元素(以万为单位)
大key会造成什么问题呢?
如何找到大key?
如何处理大key?
当Redis版本大于4.0时,可使用UNLINK命令安全地删除大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。
当Redis版本小于4.0时,避免使用阻塞式命令KEYS,而是建议通过SCAN命令执行增量迭代扫描key,然后判断进行删除。
当vaule是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
Master
最好不要做任何持久化工作,包括内存快照和 AOF
日志文件,特别是不要启用内存快照做持久化。Slave
开启 AOF
备份数据,策略为每秒同步一次。Slave
和 Master
最好在同一个局域网内。 尽量避免在压力较大的主库上增加从库。Master
调用 BGREWRITEAOF
重写 AOF
文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。Master
的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关为:Master<–Slave1<–Slave2<–Slave3…
,这样的结构也方便解决单点故障问题,实现 Slave
对 Master
的替换,也即,如果 Master
挂了,可以立马启用 Slave1
做 Master
,其他不变。Pipelining(管道)
Redis 管道是三者之中最简单的,当客户端需要执行多条 redis
命令时,可以通过管道一次性将要执行的多条命令发送给服务端,其作用是为了降低 RTT(Round Trip Time)
对性能的影响,比如我们使用 nc 命令将两条指令发送给 redis
服务端。
Redis 服务端接收到管道发送过来的多条命令后,会一直执命令,并将命令的执行结果进行缓存,直到最后一条命令执行完成,再所有命令的执行结果一次性返回给客户端 。 Pipelining示意图`
Pipelining的优势
在性能方面, Pipelining 有下面两个优势:
节省了RTT:将多条命令打包一次性发送给服务端,减少了客户端与服务端之间的网络调用次数
减少了上下文切换:当客户端/服务端需要从网络中读写数据时,都会产生一次系统调用,系统调用是非常耗时的操作,其中设计到程序由用户态切换到内核态,再从内核态切换回用户态的过程。当我们执行 10 条 redis 命令的时候,就会发生 10 次用户态到内核态的上下文切换,但如果我们使用 Pipeining 将多条命令打包成一条一次性发送给服务端,就只会产生一次上下文切换。
Redis是分布式锁本质上要实现的目标就是在 Redis 里面占一个“茅坑”,当别的进程也要来占时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。
V1:setnx命令
占坑一般是使用 setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用完了,再调用 del 指令释放茅坑。 setnx(set if not exists)
> setnx lock:fighter true
OK
... do something critical ...
> del lock:fighter
(integer) 1
但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。
V2:锁超时释放
所以在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证 5 秒之后锁会自动释放。 锁超时释放
> setnx lock:fighter true
OK
> expire lock:fighter 5
... do something critical ...
> del lock:fighter
(integer) 1
但是以上逻辑还有问题。如果在 setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是被人为杀掉的,就会导致 expire 得不到执行,也会造成死锁。
这种问题的根源就在于 setnx 和 expire 是两条指令而不是原子指令。如果这两条指令可以一起执行就不会出现问题。
V3:set指令
这个问题在Redis 2.8 版本中得到了解决,这个版本加入了 set 指令的扩展参数,使得 setnx 和expire 指令可以一起执行。 set原子指令
set lock:fighter3 true ex 5 nx OK ... do something critical ... > del lock:codehole
上面这个指令就是 setnx 和 expire 组合在一起的原子指令,这个就算是比较完善的分布式锁了。
当然实际的开发,没人会去自己写分布式锁的命令,因为有专业的轮子——Redisson。
(这种我们比较常用,我们的缓存基本都是配置数据,或者一些用户个人信息接口,数据基本不会有太大的变化)
Redis常见面试题总结(下)
Redis进阶 - 缓存问题:一致性, 穿击, 穿透, 雪崩, 污染等
Redis 缓存雪崩、缓存穿透、缓存击穿、缓存预热
【Redis】如何保证原子操作
面渣逆袭(Redis面试题八股文)必看