1 Redis部署方式
单机模式:这也是最基本的部署方式,只需要一台机器,负责读写,一般只用于开发人员自己测试。
哨兵模式:哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立运行的进程,其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。它具备自动故障转移、集群监控、消息通知等功能。
cluster集群模式:在redis3.0版本中支持了cluster集群部署的方式,这种集群部署的方式能自动将数据进行分片,每个master上放一部分数据,提供了内置的高可用服务,即使某个master挂了,服务还可以正常地提供。
1.1 分片机制
cluster集群模式,会将数据自动分片。
将不同的 key 分散放置到不同的 redis 节点,通常的做法是获取 key 的哈希值,然后根据节点数来求模,分发到不同节点,但这种做法有其明显的弊端,当我们需要增加或减少一个节点时,会造成大量的 key 无法命中,这种比例是相当高的,所以就有人提出了一致性哈希的概念。
一致性哈希有四个重要特征:
- 均衡性:平衡性,是指哈希的结果能够尽可能分布到所有的节点中去,这样可以有效的利用每个节点上的资源。
- 单调性:当节点数量变化时哈希的结果应尽可能的保护已分配的内容不会被重新分派到新的节点。
- 分散性和负载:这两个其实是差不多的意思,就是要求一致性哈希算法对 key 哈希应尽可能的避免重复。
Redis 集群没有使用一致性hash,而是引入了哈希槽的概念。
Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,每个key通过CRC16校验后对16384取模来决定放置哪个槽(Slot),每一个节点负责维护一部分槽以及槽所映射的键值数据。
计算公式:slot = CRC16(key) & 16383。
这种结构很容易添加或者删除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。使用哈希槽的好处就在于可以方便的添加或移除节点。
- 当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;
- 当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了。
2 Redis 主从同步原理
主从复制:在主从复制这种集群部署模式中,我们会将数据库分为两类,第一种称为主数据库(master),另一种称为从数据库(slave)。主数据库会负责我们整个系统中的读写操作,从数据库会负责我们整个数据库中的读操作。其中在职场开发中的真实情况是,我们会让主数据库只负责写操作,让从数据库只负责读操作,就是为了读写分离,减轻服务器的压力。
1、slave向master发起同步请求,slave会告知master自己的offset,如果是第一次,offset=-1表示需要全量同步;
2、slave连上了master,master会给slave分配一个client buffer(复制缓冲区);
3、master fork子进程dump RDB文件,dump完成后告诉父进程,父进程把RDB发给slave(通过slave的socket发过去);
4、slave载入rdb到本地磁盘;
5、在master dump RDB期间,master所有写命令,都写到这个client buffer然后同步给slave
6、slave执行同步过来的buffer命令。
在全量同步时,同步的是RDB,该过程较耗时;在增量同步时,同步的是client buffer(复制积压缓冲区,也叫replication buffer)数据,该过程比较高效。
2.1 数据延迟和不一致问题
由于主从复制的命令传播是异步的,延迟与数据的不一致是不可避免的,只能尽可能减少发生的频率。
解决优化方案就是:
- 集群尽量用物理机部署在同一个局域网中,减少网络异常的频率;
- 控制redis实例的大小(建议不要超过6G);
- 主从节点服务内存一致且配置参数一致,尽量减少由于内存不一致而触发了内存淘汰机制,导致主从数据不一致。
2.2 故障切换问题:
redis主从模式并不支持高可用,当master宕机后,整个集群就瘫痪了,需要人工干预恢复数据,为了解决这个问题于是就有了高可用的集群模式–哨兵集群。
2.3 数据过期问题
redis过期策略是:惰性删除+定期删除。
定期删除:redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。
惰性删除:在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
主从导致问题:key在master上过期了,并且也同步给salve上了,由于做了读写分离在salve读取到了这个key,以上场景在redis3.2之前salve会读到已过期key的数据。
问题解决方案:在redis3.2之后客户端也会判断key是否过期,如果过期则返回客户端null值,待master节点淘汰该key之后同步给salve才会删除这个key,以这种方式解决了读写分离下数据过期问题。
3 内存淘汰策略
如果定期删除没删除key。然后你也没即时去请求key,也就是说惰性删除也没生效。这样,redis的内存会越来越高。那么就应该采用内存淘汰机制。
在redis.conf中有一行配置:
# maxmemory-policy noeviction
1)noeviction:当内存不足以容纳新写入数据时,新写入操作会报错。
2)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用。
3)allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key。
4)volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key。这种情况一般是把redis既当缓存,又做持久化存储的时候才用。
5)volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key。
6)volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
ps:如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和 volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
4 redis数据持久化
4.1.AOF持久化
AOF是执行完命令后才记录日志的。为什么不先记录日志再执行命令呢?这是因为Redis在向AOF记录日志时,不会先对这些命令进行语法检查,如果先记录日志再执行命令,日志中可能记录了错误的命令,Redis使用日志回复数据时,可能会出错。
正是因为执行完命令后才记录日志,所以不会阻塞当前的写操作。但是会存在两个风险:
- 更执行完命令还没记录日志时,宕机了会导致数据丢失。
- AOF不会阻塞当前命令,但是可能会阻塞下一个操作。
这两个风险最好的解决方案是折中妙用AOF机制的三种写回策略 appendfsync:
- always,同步写回,每个子命令执行完,都立即将日志写回磁盘。
- everysec,每个命令执行完,只是先把日志写到AOF内存缓冲区,每隔一秒同步到磁盘。
- no:只是先把日志写到AOF内存缓冲区,有操作系统去决定何时写入磁盘。
问:AOF重写会阻塞嘛?
答:不会,AOF日志是由主线程去写的,而重写则不一样,重写过程是由后台子进程bgrewriteaof完成。
AOF的优点:数据的一致性和完整性更高,秒级数据丢失。
AOF的缺点:相同的数据集,AOF文件体积大于RDB文件。数据恢复也比较慢。
4.2.RDB持久化
RDB,就是把内存数据以快照的形式保存到磁盘上。和AOF相比,它记录的是某一时刻的数据,,并不是操作。
5 Redis缓存穿透、击穿、雪崩
5.1 缓存穿透
描述:
- 指访问一个缓存和数据库中都不存在的key,由于这个key在缓存中不存在,则会到数据库中查询,数据库中也不存在该key,无法将数据添加到缓存中,所以每次都会访问数据库导致数据库压力增大。
解决方法:
- 将空key添加到缓存中。
- 使用布隆过滤器过滤空key。
- 一般对于这种访问可能由于遭到攻击引起,可以对请求进行身份鉴权、数据合法行校验等。
5.2 缓存击穿
描述:
- 指大量请求访问缓存中的一个key时,该key过期了,导致这些请求都去直接访问数据库,短时间大量的请求可能会将数据库击垮。
解决方法:
- 添加互斥锁或分布式锁,让一个线程去访问数据库,将数据添加到缓存中后,其他线程直接从缓存中获取。
- 热点数据key不过期,定时更新缓存,但如果更新出问题会导致缓存中的数据一直为旧数据。
5.3 缓存雪崩
描述:
- 指在系统运行过程中,缓存服务宕机或大量的key值同时过期,导致所有请求都直接访问数据库导致数据库压力增大。
解决方法:
- 将key的过期时间打散,避免大量key同时过期。
- 对缓存服务做高可用处理。
- 加互斥锁,同一key值只允许一个线程去访问数据库,其余线程等待写入后直接从缓存中获取。
6 Redis调优
- 设置最大物理内存:Redis在占用maxmemory大小的内存之后就开始拒绝后续的写入请求,该参数可以确保Redis因为使用了大量内存严重影响速度或者发生OOM(out-of-memory,发现内存不足时,它会选择杀死一些进程(用户态进程,不是内核线程),以便释放内存)。
- 键名简短:越长存储的内存越大。
- 设置请求超时时间:防止无用的连接占用资源。
- 数据持久化策略:对于存储到磁盘中的快照,可以设置关掉压缩存储和CRC64数据校验。
- 优化AOF和RDB:主库可以不进行dump操作或者降低dump频率。 取消AOF持久化。
- 限制客户端连接数。
7 热key问题解决方案
7.1 热Key导致的问题
相同的key会hash到同一个slot中,也即同一个分片中,导致该redis server压力过大,可能导致集群崩溃。热点key的value 也比较大,也会造成网卡达到瓶颈。
7.2 热key探测
- 集群中每个slot的qps监控,粒度过于粗了,仅适用于前期集群监控方案,并不适用于精准探测到热key的场景。
- proxy的代理机制作为整个流量入口统计。这种方式需要至少有proxy的代理机制,对于redis架构有要求。
- redis基于LFU的热点key发现机制。redis 4.0以上的版本支持了每个节点上的基于LFU的热点key发现机制,执行redis-cli时加上–hotkeys选项。可以定时在节点中使用该命令来发现对应热点key。
redis-cli –hotkeys
- 基于Redis客户端做探测。每个client做基于时间滑动窗口的统计,超过一定的阈值之后上报至server,然后统一由server下发至各个client,并且配置对应的过期时间。
7.3 热key解决
- 对特定key或slot做限流。
- 使用二级(本地)缓存。会导致数据不一致的问题。
- 拆key。将key拆成N份,比如一个key名字叫做"good_100",那我们就可以把它拆成四份,“good_100_copy1”、“good_100_copy2”、“good_100_copy3”、“good_100_copy4”,每次更新和新增时都需要去改动这N个key,这一步就是拆key。
- 本地缓存的另外一种思路配置中心。长轮询+本地化的配置。首先服务启动时会初始化全部的配置,然后定时启动长轮询去查询当前服务监听的配置有没有变更,如果有变更,长轮询的请求便会立刻返回,更新本地配置;如果没有变更,对于所有的业务代码都是使用本地的内存缓存配置。这样就能保证分布式的缓存配置时效性与一致性。
8 数据库与缓存一致性方案
如何导致不一致问题:
当出现并发数据变更时,如A、B对同一行数据进行变更,A先变更完成,接着B变更完成,B更新缓存,A更新缓存。数据库中的数据是B的数据,但是缓存中却是A的数据。
如何解决:
- 分布式事务:A先开启事物获取数据库锁,B堵塞等待,A更新缓存之后,提交事物。B才能获取数据库锁。
- 基于binlog,与业务解耦,定时任务监听binlog改变从而更新缓存。
9 redis多线程
redis4.0时引入了多线程,额外的线程只是用于后台处理,例如:删除对象,核心流程还是单线程的。
redis6.0时再次引入多线程,多线程主要用于网络 I/O 阶段,也就是接收命令和写回结果阶段,而在执行命令阶段还是单线程的。
9.1 redis6.0多线程处理命令核心流程
- 当有读事件到来时,主线程将该客户端连接放到全局等待读队列;
- 读取数据:
1)主线程将等待读队列的客户端连接通过轮询调度算法分配给 I/O 线程处理;
2)同时主线程也会自己负责处理一个客户端连接的读事件;
3)当主线程处理完该连接读事件后,会自旋等待所有 I/O 线程处理完毕; - 命令执行:主线程按照事件被加入全局等待队列的顺序(保证执行顺序是正确的),串行执行客户端命令,然后将客户端连接放到全局等待写队列。
- 写回结果:
1)主线程将等待写队列的客户端连接通过轮询调度算法分配给 I/O 线程处理;
2)同时主线程也会自己负责处理一个客户端连接的写事件;
3)当主线程处理完该连接写事件后,会自旋等待所有 I/O 线程处理完毕;
10 redis 为什么快
- 基于内存操作。
- 使用I/O多路复用,select,epoll 等,基于reactor 模式开发了自己的网络时间处理器。
- 单线程可以避免不必要的上下文切换和竞争条件,减少这方面的性能损耗。