存储系列——redis

154 阅读33分钟

Redis

www.pdai.tech/md/db/nosql…

Why

  1. 优点
  • 读写速度快
  • 数据类型丰富
  • 原子性
  • 发布/订阅
  • 持久化
  • 分布式:Redis cluster
  1. 应用场景
  • 热点数据的缓存

    • 读取:先从redis读取,没有再读sql,并写入redis,但是可能缓存击穿,
  • 限时业务

    • redis的键可以设置过期时间,可以运用在限时活动、手机验证码中等
  • 计数器

    • redis可以incre是原子的,可以用在秒杀、分布式序列号中
  • 分布式锁

    • setnx
  • 延时操作

    • 下单时过十分钟后看是不是成功
  • 排行榜

  • 共同好友

  • 简单队列

底层数据类型

数据类型(key-value)

基本类型:

  1. String

  • get、set、del、incr、decr、incrby、decrby
  • 缓存、计数器(单线程)、session
  1. 热点数据缓存。
  2. 计数:文章阅读量、点赞数会更新地很快,可以先缓存在Redis中,定时再同步到数据库中;也可以对访问者的ip进行计数,从而进行限流。
  3. 分布式场景下共享数据和锁:Redis为独立服务,可以保存共用的数据;setnx方法只有在key不存在时才能添加成功,可以用于实现分布式锁。
  1. List

  1. 消息时间线:例如朋友圈,微博等时序排列的场景,根据产生时间依次插入列表。
  2. 可以用来实现队列、栈、消息队列。
  1. Set

  • 标签、点赞、收藏
  1. 去重:例如点赞,签到,打卡等,要防止同一用户重复请求造成重复计数,redis可以很好地帮助使用者进行去重。
  2. 关系:用户关注关系,例如共同关注,我关注的人也关注了他,可能认识的人等
  1. Zset
  • 与set相比多了一个score

  • 排行榜
  • zset很适合做排行榜类的应用。例如某网站根据点击数来排序,可以将文章id作为value,点击数作为score维护在zset中。每次点击,score+1,这样可以对前10的文章不断刷新。而对于某篇文章,可以用zrank查看其当前的排名。
  1. Hash

  • 缓存
  • 具有关联关系的多个key进行整存整取

新增类型:

  1. hyperloglogs
  • 非常省内存的统计各种计数
  1. Bitmap
  • 0 1 针对只有两种状态的、非常省内存、比如统计365天的打卡情况
  1. Geospatial
  2. stream
  • 支持多播的可持久化的消息队列
  • 简单的mq,不能满足全部要求

过期与持久化

在创建key的时候可以指定键的过期时间,可以是在某一段时间后过期,也可以是在某个时刻过期。当键过期后,需要对相应的键值对进行删除以回收内存空间。

过期键的删除有三种方式:

  1. 定时删除:在给键设置过期时,创建一个定时器。在指定时刻完成删除动作。该方式要为每个键配一个进程来执行定时任务,比较重。
  2. 惰性删除:每次读取键的时候,都检查键是否过期。如果已经过期,则删除,并返回空。该方式比较轻,但对于实时性较强的数据,历史数据很少访问,会形成堆积。
  3. 定期批量删除:Redis自身每隔一段时间,会遍历数据库,将过期的k-v删除。该方式需要占用一定的CPU资源。

实际中采用的是被动惰性删除+主动定期批量删除相结合的方式,以平衡CPU资源占用和内存回收效率。

在持久化时,也对过期键进行了处理:

  1. 在执行SAVEBGSAVE命令生成RDB文件时,已经过期的键不会保存到RDB文件中。而RDB文件在载入时,会忽略掉过期键。
  2. 基于AOF文件持久化模式下,过期键被删除后,系统会在AOF文件中显式地追加一条DEL指令。因此,当AOF文件重写时,过期键的操作都会被抹掉。

基于快照的RDB持久化 RDB(Redis database)

按一定周期对Redis数据快照进行存储(生成dump.rdb文件),当内存数据丢失时,将快照重放到内存中。生成RDB文件的指令有同步阻塞式的SAVE指令,和异步的BGSAVE指令。服务器在载入RDB文件时,则处于阻塞状态,直到文件全部载入成功。

优点:

  1. 恢复速度快
  2. 节省磁盘空间(内存数据会进行压缩成快照文件存储)

缺点:

  1. 虽然RDB通过fork子线程进行快照存储时,并且采用了写时拷贝。但是当数据量比较大时,依然会比较耗时
  2. Redis宕机时,最后一次快照后的修改会丢失

基于追加操作日志的AOF持久化 AOF(append of file)

将每次操作追加到日志文件中,重启时Redis按照日志顺序执行一次以完成恢复工作。(生成appendonly.aof文件)

RDB文件和AOF文件同时存在时,系统优先取AOF文件进行恢复。aof文件恢复时,会逐步进行回放操作。

aof记录每次写操作,文件会越来越大。当aof文件的大小超过某个阈值时,会执行aof文件的重写,只保留可以恢复文件的最小指令集。例如,某处执行了flushdb命令,则其之前的写操作记录都可以删除。具体操作是,fork出一个子进程,先写一个临时文件,然后重命名为appendonly.aof并将原来的aof文件覆盖。

aof文件的重写是有开销的,因此只有满足条件才会执行重写:当前aof文件大小大于某个值,并且当前aof文件大小相比上次重写增加了一倍。

优点:

  1. 备份粒度更细,丢失数据更少(至多丢失最后一次写操作)
  2. aof文件对用户可读,通过操作aof文件,可以处理误操作(例如执行了flushdb,可以在aof文件中删除该次操作,重写后便可恢复数据)

缺点:

  1. 相比RDB占用更多磁盘空间
  2. 恢复速度慢
  3. 如果设置为每次写操作都马上同步,会有性能问题

实际上,RDB与AOF组合持久化的方式更佳,定期备份的RDB文件提升了恢复速度,减少AOF文件的大小,而AOF又能尽量减少修改丢失。

性能与实践

通过上面的分析,我们都知道RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。

  • 降低fork的频率,比如可以手动来触发RDB生成快照、与AOF重写;
  • 控制Redis最大使用内存,防止fork耗时过长;
  • 使用更牛逼的硬件;
  • 合理配置Linux的内存分配策略,避免因为物理内存不足导致fork失败。

在线上我们到底该怎么做?我提供一些自己的实践经验。

  • 如果Redis中的数据并不是特别敏感或者可以通过其它方式重写生成数据,可以关闭持久化,如果丢失数据可以通过其它途径补回;
  • 自己制定策略定期检查Redis的情况,然后可以手动触发备份、重写数据;
  • 单机如果部署多个实例,要防止多个机器同时运行持久化、重写操作,防止出现内存、CPU、IO资源竞争,让持久化变为串行;
  • 可以加入主从机器,利用一台从机器进行备份处理,其它机器正常响应客户端的命令;
  • RDB持久化与AOF持久化可以同时存在,配合使用。

四、复制与分片

1. 主从复制

Redis集群为了提高读写性能,增强灾备能力,分为主从模式,即一个master和多个slave。master用于接收写请求,slave用于接收读请求。每次写请求到master后,master会将写指令同步广播给到连接到自己的salve机器,从而构成了一个树状结构。

主从关系可以通过info replication指令获得。

复制过程根据起始状态,可以分为两种情况:新上的从库追赶主库,断联的从库重新上线后追赶主库。

新从库的复制

salve机器通过salveof ip port指令向指定的master注册。

注册成功后,master会向该salve机器发送rdb文件,rdb文件传输过程中,master会将自己在发送期间执行的写命令在缓冲区中记录,等rdb文件发送完毕后,会接着向从服务器发送存储在缓冲区中的写命令。

salve则丢弃所有此前的旧数据,载入master发来的rdb文件,载入完成后接收master发来的写命令。

当salve追赶上master后,master每执行一次写命令,就向slave发送相同的写命令。

断联后重新上线的老从库复制

slave可能因为网络的问题暂时与master断联,断联期间master执行的操作指令无法传播到slave。当slave重新上线时,需要追赶上这部分的差异。

追赶的方式是,master从上次复制的地方开始,将其后的变更发送给slave,即部分复制。相比全量复制,部分复制的psync更为精细,因而更节省资源,但是逻辑要更为复杂。为了实现部分复制,需要解决下面两个问题:

  1. slave重联成功后,怎么确定当前连的master就是之前的master?
  2. 怎么确定slave相对master落后了哪些内容?

第一个身份校验的问题通过run_id来解决。run_id是每个Redis服务器的唯一id,在启动时自动生成,由40个随机的十六进制字符组成。当slave向master注册时,master会将自己的run_id下发给slave,假设为run_id_m。slave断线重联后,会将此前保存的run_id_m发送给当前的master进行核验,如果一致,说明此前存在主从关系,是断线后重联,具备部分复制的前提。如果slave没有run_id_m或者提供的run_id_m与当前master不一致,说明此前不存在主从关系,只能采用全量复制。

第二个复制进度的问题通过复制偏移量来解决。master将接收到的写命令按序编号并保存,然后依次传播给salve。简化举例,假设总共3条指令,master和slave的初始偏移量分别为offset_moffset_s

  1. master向slave发送N条指令:offset_m + N
  2. slave接收到这N条指令后:offset_s + N
  3. 每次成功的复制后,offset_m = offset_s

一旦slave断联,offset_moffset_s将不一致。当slave重联成功后,master可以根据主从offset的差异,来判断应该从哪里开始进行复制。

master会存储每条历史写指令吗?No,那是AOF干的事情!

对于主从复制这个场景,master将命令(包含对应的偏移量)塞入一个固定长度的有序队列,只保存最新的一批指令。如果salve的offset_s对应的指令在队列中,则采取部分复制,将其后的所有命令都发送给salve。如果不在,说明slave断联太久,只能采取全量复制。

(来源:www.pdai.tech/md/db/nosql…

读写分离及其中的问题

在主从复制基础上实现的读写分离,可以实现Redis的读负载均衡:由主节点提供写服务,由一个或多个从节点提供读服务(多个从节点既可以提高数据冗余程度,也可以最大化读负载能力);在读负载较大的应用场景下,可以大大提高Redis服务器的并发量。下面介绍在使用Redis读写分离时,需要注意的问题。

  • 延迟与不一致问题

前面已经讲到,由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:优化主从节点之间的网络环境(如在同机房部署);监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;使用集群同时扩展写负载和读负载等。

在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。

  • 数据过期问题

在单机版Redis中,存在两种删除策略:

  • 惰性删除:服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。
  • 定期删除:服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。

在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。

Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。

  • 故障切换问题

在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的Redis节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写Redis数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。

  • 总结

在使用读写分离之前,可以考虑其他方法增加Redis的读负载能力:如尽量优化主节点(减少慢查询、减少持久化等其他情况带来的阻塞等)提高负载能力;使用Redis集群同时提高读负载能力和写负载能力等。如果使用读写分离,可以使用哨兵,使主从节点的故障切换尽可能自动化,并减少对应用程序的侵入

2. 主从切换

如果slave宕机,则其与master的从属关系会解除,重新上线时需要重新与master建立关系。

如果master宕机,salve会先进行等待,如果master又重新上线,则仍然保持master身份。当master长时间宕机时,可以执行slaveof no one指令,将某台slave升级为master,完成灾备切换。

手动切换难以做到高效,因此Redis引入哨兵模式。哨兵机器(可能不止一个)心跳监测master状态。当哨兵中的多数发现master宕机时,则会投票将某台slave升级为master,自动完成主从切换。哨兵选择新master的依据包括:各个slave预先配置的优先级(越小越好,配置为0表示永不选择为master)、各个slave的数据版本(越新越好)、salve的runid(前两项一样时选择runid小的)。当原宕机master重新上线时,哨兵会自动将其变为新master的slave。

但是当slave数量越来越多时,树的宽度太大,每次master都需要向所有slave同步写操作,会有性能问题。因此,slave也可以接受salve,通过增加树的高度来缩小宽度,降低master的同步压力。

3. 分片

当写入负载持续增加时,单台master的性能有限,需要进行分区。集群将key哈希到16384个槽(slot),并划分成多个区间到每个master节点。

例如集群中有3个master节点,则分别管理0-54605461-1092010921-16383对应slot的写入。假设设置某个key-value,key哈希后的值为5465,则该key由第二个master负责。集群中每个master不仅保存着自己所负责的slot信息,也保存着其他master负责的slot信息。每个master都可以接收写请求,当某个master发现写入的key不在自己的管理范围内,会根据本地的集群配置文件,自动重定向到对应的mster完成写入。

连接Redis集群使用Redis-cli-c命令

master节点通过一个长度为2048字节的二进制数组来记录slot信息。2048字节总共16384位,每一位对应一个槽,1表示该master负责这个槽位,0表示不负责。节点可以在O(1)时间内判定某个槽是否由自己负责。同时,每个master会维护一个长度为16384的数组,来记录某个slot分配给了哪个master节点。

当某个master节点接受到写请求时,首先通过二进制数组O(1)来判断是否由自己负责,如果不是自己负责,可以通过第二个数组O(1)判断该slot由哪个节点负责并返回给client,client重定向到该节点。

cluster addslot slot1...指令可以将指定的槽(slot1)指派给接收指令的节点,如在某节点上执行cluster addslot 1,2,则将槽1和槽2指派给当前节点。

当分区进行扩容时,原有的槽分布要重新划分,假设现在集群中只有一个master节点。现在增加一个master节点,则扩容后分别负责槽0-81918192-16383。扩容过程即将原本在老节点上槽8192-16383内保存的key-value,迁移到新加入的节点上,并刷新各个主节点记录的槽指派信息。迁移过程由Redis专门的集群管理软件redis-trib来控制,其过程如下:

迁移过程并不原子,例如slot(k)中有两对k-v:(k1,v1),(k2,v2)。(k1,v1)已经迁移到目标master,(k2,v2)仍处于源master。当客户端来查询k2时,能直接拿到结果。当客户端来查询k1时,源master返回一个ASK错误,并带上k1所在节点的ip+port,客户端会重新到目标master上查询。该过程对用户屏蔽,由客户端自动完成。

分区后的Redis集群实现了水平扩容,分担写压力。同时多主无中心的结构,配置相对简单。例如:集群的服务发现(如Zookeeper)都需要一个中心的服务,而Redis集群则是在各个节点维护一个集群配置文件,请求到任何节点,都可以自动路由到相应节点。这种无中心结构是raft算法的一个典型应用。

发布订阅

发送者 (pub) 发送消息,订阅者 (sub) 接收消息

SUBSCRIBE runoobChat

PUBLISH runoobChat "Redis PUBLISH test"

基于频道的发布订阅

基于模式的发布订阅

应用场景:

1、异步消息通知

需要同一套redis系统

2、任务通知

3、参数刷新加载

事务

Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。

总结说:redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。

Redis事务相关命令和使用

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。

  • MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。
  • EXEC:执行事务中的所有操作命令。
  • DISCARD:取消事务,放弃执行事务块中的所有命令。
  • WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。
  • UNWATCH:取消WATCH对所有key的监视

CAS操作实现乐观锁

被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。

  • watch 命令实现监视

在事务开始前用WATCH监控k1,之后修改k1为11,说明事务开始前k1值被改变,MULTI开始事务,修改k1值为12,k2为22,执行EXEC,发回nil,说明事务回滚;查看下k1、k2的值都没有被事务中的命令所改变。

Redis事务执行步骤

通过上文命令执行,很显然Redis事务执行是三个阶段:

  • 开启:以MULTI开始一个事务
  • 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
  • 执行:由EXEC命令触发事务

当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:

  • 如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个, 那么服务器立即执行这个命令。
  • 与此相反, 如果客户端发送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复。

如何理解Redis与事务的ACID?

一般来说,事务有四个性质称为ACID,分别是原子性,一致性,隔离性和持久性。这是基础,但是很多文章对Redis 是否支持ACID有一些异议,我觉的有必要梳理下:

  • 原子性atomicity

首先通过上文知道 运行期的错误是不会回滚的,很多文章由此说Redis事务违背原子性的;而官方文档认为是遵从原子性的。

Redis官方文档给的理解是,Redis的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。而不是完全成功。

  • 一致性consistency

redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结。

  • 隔离性Isolation

redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断。

但是,Redis不像其它结构化数据库有隔离级别这种设计。

  • 持久性Durability

redis 事务是不保证持久性的,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑。

缓存带来的问题

缓存穿透

  • 问题来源

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求。由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。

在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

  • 解决方案
  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击

返回空对象缺点:

如果有大量的key穿透,缓存空对象会占用宝贵的内存空间

  1. 布隆过滤器。bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,

布隆过滤器优点:

节省空间:不需要存储数据本身,只需要存储数据对应hash比特位

时间复杂度低:插入和查找的时间复杂度都为O(k),k为哈希函数的个数

布隆过滤器缺点:

存在假阳性:布隆过滤器判断存在,可能出现元素不在集合中。

不能删除元素:如果一个元素被删除,但是却不能从布隆过滤器中删除。

缓存击穿

  • 问题来源

缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。

  • 解决方案

1、设置热点数据永远不过期。

  • 物理不过期,针对热点key不设置过期时间
  • 逻辑过期,把过期时间存在key对应的value里,如果发现要过期了或者已经过期了,通过一个后台的异步线程进行缓存的构建

这种方法对于性能非常友好,唯一不足的就是构建缓存时候,其余线程(非构建缓存的线程)可能访问的是老数据,对于不追求严格强一致性的系统是可以接受的,但是在需要追求强一致性的系统下是无法使用的

2、接口限流与熔断,降级。重要的接口一定要做好限流策略,防止用户恶意刷接口,同时要降级准备,当接口中的某些 服务 不可用时候,进行熔断,失败快速返回机制。

3、加互斥锁

让一个线程回写缓存,其他线程等待回写缓存线程执行完,重新读缓存即可。

同一时间只有一个线程读数据库然后回写缓存,其他线程都处于阻塞状态。

如果是分布式应用,根据情况可以考虑使用分布式锁,

缓存雪崩

  • 问题来源

缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

  • 解决方案
  1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
  3. 设置热点数据永远不过期。

缓存污染(或满了)

缓存污染问题说的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间。

缓存污染会随着数据的持续增加而逐渐显露,随着服务的不断运行,缓存中会存在大量的永远不会再次被访问的数据。缓存空间是有限的,如果缓存空间满了,再往缓存里写数据时就会有额外开销,影响Redis性能。这部分额外开销主要是指写的时候判断淘汰策略,根据淘汰策略去选择要淘汰的数据,然后进行删除操作。

缓存预热

什么是缓存预热?

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统,这样就可以避免在用户请求的时候,先查询数据库,然后再将数据回写到缓存。

如果不进行预热, 那么 Redis 初始状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。

缓存预热方法:

  • 数据量不大的时候,工程启动的时候进行加载缓存动作;
  • 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
  • 数据量太大的时候,优先保证热点数据进行提前加载到缓存。

缓存降级

缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。

降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。

最大缓存设置多大

系统的设计选择是一个权衡的过程:大容量缓存是能带来性能加速的收益,但是成本也会更高,而小容量缓存不一定就起不到加速访问的效果。一般来说,我会建议把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销

不过,缓存被写满是不可避免的, 所以需要数据淘汰策略。

缓存淘汰策略

Redis共支持八种淘汰策略,分别是noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。

怎么理解呢?主要看分三类看:

  • 不淘汰

    • noeviction (v4.0后默认的)
  • 对设置了过期时间的数据中进行淘汰

    • 随机:volatile-random
    • ttl:volatile-ttl
    • lru:volatile-lru
    • lfu:volatile-lfu
  • 全部数据进行淘汰

    • 随机:allkeys-random
    • lru:allkeys-lru
    • lfu:allkeys-lfu

数据库和缓存一致性

  • 问题来源

使用redis做一个缓冲操作,让请求先访问到redis,而不是直接访问MySQL等数据库:

读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举一个例子:

1.如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。

2.如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。

因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。

4种相关模式

更新缓存的的Design Pattern有四种:Cache aside, Read through, Write through, Write behind caching; 我强烈建议你看看这篇,左耳朵耗子的文章:缓存更新的套路 (opens new window)

节选最最常用的Cache Aside Pattern, 总结来说就是

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先更新数据库,然后再删除缓存。

其具体逻辑如下:

  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:先把数据存到数据库中,成功后,再让缓存失效。

注意,我们的更新是先更新数据库,成功后,让缓存失效。那么,这种方式是否可以没有文章前面提到过的那个问题呢?我们可以脑补一下。

一个是查询操作,一个是更新操作的并发,首先,没有了删除cache数据的操作了,而是先更新了数据库中的数据,此时,缓存依然有效,所以,并发的查询操作拿的是没有更新的数据,但是,更新操作马上让缓存的失效了,后续的查询操作再把数据从数据库中拉出来。而不会像文章开头的那个逻辑产生的问题,后续的查询操作一直都在取老的数据。

这是标准的design pattern,包括Facebook的论文《Scaling Memcache at Facebook (opens new window)》也使用了这个策略。为什么不是写完数据库后更新缓存?你可以看一下Quora上的这个问答《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend? (opens new window)》,主要是怕两个并发的写操作导致脏数据。

那么,是不是Cache Aside这个就不会有并发问题了?不是的,比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。

但,这个case理论上会出现,不过,实际上出现的概率可能非常低,因为这个条件需要发生在读缓存时缓存失效,而且并发着有一个写操作。而实际上数据库的写操作会比读操作慢得多,而且还要锁表,而读操作必需在写操作前进入数据库操作,而又要晚于写操作更新缓存,所有的这些条件都具备的概率基本并不大。

所以,这也就是Quora上的那个答案里说的,要么通过2PC或是Paxos协议保证一致性,要么就是拼命的降低并发时脏数据的概率,而Facebook使用了这个降低概率的玩法,因为2PC太慢,而Paxos太复杂。当然,最好还是为缓存设置上过期时间。

方案:队列 + 重试机制

流程如下所示

  • 更新数据库数据;
  • 缓存因为种种问题删除失败
  • 将需要删除的key发送至消息队列
  • 自己消费消息,获得需要删除的key
  • 继续重试删除操作,直到成功

然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。

方案:异步更新缓存(基于订阅binlog的同步机制)

  1. 技术整体思路

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

1)读Redis:热数据基本都在Redis

2)写MySQL: 增删改都是操作MySQL

3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis

  1. Redis更新

1)数据操作主要分为两大块:

  • 一个是全量(将全部数据一次写入到redis)
  • 一个是增量(实时更新)

这里说的是增量,指的是mysql的update、insert、delate变更数据。

2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据

这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。

这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。

当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。

redis监控

什么样的场景会谈到redis监控体系?

一个大型系统引入了Redis作为缓存中间件,具体描述如下:

  • 部署架构采用Redis-Cluster模式;
  • 后台应用系统有几十个,应用实例数超过二百个;
  • 所有应用系统共用一套缓存集群;
  • 集群节点数几十个,加上容灾备用环境,节点数量翻倍;
  • 集群节点内存配置较高。

问题描述

系统刚开始关于Redis的一切都很正常,随着应用系统接入越来越多,应用系统子模块接入也越来越多,开始出现一些问题,应用系统有感知,集群服务端也有感知,如下描述:

  • 集群节点崩溃;
  • 集群节点假死;
  • 某些后端应用访问集群响应特别慢。

其实问题的根源都是架构运维层面的欠缺,对于Redis集群服务端的运行监控其实很好做,上文也介绍了很多直接的命令方式,但只能看到服务端的一些常用指标信息,无法深入分析,治标不治本,对于Redis的内部运行一无所知,特别是对于业务应用如何使用Redis集群一无所知:

  • Redis集群使用的热度问题?
  • 哪些应用占用的Redis内存资源多?
  • 哪些应用占用Redis访问数最高?
  • 哪些应用使用Redis类型不合理?
  • 应用系统模块使用Redis资源分布怎么样?
  • 应用使用Redis集群的热点问题?

构建Redis监控体系具备什么价值?

Redis监控告警的价值对每个角色都不同,重要的几个方面:

  • redis故障快速通知,定位故障点;
  • 分析redis故障的Root cause
  • redis容量规划和性能管理
  • redis硬件资源利用率和成本

redis故障快速发现,定位故障点和解决故障

当redis出现故障时,运维人员应在尽可能短时间内发现告警;如果故障对服务是有损的(如大面积网络故障或程序BUG),需立即通知SRE和RD启用故障预案(如切换机房或启用emergency switch)止损。

如果没完善监控告警; 假设由RD发现服务故障,再排查整体服务调用链去定位;甚于用户发现用问题,通过客服投诉,再排查到redis故障的问题;整个redis故障的发现、定位和解决时间被拉长,把一个原本的小故障被”无限”放大。

分析redis故障的Root cause

任何一个故障和性能问题,其根本“诱因”往往只有一个,称为这个故障的Root cause。

一个故障从DBA发现、止损、分析定位、解决和以后规避措施;最重要一环就是DBA通过各种问题表象,层层分析到Root cause;找到问题的根据原因,才能根治这类问题,避免再次发生。

完善的redis监控数据,是我们分析root cause的基础和证据。

问题表现是综合情的,一般可能性较复杂,这里举2个例子:

  • 服务调用Redis响应时间变大的性能总是;可能网络问题,redis慢查询,redis QPS增高达到性能瓶颈,redis fork阻塞和请求排队,redis使用swap, cpu达到饱和(单核idle过低),aof fsync阻塞,网络进出口资源饱和等等
  • redis使用内存突然增长,快达到maxmemory; 可能其个大键写入,键个数增长,某类键平均长度突增,fork COW, 客户端输入/输出缓冲区,lua程序占用等等

Root cause是要直观的监控数据和证据,而非有技术支撑的推理分析。

  • redis响应抖动,分析定位root casue是bgsave时fork导致阻塞200ms的例子。而不是分析推理:redis进程rss达30gb,响应抖动时应该有同步,fork子进程时,页表拷贝时要阻塞父进程,估计页表大小xx,再根据内存copy连续1m数据要xx 纳秒,分析出可能fork阻塞导致的。(要的不是这种分析)

Redis容量规划和性能管理

通过分析redis资源使用和性能指标的监控历史趋势数据;对集群进行合理扩容(Scale-out)、缩容(Scale-back);对性能瓶颈优化处理等。

Redis资源使用饱和度监控,设置合理阀值;

一些常用容量指标:redis内存使用比例,swap使用,cpu单核的饱和度等;当资源使用容量预警时,能及时扩容,避免因资源使用过载,导致故障。

另一方面,如果资源利用率持续过低,及时通知业务,并进行redis集群缩容处理,避免资源浪费。

进一步,容器化管理redis后,根据监控数据,系统能自动地弹性扩容和缩容。

Redis性能监控管理,及时发现性能瓶颈,进行优化或扩容,把问题扼杀在”萌芽期“,避免它”进化“成故障。

Redis硬件资源利用率和成本

从老板角度来看,最关心的是成本和资源利用率是否达标。

如果资源不达标,就得推进资源优化整合;提高硬件利用率,减少资源浪费。砍预算,减成本。

资源利用率是否达标的数据,都是通过监控系统采集的数据。

(来源:www.pdai.tech/md/db/nosql…

redis性能优化

从资源使用角度来看,包含的知识点如下:

  • CPU 相关:使用复杂度过高命令、数据的持久化,都与耗费过多的 CPU 资源有关
  • 内存相关:bigkey 内存的申请和释放、数据过期、数据淘汰、碎片整理、内存大页、内存写时复制都与内存息息相关
  • 磁盘相关:数据持久化、AOF 刷盘策略,也会受到磁盘的影响
  • 网络相关:短连接、实例流量过载、网络流量过载,也会降低 Redis 性能
  • 计算机系统:CPU 结构、内存分配,都属于最基础的计算机系统知识
  • 操作系统:写时复制、内存大页、Swap、CPU 绑定,都属于操作系统层面的知识

本地缓存与分布式缓存

  • 本地缓存:与应用进程同一进程,数据读写在同一个进程中进行
  1. 访问速度快,因为没有网络传输,性能较好。
  2. 只能被该应用进程访问,不能被其他应用进程访问,不能保证分布式一致性。
  3. 应用进程重启后数据会丢失,因此对于需要持久化的数据需要及时保存
  4. 不能进行大数据存储

更适合缓存只读数据。每个连接的数据都是独立的,如果需要保持多个部署节点一致,需要使用分布式缓存统一存储,不同部署节点的应用进程访问统一的分布式缓存进行数据读写。

一般是键值对存储。要保证线程安全。比如java的CocurrentHashMap和golang的sync.map

  • 分布式缓存:独立部署的进程,一般与应用进程部署在不同的机器,数据读写需要依靠网络传输。
  1. 可以支持大数据量的存储,部署应用进程重启影响。
  2. 数据集中存储,保证一致性。
  3. 分布式缓存一般都有数据副本,可以读写分离,保证高并发场景下的数据读写性能问题(高性能)。并且一般会在多个节点冗余存储数据,在某个节点宕机后保证高可用。
  4. 需要依靠网络传输数据,性能低于本地缓存。(一般,单个key的value大小受限)

典型实现:memCached,Redis

reference:

有的参考找不到了,发现请联系我。

ost.51cto.com/posts/1002