Redis知识点学习

Redis 简介

Redis本质上是一个Key-Value类型的内存数据库,整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。

因为是纯内存操作,Redis的性能非常出色,每秒可以处理超过 10万次读写操作,是已知性能最快的Key-Value DB。可用于缓存、事件发布或订阅、高速队列等场景。

该数据库使用ANSI C语言编写,支持网络,提供字符串、哈希、列表、队列、集合结构直接存取,基于内存,可持久化。Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。

Redis的通讯协议

Redis 的通讯协议是文本协议,Redis服务器与客户端通过 RESP(Redis Serialization Protocol)协议通信。

文本协议虽然会浪费流量,但它的优点在于直观,非常的简单,解析性能极其的好,我们不需要一个特殊的 Redis 客户端仅靠 Telnet 或者是文本流就可以跟 Redis 进行通讯。

客户端的命令格式:

  • 简单字符串 Simple Strings,以 “+”加号开头。

  • 错误 Errors,以”-“减号开头。

  • 整数型 Integer,以 “:” 冒号开头。

  • 大字符串类型 Bulk Strings,以 “$”美元符号开头。

  • 数组类型 Arrays,以 “*“星号开头。

Redis文档 认为简单的实现,快速的解析,直观理解是采用 RESP 文本协议最重要的地方,有可能文本协议会造成一定量的流量浪费,但却在性能上和操作上快速简单,这中间也是一个权衡和协调的过程。

Redis的应用场景有哪些

1、会话缓存(最常用)
2、消息队列,比如支付
3、活动排行榜或计数
4、发布、订阅消息(消息通知)
5、商品列表、评论列表等

为什么要用 Redis 而不用 map/guava 做缓存

缓存分为本地缓存和分布式缓存。

以java为例,使用自带的map或者guava实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。

使用 Redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 Redis 或 memcached服务的高可用,整个程序架构上较为复杂。

Redis 和 memcached 的区别

  1. Redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcached支持简单的数据类型,String。
  2. Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecached把数据全部存在内存之中。
  3. 集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是Redis目前是原生支持cluster模式的,Redis官方就是支持Redis cluster集群模式的,比memcached来说要更好。
  4. Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的(避免了频繁的上下文切换)多路 IO 复用模型。

Redis占用内存大小

Redis是基于内存的key-value数据库,因为系统的内存大小有限,所以在使用Redis的时候可以配置Redis能使用的最大的内存大小。

通过配置文件配置

通过启动Redis服务的指定的Redis配置文件(默认是Redis安装目录下的Redis.conf文件)修改。

1
2
//设置Redis最大占用内存大小为100M
maxmemory 100mb

通过命令修改

Redis支持运行时通过命令动态修改内存大小

1
2
3
4
5
//设置Redis最大占用内存大小为100M
127.0.0.1:6379> config set maxmemory 100mb

//获取设置的Redis能使用的最大内存大小
127.0.0.1:6379> config get maxmemory

如果不设置最大内存大小或者设置最大内存大小为0,在64位操作系统下不限制内存大小,在32位操作系统下最多使用3GB内存。

Redis 删除过期数据

定期删除:Redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 Redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!

惰性删除:定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被Redis给删除掉。这就是所谓的惰性删除,也是够懒的哈!

Redis 内存淘汰机制

Redis 提供 8 种数据淘汰策略:

  1. no-enviction:默认的策略,禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错(DEL请求和部分特殊请求除外)。
  2. allkeys-lru:当内存不足以容纳新写入数据时,在键空间(server.db[i].dict)中,移除最近最少使用的 key(这个是最常用的)。
  3. allkeys-random:当内存不足以容纳新写入数据时,在键空间(server.db[i].dict)中,随机移除某个 key(这个一般没人用,为啥要随机,肯定是把最近最少使用的 key 给淘汰)。
  4. volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,移除最近最少使用的 key(这个一般不太合适)。
  5. volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,随机移除某个 key。
  6. volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间(server.db[i].expires)中,有更早过期时间的 key 优先移除。
  7. volatile-lfu:在设置了过期时间的key中使用LFU算法淘汰key(Redis4.0新增)。
  8. allkeys-lfu:在所有key中使用LFU算法淘汰淘汰和数据(Redis4.0新增)。

Redis淘汰数据时还会同步到aof。

如何获取及设置内存套餐策略

🍨 获取当前内存淘汰策略

1
127.0.0.1:6379> config get maxmemory-policy

🎃 通过配置文件设置内存淘汰策略

1
maxmemory-policy allkeys-lru

🍰 通过命令修改淘汰策略

1
127.0.0.1:6379> config set maxmemory-policy allkeys-lru

Redis 持久化机制

Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)。

快照(snapshotting)持久化(RDB)

Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。

Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用。

Redis会单独创建fork()一个子进程,将当前父进程的数据库数据复制到子进程的内存中,然后由子进程写入到临时文件中,持久化的过程结束了,再用这个临时文件替换上次的快照文件,然后子进程退出,内存释放。

需要注意的是,每次快照持久化都会将主进程的数据库数据复制一遍,导致内存开销加倍,若此时内存不足,则会阻塞服务器运行,直到复制结束释放内存;
都会将内存数据完整写入磁盘一次,所以如果数据量大的话,而且写操作频繁,必然会引起大量的磁盘I/O操作,严重影响性能,并且最后一次持久化后的数据可能会丢失;

快照持久化是 Redis 默认采用的持久化方式,在 Redis.conf 配置文件中默认有此下配置:

1
2
3
4
5
save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。 

save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。

优点:

  1. 只有一个文件dump.rdb,方便持久化。
  2. 容灾性好,一个文件可以保存到安全的磁盘。
  3. 性能最大化,fork子进程来完成写操作,让主进程继续处理命令,所以是IO最大化。
  4. 相对于数据集大时,比AOF的启动效率更高。

缺点:

  1. 数据安全性低,通过配置save参数来达到定时的写快照,比如 每900 秒有1个键被修改就进行一次快照,每300秒至少有10个键被修改进行快照,每60秒有至少10000个键被修改进行记录。所以如果当服务器还在等待写快照时出现了宕机,那么将会丢失数据。
  2. fork子进程时可能导致服务器停机1秒,数据集太大。

AOF(append-only file)持久化

与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案。

以日志的形式记录每个写操作(读操作不记录),只需追加文件但不可以改写文件,Redis启动时会根据日志从头到尾全部执行一遍以完成数据的恢复工作。包括flushDB也会执行。

默认情况下 Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:

1
appendonly yes

开启 AOF 持久化后每执行一条会更改 Redis 中的数据的命令,Redis 就会将该命令写入硬盘中的 AOF 文件。

AOF 文件的保存位置和 RDB 文件的位置相同,都是通过 dir 参数设置的,默认的文件名是 appendonly.aof。

在 Redis 的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:

1
2
3
appendfsync always #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度 
appendfsync everysec #每秒钟同步一次,显示地将多个写命令同步到硬盘
appendfsync no #让操作系统决定何时进行同步

为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec 选项 ,让 Redis 每秒同步一次 AOF 文件,Redis 性能几乎没受到任何影响。

而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis 还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。

优点:

  1. 数据安全,aof持久化可以配置appendfsync属性,有always,每进行一次命令操作就记录到aof文件中一次;everySec,就是每秒内进行一次文件的写操作;no就是不进行aof文件的写操作。
  2. 通过append模式写文件,即使中途服务器宕机,可以通过Redis-check-aof工具解决数据一致性问题。
  3. AOF机制的rewrite模式,用来将过大的aof文件缩小,实现原理是将所有的set 通过一句set 命令总结,所有的SADD命令用总结为一句,这样每种命令都概括为一句来执行,就可以减少aof文件的大小了。(注意,在重写的过程中,是创建子进程来完成重写操作,主进程每个命令都会在AOF缓冲区和AOF重写缓冲区进行保存,这样旧版aof文件可以实现数据最新,当更新完后将重写缓冲区中的数据写入新的aof文件中然后就可以将新的文件替换掉旧版的文件。默认触发是当aof文件大小是上次重写后大小的一倍且文件大于64M时触发。)

缺点:

  1. 文件会比RDB形式的文件大。
  2. 数据集大的时候,比rdb启动效率低。

总结

  1. 当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。一般情况下,只要使用默认开启的RDB即可,因为相对于AOF,RDB便于进行数据库备份,并且恢复数据集的速度也要快很多。

  2. 开启持久化缓存机制,对性能会有一定的影响,特别是当设置的内存满了的时候,更是下降到几百reqs/s。所以如果只是用来做缓存的话,可以关掉持久化。

Redis 4.0 对于持久化机制的优化

Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 aof-use-rdb-preamble 开启)。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。

这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。

当然缺点也是有的,AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

Sentinel (哨兵)原理

哨兵的功能

  • 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。
  • 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。
  • 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前 Redis 服务的主节点地址。
  • 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

哨兵系统中的主从节点,与普通的主从节点并没有什么区别,故障发现和转移是由哨兵来控制和完成的。

  • 哨兵节点本质上是 Redis 节点。
  • 每个哨兵节点,只需要配置监控主节点,便可以自动发现其他的哨兵节点和从节点。
  • 在哨兵节点启动和故障转移阶段,各个节点的配置文件会被重写(config rewrite)。

哨兵的原理

定时任务

每个哨兵节点维护了 3 个定时任务,定时任务的功能分别如下:

  1. 通过向主从节点发送 info 命令获取最新的主从结构。
  2. 通过发布订阅功能获取其他哨兵节点的信息。
  3. 通过向其他节点发送 ping 命令进行心跳检测,判断是否下线。

主观下线

在心跳检测的定时任务中,如果其他节点超过一定时间没有回复,哨兵节点就会将其进行主观下线。
顾名思义,主观下线的意思是一个哨兵节点“主观地”判断下线;与主观下线相对应的是客观下线。

客观下线

哨兵节点在对主节点进行主观下线后,会通过 sentinelis-master-down-by-addr 命令询问其他哨兵节点该主节点的状态。
如果判断主节点下线的哨兵数量达到一定数值,则对该主节点进行客观下线。

需要特别注意的是,客观下线是主节点才有的概念;如果从节点和哨兵节点发生故障,被哨兵主观下线后,不会再有后续的客观下线和故障转移操作。

选举领导者哨兵节点

当主节点被判断客观下线以后,各个哨兵节点会进行协商,选举出一个领导者哨兵节点,并由该领导者节点对其进行故障转移操作。

监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是 Raft 算法。

Raft 算法的基本思路是先到先得:即在一轮选举中,哨兵 A 向 B 发送成为领导者的申请,如果 B 没有同意过其他哨兵,则会同意 A 成为领导者。

一般来说,谁先完成客观下线,一般就能成为领导者。

故障转移

选举出的领导者哨兵,开始进行故障转移操作,该操作大体可以分为 3 个步骤:

  1. 在从节点中选择新的主节点:
    选择的原则是,首先过滤掉不健康的从节点,然后选择优先级最高的从节点(由 slave-priority 指定)。
    如果优先级无法区分,则选择复制偏移量最大的从节点;如果仍无法区分,则选择 runid 最小的从节点。

  2. 更新主从状态:
    通过 slaveof no one 命令,让选出来的从节点成为主节点;并通过 slaveof 命令让其他节点成为其从节点。

  3. Sentinel 被授权后,它将会获得宕掉的 master 的一份最新配置版本号 (config-epoch),当 failover 执行结束以后,这个版本号将会被用于最新的配置,通过广播形式通知其它 Sentinel,其它的 Sentinel 则更新对应 master 的配置。

Redis的并发竞争key问题

问题描述

同时有多个子系统去set一个key。这个时候要注意什么呢?

百度一下基本都是推荐用 Redis 事务机制。但是并不推荐使用 Redis 的事务机制。因为生产环境,基本都是 Redis 集群环境,做了数据分片操作。一个事务中有涉及到多个 Key 操作的时候,这多个 Key 不一定都存储在同一个 Redis-server 上。因此,Redis 的事务机制,十分鸡肋。

解决方案

如果对这个 Key 操作,不要求顺序

这种情况下,准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可,比较简单。

如果对这个key操作,要求顺序

假设有一个key1,系统A需要将key1设置为valueA,系统B需要将key1设置为valueB,系统C需要将key1设置为valueC.

期望按照key1的value值按照 valueA–>valueB–>valueC的顺序变化。这时在数据写入数据库的时候,需要保存一个时间戳。假设时间戳如下:

1
2
3
4
5
系统A key 1 {valueA 3:00}

系统B key 1 {valueB 3:05}

系统C key 1 {valueC 3:10}

那么,假设这会系统B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。

其他方法,比如利用队列,将set方法变成串行访问也可以。

缓存雪崩

简介

缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。

通俗来说就是:由于原有缓存失效(或者数据未加载到缓存中),新缓存未到期间(缓存正常从Redis中获取)所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机,造成系统的崩溃。

正常访问如下:
正常访问

缓存失效的时候如下图:
缓存失效

解决办法

  1. 事前:尽量保证整个 Redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。

  2. 事中:本地 Ehcache 缓存 + Hystrix 限流&降级,避免 数据库 崩掉。

  3. 事后:利用 Redis 持久化机制保存的数据尽快恢复缓存。

  4. 加锁计数(即限制并发的数量,可以用semphore)或者起一定数量的队列来避免缓存失效时大量请求并发到数据库。但这种方式会降低吞吐量。

  5. 分析用户行为,然后失效时间均匀分布。或者在失效时间的基础上再加1~5分钟的随机数。

  6. 如果是某台缓存服务器宕机,则考虑做主备。

缓存穿透

简介

一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。

解决办法

  1. 最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中。一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。

  2. 也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

缓存并发

简介

如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。

解决办法

对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。

缓存预热

简介

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

解决办法

  1. 直接写个缓存刷新页面,上线时手工操作下;

  2. 数据量不大,可以在项目启动的时候自动进行加载;

  3. 定时刷新缓存;

Redis常见性能问题和解决方案

  1. Master最好不要做任何持久化工作,如RDB内存快照和AOF日志文件
  2. 如果数据比较重要,某个Slave开启AOF备份数据,策略设置为每秒同步一次
  3. 为了主从复制的速度和连接的稳定性,Master和Slave最好在同一个局域网内
  4. 尽量避免在压力很大的主库上增加从库
  5. 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2 <- Slave3…
    这样的结构方便解决单点故障问题,实现Slave对Master的替换。如果Master挂了,可以立刻启用Slave1做Master,其他不变。

Redis 主从复制

原理

  1. 从服务器连接主服务器,发送SYNC(同步)命令;
  2. 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记录此后执行的所有写命令;
  3. 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令;
  4. 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
  5. 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
  6. 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令;(从服务器初始化完成)
  7. 主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令(从服务器初始化完成后的操作)。

优点

  1. 支持主从复制,主机会自动将数据同步到从机,可以进行读写分离;
  2. 为了分载Master的读操作压力,Slave服务器可以为客户端提供只读操作的服务,写服务仍然必须由Master来完成;
  3. Slave同样可以接受其它Slaves的连接和同步请求,这样可以有效的分载Master的同步压力;
  4. Master Server是以非阻塞的方式为Slaves提供服务。所以在Master-Slave同步期间,客户端仍然可以提交查询或修改请求;
  5. Slave Server同样是以非阻塞的方式完成数据同步。在同步期间,如果有客户端提交查询请求,Redis则返回同步之前的数据。

缺点

  1. Redis不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
  2. 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
  3. Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

Redis 哨兵模式

工作方式

  1. 每个Sentinel(哨兵)进程以每秒钟一次的频率向整个集群中的Master主服务器,Slave从服务器以及其他Sentinel(哨兵)进程发送一个 PING 命令。
  2. 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel(哨兵)进程标记为主观下线(SDOWN)
  3. 如果一个Master主服务器被标记为主观下线(SDOWN),则正在监视这个Master主服务器的所有 Sentinel(哨兵)进程要以每秒一次的频率确认Master主服务器的确进入了主观下线状态
  4. 当有足够数量的 Sentinel(哨兵)进程(大于等于配置文件指定的值)在指定的时间范围内确认Master主服务器进入了主观下线状态(SDOWN), 则Master主服务器会被标记为客观下线(ODOWN)
  5. 在一般情况下, 每个 Sentinel(哨兵)进程会以每 10 秒一次的频率向集群中的所有Master主服务器、Slave从服务器发送 INFO 命令。
  6. 当Master主服务器被 Sentinel(哨兵)进程标记为客观下线(ODOWN)时,Sentinel(哨兵)进程向下线的 Master主服务器的所有 Slave从服务器发送 INFO 命令的频率会从 10 秒一次改为每秒一次。
  7. 若没有足够数量的 Sentinel(哨兵)进程同意 Master主服务器下线, Master主服务器的客观下线状态就会被移除。若 Master主服务器重新向 Sentinel(哨兵)进程发送 PING 命令返回有效回复,Master主服务器的主观下线状态就会被移除。

优点

  1. 哨兵模式是基于主从模式的,所有主从的优点,哨兵模式都具有。
  2. 主从可以自动切换,系统更健壮,可用性更高。

缺点

  • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。

Redis Cluster集群模式

Redis3以后,节点之间提供了完整的sharding(分片)、replication(主备感知能力)、failover(故障转移)的特性。

配置一致性

每个节点(Node)内部都保存了集群的配置信息,存储在clusterState中,通过引入自增的epoch变量来使得集群配置在各个节点间保持一致。

sharding数据分片

将所有数据划分为16384个分片(slot),每个节点会对应一部分slot,每个key都会根据分布算法映射到16384个slot中的一个,分布算法为slotId=crc16(key)%16384

当一个client访问的key不在对应节点的slots中,Redis会返回给client一个moved命令,告知其正确的路由信息从而重新发起请求。client会根据每次请求来缓存本地的路由缓存信息,以便下次请求直接能够路由到正确的节点。

分片迁移

分片迁移的触发和过程控制由外部系统完成,Redis只提供迁移过程中需要的原语支持。

主要包含两种:一种是节点迁移状态设置,即迁移前标记源、目标节点;另一种是key迁移的原子化命令。

failover故障转移

故障发现

节点间两两通过TCP保持连接,周期性进行PING、PONG交互,若对方的PONG相应超时未收到,则将其置为PFAIL状态,并传播给其他节点。

故障确认

当集群中有一半以上的节点对某一个PFAIL状态进行了确认,则将起改为FAIL状态,确认其故障。

slave选举

当有一个master挂掉了,则其slave重新竞选出一个新的master。主要根据各个slave最后一次同步master信息的时间,越新表示slave的数据越新,竞选的优先级越高,就更有可能选中。竞选成功之后将消息传播给其他节点。

集群不可用的情况

  • 集群中任意master挂掉,且当前master没有slave。

  • 集群中超过半数以上master挂掉。

Redis-Cluster采用无中心结构的特点

  1. 所有的Redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
  2. 节点的fail是通过集群中超过半数的节点检测失效时才生效。
  3. 客户端与Redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

Pipeline有什么好处,为什么要用pipeline

可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。

使用Redis-benchmark进行压测的时候可以发现影响Redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

如果有大量的key需要设置同一时间过期需要注意什么

如果大量的key过期时间设置的过于集中,到过期的那个时间点,Redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。

Redis异步队列

原理

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

可不可以不用sleep

list有个指令叫blpop,在没有消息时,它会阻塞住直到消息到来。

能不能生产一次消费多次

使用pub/sub主题订阅者模式,可以实现1:N的消息队列。

pub/sub有什么缺点

在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。

Redis如何实现延时队列

使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来

使用keys指令可以扫出指定模式的key列表。

如果这个Redis正在给线上的业务提供服务,那使用keys指令会有什么问题

Redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。


LRU算法

Redis内存淘汰策略中有用到LRU算法,那么什么是LRU算法呢?

算法介绍

LRU(Least Recently Used),即最近最少使用,是一种缓存置换算法。其核心思想是:如果一个数据在最近一段时间没有被用到,那么将来被使用到的可能性也很小,所以就可以被淘汰掉。

Java实现简单的LRU算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
public class LRUCache<k, v> {
//容量
private int capacity;
//当前有多少节点的统计
private int count;
//缓存节点
private Map<k, node> nodeMap;
private Node head;
private Node tail;

public LRUCache(int capacity) {
if (capacity < 1) {
throw new IllegalArgumentException(String.valueOf(capacity));
}
this.capacity = capacity;
this.nodeMap = new HashMap<>();
//初始化头节点和尾节点,利用哨兵模式减少判断头结点和尾节点为空的代码
Node headNode = new Node(null, null);
Node tailNode = new Node(null, null);
headNode.next = tailNode;
tailNode.pre = headNode;
this.head = headNode;
this.tail = tailNode;
}

public void put(k key, v value) {
Node node = nodeMap.get(key);
if (node == null) {
if (count >= capacity) {
//先移除一个节点
removeNode();
}
node = new Node<>(key, value);
//添加节点
addNode(node);
} else {
//移动节点到头节点
moveNodeToHead(node);
}
}

public Node get(k key) {
Node node = nodeMap.get(key);
if (node != null) {
moveNodeToHead(node);
}
return node;
}

private void removeNode() {
Node node = tail.pre;
//从链表里面移除
removeFromList(node);
nodeMap.remove(node.key);
count--;
}

private void removeFromList(Node node) {
Node pre = node.pre;
Node next = node.next;

pre.next = next;
next.pre = pre;

node.next = null;
node.pre = null;
}

private void addNode(Node node) {
//添加节点到头部
addToHead(node);
nodeMap.put(node.key, node);
count++;
}

private void addToHead(Node node) {
Node next = head.next;
next.pre = node;
node.next = next;
node.pre = head;
head.next = node;
}

public void moveNodeToHead(Node node) {
//从链表里面移除
removeFromList(node);
//添加节点到头部
addToHead(node);
}

class Node<k, v> {
k key;
v value;
Node pre;
Node next;

public Node(k key, v value) {
this.key = key;
this.value = value;
}
}
}

LRU在Redis中的实现

Redis使用的是近似LRU算法,它跟常规的LRU算法还不太一样。

近似LRU算法通过随机采样法淘汰数据,每次随机出5(默认)个key,从里面淘汰掉最近最少使用的key。

可以通过maxmemory-samples参数修改采样数量:例:maxmemory-samples 10maxmenory-samples配置的越大,淘汰的结果越接近于严格的LRU算法。

Redis为了实现近似LRU算法,给每个key增加了一个额外增加了一个24bit的字段,用来存储该key最后一次被访问的时间。

Redis3.0对近似LRU的优化

Redis3.0对近似LRU算法进行了一些优化。新算法会维护一个候选池(大小为16),池中的数据根据访问时间进行排序,第一次随机选取的key都会放入池中,随后每次随机选取的key只有在访问时间小于池中最小的时间才会放入池中,直到候选池被放满。

当放满后,如果有新的key需要放入,则将池中最后访问时间最大(最近被访问)的移除。

当需要淘汰的时候,则直接从池中选取最近访问时间最小(最久没被访问)的key淘汰掉就行。

LFU算法

LFU算法是Redis4.0里面新加的一种淘汰策略。它的全称是Least Frequently Used

它的核心思想是根据key的最近被访问的频率进行淘汰,很少被访问的优先被淘汰,被访问的多的则被留下来。

LFU算法能更好的表示一个key被访问的热度。假如你使用的是LRU算法,一个key很久没有被访问到,只刚刚是偶尔被访问了一次,那么它就被认为是热点数据,不会被淘汰,而有些key将来是很有可能被访问到的则被淘汰了。

如果使用LFU算法则不会出现这种情况,因为使用一次并不会使一个key成为热点数据。

Redis究竟有没有ACID事务

原子性

事务具备原子性指的是,数据库将事务中多个操作当作一个整体来执行,服务要么执行事务中所有的操作,要么一个操作也不会执行。

事务队列

Redis 开始事务 multi 命令后,Redis 会为这个事务生成一个队列,每次操作的命令都会按照顺序插入到这个队列中。

这个队列里面的命令不会被马上执行,直到 exec 命令提交事务,所有队列里面的命令会被一次性,并且排他的进行执行。

Redis事务操作

对应如下图:

Redis事务操作示意图

从上面的例子可以看出,当执行一个成功的事务,事务里面的命令都是按照队列里面顺序的并且排他的执行。

但原子性又一个特点就是要么全部成功,要么全部失败,也就是我们传统 DB 里面说的回滚。

当我们执行一个失败的事务:

Redis执行失败事务

可以发现,就算中间出现了失败,set abc x 这个操作也已经被执行了,并没有进行回滚,从严格的意义上来说 Redis 并不具备原子性。

为何Redis不支持回滚

这个其实跟 Redis 的定位和设计有关系,先看看为何MySQL 可以支持回滚:

这个还是跟写 Log 有关系,Redis 是完成操作之后才会进行 AOF 日志记录,AOF 日志的定位只是记录操作的指令记录。

而 MySQL 有完善的 Redolog,并且是在事务进行 Commit 之前就会写完成 Redolog,Binlog:

MySQL写日志流程

要知道 MySQL 为了能进行回滚是花了不少的代价,Redis 应用的场景更多是对抗高并发具备高性能,所以 Redis 选择更简单,更快速无回滚的方式处理事务也是符合场景。

一致性

事务具备一致性指的是,如果数据库在执行事务之前是一致的,那么在事务执行之后,无论事务是否成功,数据库也应该是一致的。

从 Redis 来说可以从 2 个层面看,一个是执行错误是否有确保一致性,另一个是宕机时,Redis 是否有确保一致性的机制。

执行错误是否确保一致性

Redis执行错误

依然去执行一个错误的事务,在事务执行的过程中会识别出来并进行错误处理,这些错误并不会对数据库作出修改,也不会对事务的一致性产生影响。

宕机对一致性的影响

暂不考虑分布式高可用的 Redis 解决方案,先从单机看宕机恢复是否能满意数据完整性约束。

无论是 RDB 还是 AOF 持久化方案,可以使用 RDB 文件或 AOF 文件进行恢复数据,从而将数据库还原到一个一致的状态。

再议一致性

如果仅仅就 ACID 的表述上来说,一致性就是从 A 状态经过事务到达 B 状态没有破坏各种约束性,仅就 Redis 而言不谈实现的业务,那显然就是满意一致性。

但如果加上业务去谈一致性,例如,A 转账给 B,A 减少 10 块钱,B 增加 10 块钱,因为 Redis 并不具备回滚,也就不具备传统意义上的原子性,所以 Redis 也应该不具备传统的一致性。

其实,这里只是简单讨论下 Redis 在传统 ACID 上的概念怎么进行对接,或许,有可能是我想多了,用传统关系型数据库的 ACID 去审核 Redis 是没有意义的,Redis 本来就没有意愿去实现 ACID 的事务。

隔离性

隔离性指的是,数据库中有多个事务并发的执行,各个事务之间不会相互影响,并且在并发状态下执行的事务和串行执行的事务产生的结果是完全相同的。

Redis 因为是单线程操作,所以在隔离性上有天生的隔离机制,当 Redis 执行事务时,Redis 的服务端保证在执行事务期间不会对事务进行中断,所以,Redis 事务总是以串行的方式运行,事务也具备隔离性。

持久性

事务的持久性指的是,当一个事务执行完毕,执行这个事务所得到的结果被保存在持久化的存储中,即使服务器在事务执行完成后停机了,执行的事务的结果也不会被丢失。

Redis 是否具备持久化,这个取决于 Redis 的持久化模式:

  • 纯内存运行,不具备持久化,服务一旦停机,所有数据将丢失。

  • RDB 模式,取决于 RDB 策略,只有在满足策略才会执行 Bgsave,异步执行并不能保证 Redis 具备持久化。

  • AOF 模式,只有将 appendfsync 设置为 always,程序才会在执行命令同步保存到磁盘,这个模式下,Redis 具备持久化。(将 appendfsync 设置为 always,只是在理论上持久化可行,但一般不会这么操作)

简单总结:

  • Redis 具备了一定的原子性,但不支持回滚。

  • Redis 不具备 ACID 中一致性的概念。(或者说 Redis 在设计时就无视这点)

  • Redis 具备隔离性。

  • Redis 通过一定策略可以保证持久性。

Redis 和 ACID 纯属站在使用者的角度去思想,Redis 设计更多的是追求简单与高性能,不会受制于传统 ACID 的束缚。

本文标题:Redis知识点学习

文章作者:王洪博

发布时间:2019年04月27日 - 15:04

最后更新:2019年12月04日 - 05:12

原始链接:http://whb1990.github.io/posts/b1cc4593.html

▄︻┻═┳一如果你喜欢这篇文章,请点击下方"打赏"按钮请我喝杯 ☕
0%