redis

redis

1 基本数据类型

图片

1.1String类型

**用途:**适用于简单 key-value 存储、setnx key value 实现分布式锁、计数器(原子性)、分布式全局唯一 ID。

**底层:**C 语言中 String 用 char[] 数组表示,源码中用 SDS(simple dynamic string) 封装 char[],这是是 Redis 存储的最小单元,一个 SDS 最大可以存储512M 信息。

struct sdshdr{  
unsigned int len; // 标记char[]的长度  
unsigned int free; //标记char[]中未使用的元素个数  
char buf[]; // 存放元素的坑
}

Redis 对 SDS 再次封装生成了 RedisObject,核心有两个作用:

  1. 说明是 5 种类型哪一种。
  2. 里面有指针用来指向 SDS。

当你执行 set name sowhat 的时候,其实 Redis 会创建两个 RedisObject 对象,**键的 RedisObject 和 值的 RedisOjbect,**其中它们 type = REDIS_STRING,而 SDS 分别存储的就是 name 跟 sowhat 字符串咯。

并且 Redis 底层对 SDS 有如下优化:

  1. SDS 修改后大小 > 1M 时 系统会多分配空间来进行空间预分配。
  2. SDS 是惰性释放空间的,你 free 了空间,可是系统把数据记录下来下次想用时候可直接使用。不用新申请空间。
命令说明*案例*
set添加key-valueset username admin
get根据key获取数据get username
strlen根据key获取值的长度strlen key
exists判断key是否存在exists name 返回1存在 0不存在
del删除redis中的keydel key
Keys用于查询符合条件的keykeys * 查询redis中全部的keykeys n?me 使用占位符获取数据keys nam* 获取nam开头的数据
mset赋值多个key-valuemset key1 value1 key2 value2 key3 value3
mget获取多个key的值mget key1 key2
append对某个key的值进行追加append key value
type检查某个key的类型type key
select切换redis数据库select 0-15 redis中共有16个数据库
flushdb清空单个数据库flushdb
flushall清空全部数据库flushall
incr自动加1incr key
decr自动减1decr key
incrby指定数值添加incrby key 10
decrby指定数值减decrby key 10
expire指定key的生效时间 单位秒expire key 20 key20秒后失效
pexpire指定key的失效时间 单位毫秒pexpire key 2000key 2000毫秒后失效
ttl检查key的剩余存活时间ttl key -2数据不存在 -1该数据永不超时
persist撤销key的失效时间persist key

1.2、List

图片

查看源码底层 adlist.h 会发现底层就是个 双端链表,该链表最大长度为 2^32-1。常用就这几个组合。

  • lpush + lpop = stack 先进后出的栈
  • lpush + rpop = queue 先进先出的队列
  • lpush + ltrim = capped collection 有限集合
  • lpush + brpop = message queue 消息队列

一般可以用来做简单的消息队列,并且**当数据量小的时候可能用到独有的压缩列表来提升性能。**当然专业点还是要 RabbitMQ、ActiveMQ 等。

命令说明*案例*
lpush从队列的左边入队一个或多个元素LPUSH key value [value …]
rpush从队列的右边入队一个或多个元素RPUSH key value [value …]
lpop从队列的左端出队一个元素LPOP key
rpop从队列的右端出队一个元素RPOP key
lpushx当队列存在时从队列的左侧入队一个元素LPUSHX key value
rpushx当队列存在时从队列的右侧入队一个元素RPUSHx key value
lrange从列表中获取指定返回的元素LRANGE key start stop Lrange key 0 -1 获取全部队列的数据
lrem从存于 key 的列表里移除前 count 次出现的值为 value 的元素。 这个 count 参数通过下面几种方式影响这个操作:· count > 0: 从头往尾移除值为 value 的元素。· count < 0: 从尾往头移除值为 value 的元素。· count = 0: 移除所有值为 value 的元素。LREM list -2 “hello” 会从存于 list 的列表里移除最后两个出现的 “hello”。需要注意的是,如果list里没有存在key就会被当作空list处理,所以当 key 不存在的时候,这个命令会返回 0。
Lset设置 index 位置的list元素的值为 valueLSET key index value

1.3、Hash

散列非常适用于将一些相关的数据存储在一起,比如用户的购物车。该类型在日常用途还是挺多的。

这里需要明确一点:Redis 中只有一个 K,一个 V。其中 K 绝对是字符串对象,而 V 可以是 String、List、Hash、Set、ZSet 任意一种。

hash 的底层主要是采用字典 dict 的结构,整体呈现层层封装。从小到大如下:

dictEntry

真正的数据节点,包括 key、value 和 next 节点。

图片

dictht
  1. 数据 dictEntry 类型的数组,每个数组的 item 可能都指向一个链表;
  2. 数组长度 size;
  3. sizemask 等于 size - 1;
  4. 当前 dictEntry 数组中包含总共多少节点。

图片

dict
  1. dictType 类型,包括一些自定义函数,这些函数使得 key 和 value 能够存储;
  2. rehashidx 其实是一个标志量,如果为-1说明当前没有扩容,如果不为 -1 则记录扩容位置;
  3. dictht数组,两个Hash表;
  4. iterators 记录了当前字典正在进行中的迭代器。

图片

组合后结构就是如下:

图片

渐进式扩容

为什么 dictht ht[2] 是两个呢?

目的是在扩容的同时不影响前端的 CURD,慢慢的把数据从 ht[0] 转移到 ht[1]中,同时 rehashindex 来记录转移的情况,当全部转移完成,将 ht[1] 改成 ht[0] 使用。

rehashidx = -1 说明当前没有扩容,rehashidx != -1 则表示扩容到数组中的第几个了。

扩容之后的数组大小为大于 used*2 的 2 的 n 次方的最小值,跟 HashMap 类似。然后挨个遍历数组同时调整 rehashidx 的值,对每个 dictEntry[i] 再挨个遍历链表将数据 Hash 后重新映射到 dictht[1]里面。并且 dictht[0].use 跟 dictht[1].use 是动态变化的。

图片

整个过程的重点在于 rehashidx,其为第一个数组正在移动的下标位置,如果当前内存不够,或者操作系统繁忙,扩容的过程可以随时停止。

停止之后如果对该对象进行操作,那是什么样子的呢?

  1. 如果是新增,则直接新增后第二个数组,因为如果新增到第一个数组,以后还是要移过来,没必要浪费时间
  2. 如果是删除,更新,查询,则先查找第一个数组,如果没找到,则再查询第二个数组。

图片

命令说明*案例*
hset为对象添加数据hset key field value
hget获取对象的属性值hget key field
hexists判断对象的属性是否存在HEXISTS key field1表示存在 0表示不存在
hdel删除hash中的属性hdel user field [field …]
hgetall获取hash全部元素和值HGETALL key
hkyes获取hash中的所有字段HKEYS key
hlen获取hash中所有属性的数量hlen key
hmget获取hash里面指定字段的值hmget key field [field …]
hmset为hash的多个字段设定值hmset key field value [field value …]
hsetnx设置hash的一个字段,只有当这个字段不存在时有效HSETNX key field value
hstrlen获取hash中指定key的值的长度HSTRLEN key field
hvals获取hash的所有值HVALS user

1.4、Set

如果你明白 Java 中 HashSet 是 HashMap 的简化版,那么这个 Set 应该也理解了。都是一样的套路而已。这里你可以认为是没有 Value 的 Dict。看源码 t.set.c 就可以了解本质了。

int setTypeAdd(robj *subject, robj *value) {    
		long long llval;    
	if (subject->encoding == REDIS_ENCODING_HT) {         
	// 看到底层调用的还是dictAdd,只不过第三个参数= NULL         
	if (dictAdd(subject->ptr,value,NULL) == DICT_OK) {            
        incrRefCount(value);            
        return 1;        
        }
        ....

1.5、ZSet

范围查找的天敌就是有序集合,看底层 redis.h 后就会发现 Zset 用的就是可以跟二叉树媲美的跳跃表来实现有序。跳表就是多层链表的结合体,跳表分为许多层(level),每一层都可以看作是数据的索引,这些索引的意义就是加快跳表查找数据速度。

每一层的数据都是有序的,上一层数据是下一层数据的子集,并且第一层(level 1)包含了全部的数据;层次越高,跳跃性越大,包含的数据越少。并且随便插入一个数据该数据是否会是跳表索引完全随机的跟玩骰子一样。

跳表包含一个表头,它查找数据时,是从上往下,从左往右进行查找。现在找出值为 37 的节点为例,来对比说明跳表和普遍的链表。

没有跳表查询 比如我查询数据 37,如果没有上面的索引时候路线如下图:

图片

有跳表查询 有跳表查询 37 的时候路线如下图:

图片

应用场景:积分排行榜、时间排序新闻、延时队列

2、底层原理

2.1、bitmap

BitMap 原本的含义是用一个比特位来映射某个元素的状态。由于一个比特位只能表示 0 和 1 两种状态,所以 BitMap 能映射的状态有限,但是使用比特位的优势是能大量的节省内存空间。

在 Redis 中BitMap 底层是基于字符串类型实现的,可以把 Bitmaps 想象成一个以比特位为单位的数组,数组的每个单元只能存储 0 和 1,数组的下标在 Bitmaps 中叫做偏移量,BitMap 的 offset 值上限 2^32 - 1。

图片

用户签到:

key = 年份:用户id offset = (今天是一年中的第几天) % (今年的天数)

统计活跃用户:

使用日期作为 key,然后用户 id 为 offset 设置不同 offset 为 0 1 即可。

PS : Redis 的通讯协议是基于 TCP 的应用层协议 RESP(REdis Serialization Protocol)。

2.2HyperLogLog

HyperLogLog :是一种概率数据结构,它使用概率算法来统计集合的近似基数。而它算法的最本源则是伯努利过程 + 分桶 + 调和平均数

功能:误差允许范围内做基数统计 (基数就是指一个集合中不同值的个数) 的时候非常有用,每个 HyperLogLog 的键可以计算接近 2^64 不同元素的基数,而大小只需要 12KB。错误率大概在 0.81%。所以如果用做 UV 统计很合适。

HyperLogLog 底层一共分了 2^14 个桶,也就是 16384 个桶。每个(registers)桶中是一个 6 bit 的数组,这里有个骚操作就是一般人可能直接用一个字节当桶浪费 2 个 bit 空间,但是 Redis 底层只用 6 个然后通过前后拼接实现对内存用到了极致,最终就是 16384*6/8/1024 = 12KB。

2.3、Bloom Filter

使用布隆过滤器得到的判断结果:不存在的一定不存在,存在的不一定存在。

布隆过滤器原理:

当一个元素被加入集合时,通过 K 个散列函数将这个元素映射成一个位数组中的 K 个点(有效降低冲突概率),把它们置为 1。检索时,我们只要看看这些点是不是都是 1 就知道集合中有没有它了:如果这些点有任何一个为 0,则被检元素一定不在;如果都是 1,则被检元素很可能在。这就是布隆过滤器的基本思想。

想玩的话可以用 Google 的 guava 包玩耍一番。

图片

3.持久化

因为 Redis 数据在内存,断电既丢,因此持久化到磁盘是必须得有的,Redis提供了 RDB 跟 AOF 两种模式。

3.1、RDB

RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。更适合做冷备。

优点:

  1. 压缩后的二进制文,适用于备份、全量复制,用于灾难恢复加载 RDB 恢复数据远快于 AOF 方式,适合大规模的数据恢复。
  2. 如果业务对数据完整性和一致性要求不高,RDB 是很好的选择。数据恢复比 AOF 快。

缺点:

  1. RDB 是周期间隔性的快照文件,数据的完整性和一致性不高,因为 RDB 可能在最后一次备份时宕机了。
  2. 备份时占用内存,因为 Redis 在备份时会独立 fork 一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。所以要考虑到大概两倍的数据膨胀性。

注意手动触发及 COW:

  1. SAVE 直接调用 rdbSave ,阻塞 Redis 主进程,导致无法提供服务。
  2. BGSAVE 则 fork 出一个子进程,子进程负责调用 rdbSave ,在保存完成后向主进程发送信号告知完成。在 BGSAVE 执行期间仍可以继续处理客户端的请求。
  3. Copy On Write 机制,备份的是开始那个时刻内存中的数据,只复制被修改内存页数据,不是全部内存数据。
  4. Copy On Write 时如果父子进程大量写操作会导致分页错误。

图片

3.2、AOF

AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,因为这个模式是只追加的方式,所以没有任何磁盘寻址的开销,所以很快,有点像 Mysql 中的 binlog。AOF 更适合做热备。

**优点:**AOF 是一秒一次去通过一个后台的线程 fsync 操作,数据丢失不用怕。

缺点:

  1. 对于相同数量的数据集而言,AOF 文件通常要大于 RDB 文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
  2. 根据同步策略的不同,AOF 在运行效率上往往会慢于 RDB。总之,每秒同步策略的效率是比较高的。

AOF 整个流程分两步:

第一步是命令的实时写入,不同级别可能有 1 秒数据损失。命令先追加到aof_buf 然后再同步到 AO 磁盘,如果实时写入磁盘会带来非常高的磁盘 IO,影响整体性能。

第二步是对 aof 文件的重写,目的是为了减少 AOF 文件的大小,可以自动触发或者手动触发(BGREWRITEAOF),是 Fork 出子进程操作,期间 Redis 服务仍可用。

图片

  1. 在重写期间,由于主进程依然在响应命令,为了保证最终备份的完整性;它依然会写入旧的 AOF 中,如果重写失败,能够保证数据不丢失。
  2. 为了把重写期间响应的写入信息也写入到新的文件中,因此也会为子进程保留一个 buf,防止新写的 file 丢失数据。
  3. 重写是直接把当前内存的数据生成对应命令,并不需要读取老的 AOF 文件进行分析、命令合并。
  4. 无论是 RDB 还是 AOF 都是先写入一个临时文件,然后通过 rename 完成文件的替换工作。

关于 Fork 的建议:

  1. 降低 fork 的频率,比如可以手动来触发 RDB 生成快照、与 AOF 重写;
  2. 控制 Redis 最大使用内存,防止 fork 耗时过长;
  3. 配置牛逼点,合理配置 Linux 的内存分配策略,避免因为物理内存不足导致 fork 失败。
  4. Redis 在执行 BGSAVE 和 BGREWRITEAOF 命令时,哈希表的负载因子>=5,而未执行这两个命令时>=1。目的是尽量减少写操作,避免不必要的内存写入操作。
  5. 哈希表的扩展因子:哈希表已保存节点数量 / 哈希表大小。因子决定了是否扩展哈希表。

3.3、恢复

启动时会先检查 AOF(数据更完整)文件是否存在,如果不存在就尝试加载 RDB。

图片

3.4、建议

既然单独用 RDB 会丢失很多数据。单独用 AOF,数据恢复没 RDB 来的快,所以出现问题了第一时间用 RDB 恢复,然后 AOF 做数据补全才是王道。

4、redis高效原因

4.1、 基于内存实现:

数据都存储在内存里,相比磁盘 IO 操作快百倍,操作速率很快。

4.2、高效的数据结构(内存资源高可用):

Redis 底层多种数据结构支持不同的数据类型,比如 HyperLogLog 它连 2 个字节都不想浪费。

4.3、丰富而合理的编码:

Redis 底层提供了 丰富而合理的编码 ,五种数据类型根据长度及元素的个数适配不同的编码格式。

  • String:自动存储 int 类型,非 int 类型用 raw 编码。
  • List:字符串长度且元素个数小于一定范围使用 ziplist 编码,否则转化为 linkedlist 编码。
  • Hash:hash 对象保存的键值对内的键和值字符串长度小于一定值及键值对。
  • Set:保存元素为整数及元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码。
  • Zset:保存的元素个数小于定值且成员长度小于定值使用 ziplist 编码,任意条件不满足,则使用 skiplist 编码。

4.4、合适的线程模型:

I/O 多路复用模型同时监听客户端连接,多线程是需要上下文切换的,对于内存数据库来说这点很致命。

图片

redis6.0后引入多线程提速

要知道 读写网络的 read/write 系统耗时 >> Redis 运行执行耗时,Redis 的瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:

  1. 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式;
  2. 使用多线程充分利用多核,典型的实现比如 Memcached。

协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。所以 Redis 支持多线程主要就是两个原因:

  1. 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核;
  2. 多线程任务可以分摊 Redis 同步 IO 读写负荷。

关于多线程须知:

  1. Redis 6.0 版本默认多线程是关闭的 io-threads-do-reads no;
  2. Redis 6.0 版本 开启多线程后 线程数也要谨慎设置;
  3. 多线程可以使得性能翻倍,但是多线程只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。

5、常见问题

5.1、缓存雪崩

雪崩定义:Redis 中大批量 key 在同一时间同时失效导致所有请求都打到了 MySQL。而 MySQL 扛不住导致大面积崩塌。

雪崩解决方案:

  • 缓存数据的过期时间加上个随机值,防止同一时间大量数据过期现象发生;
  • 如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中;
  • 设置热点数据永远不过期。

5.2、缓存穿透

穿透定义:缓存穿透是指缓存和数据库中都没有的数据,比如 ID 默认>0,黑客一直 请求 ID= -12 的数据那么就会导致数据库压力过大,严重会击垮数据库。

穿透解决方案:

  • 后端接口层增加用户鉴权校验,参数做校验等。
  • 单个 IP 每秒访问次数超过阈值直接拉黑 IP,关进小黑屋 1 天,在获取 IP 代理池的时候我就被拉黑过;
  • 从缓存取不到的数据,在数据库中也没有取到,这时也可以将 key-value 对写为 key-null 失效时间可以为 15 秒防止恶意攻击;
  • 用 Redis 提供的 Bloom Filter 特性也 OK。

5.3、缓存击穿

击穿定义:大并发集中对这一个热点 key 进行访问,当这个 Key 在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库。

击穿解决:设置热点数据永远不过期加上互斥锁也能搞定了。

5.4、双写一致性

双写:缓存跟数据库均更新数据,如何保证数据一致性?

先更新数据库,再更新缓存

安全问题:线程 A 更新数据库->线程 B 更新数据库->线程 B 更新缓存->线程 A 更新缓存。导致脏读。

业务场景:读多写少场景,频繁更新数据库而缓存根本没用。更何况如果缓存是叠加计算后结果更浪费性能。

先删缓存,再更新数据库

A 请求写来更新缓存。

B 发现缓存不在去数据查询旧值后写入缓存。

A 将数据写入数据库,此时缓存跟数据库不一致。

因此 FackBook 提出了 Cache Aside Pattern

失效:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。

命中:应用程序从 cache 中取数据,取到后返回。

更新:先把数据存到数据库中,成功后,再让缓存失效。

5.5、脑裂

脑裂是指因为网络原因,导致 master 节点、slave 节点 和 sentinel 集群处于不用的网络分区,此时因为 sentinel 集群无法感知到 master 的存在,所以将slave 节点提升为 master 节点 此时存在两个不同的 master 节点就像一个大脑分裂成了两个。

其实在 Hadoop 、Spark 集群中都会出现这样的情况,只是解决方法不同而已(用 ZK 配合强制杀死)。

集群脑裂问题中,如果客户端还在基于原来的 master 节点继续写入数据那么新的 master 节点将无法同步这些数据,当网络问题解决后 sentinel 集群将原先的 master 节点降为 slave 节点,此时再从新的 master 中同步数据将造成大量的数据丢失。

Redis 处理方案是 redis 的配置文件中存在两个参数

min-replicas-to-write 3  表示连接到master的最少slave数量
min-replicas-max-lag 10  表示slave连接到master的最大延迟时间

如果连接到 master 的 slave 数量 < 第一个参数 且 ping 的延迟时间 <= 第二个参数,那么 master 就会拒绝写请求。配置了这两个参数后如果发生了集群脑裂,则原先的 master 节点接收到客户端的写入请求会拒绝就可以减少数据同步之后的数据丢失。

5.6、事务

MySQL 中的事务还是挺多道道的还要,而在 Redis 中的事务只要有如下三步:

图片

关于事务具体结论:

  • redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。
  • Redis 事务没有隔离级别的概念:批量操作在发送 EXEC 命令前被放入队列缓存,并不会被实际执行,也就不存在事务内的查询要看到事务里的更新,事务外查询不能看到。
  • Redis 不保证原子性:Redis 中单条命令是原子性执行的,但事务不保证原子性。
  • Redis 编译型错误事务中所有代码均不执行,指令使用错误。运行时异常是错误命令导致异常,其他命令可正常执行。
  • watch 指令类似于乐观锁,在事务提交时,如果 watch 监控的多个 KEY 中任何 KEY 的值已经被其他客户端更改,则使用 EXEC 执行事务时,事务队列将不会被执行。

5.7、正确开发步骤

上线前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。

上线时:本地 ehcache 缓存 + Hystrix 限流 + 降级,避免 MySQL 扛不住。上线后:Redis 持久化采用 RDB + AOF 来保证断点后自动从磁盘上加载数据,快速恢复缓存数据。

6 Redis清理内策略

6.1、Redis 的过期策略

Redis 中过期策略 通常有以下三种:

定时过期:

每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即对 key 进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的 CPU 资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。

惰性过期:

只有当访问一个 key 时,才会判断该 key 是否已过期,过期则清除。该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期 key 没有再次被访问,从而不会被清除,占用大量内存。

定期过期:

每隔一定的时间,会扫描一定数量的数据库的 expires 字典中一定数量的 key,并清除其中已过期的 key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得 CPU 和内存资源达到最优的平衡效果。

expires 字典会保存所有设置了过期时间的 key 的过期时间数据,其中 key 是指向键空间中的某个键的指针,value 是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该 Redis 集群中保存的所有键。

Redis 采用的过期策略:惰性删除 + 定期删除。memcached 采用的过期策略:惰性删除。

6.2、6 种内存淘汰策略

Redis 的内存淘汰策略是指在 Redis 的用于缓存的内存不足时,怎么处理需要新写入且需要申请额外空间的数据。

volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  • no-enviction(驱逐):禁止驱逐数据,不删除的意思。

面试常问常考的也就是 LRU 了,大家熟悉的 LinkedHashMap 中也实现了 LRU 算法的,实现如下:

class SelfLRUCache<K, V> extends LinkedHashMap<K, V> {    
private final int CACHE_SIZE;    
        /**     
        * 传递进来最多能缓存多少数据     
        * @param cacheSize 缓存大小     
        */   
        public SelfLRUCache(int cacheSize) { 
            // true 表示让 linkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。        
            super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);        
            CACHE_SIZE = cacheSize;    
        }    
        @Override    
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {        
            // 当 map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。        
            return size() > CACHE_SIZE;    
        }
}

6.3、总结

Redis 的内存淘汰策略的选取并不会影响过期的 key 的处理。内存淘汰策略用于处理内存不足时的需要申请额外空间的数据,过期策略用于处理过期的缓存数据。

7、Redis 集群部署

单机问题有机器故障、容量瓶颈、QPS 瓶颈。在实际应用中,Redis 的多机部署时候会涉及到 redis 主从复制、Sentinel 哨兵模式、Redis Cluster。

模式优点缺点
单机版架构简单,部署方便机器故障、容量瓶颈、QPS 瓶颈
主从复制高可靠性,读写分离故障恢复复杂,主库的写跟存受单机限制
Sentinel 哨兵集群部署简单,HA原理繁琐,slave 存在资源浪费,不能解决读写分离问题
Redis Cluster数据动态存储solt,可扩展,高可用客户端动态感知后端变更,批量操作支持查

7.1、redis 主从复制

该模式下 具有高可用性且读写分离, 会采用 增量同步 跟 全量同步 两种机制。

全量同步

图片

Redis 全量复制一般发生在 Slave 初始化阶段,这时 Slave 需要将 Master 上的所有数据都复制一份:

  • slave 连接 master,发送 psync 命令。
  • master 接收到 psync 命名后,开始执行 bgsave 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令。
  • master 发送快照文件到 slave,并在发送期间继续记录被执行的写命令。
  • slave 收到快照文件后丢弃所有旧数据,载入收到的快照。
  • master 快照发送完毕后开始向slave发送缓冲区中的写命令。
  • slave 完成对快照的载入,开始接收命令请求,并执行来自 master 缓冲区的写命令。
增量同步

也叫指令同步,就是从库重放在主库中进行的指令。Redis 会把指令存放在一个环形队列当中,因为内存容量有限,如果备机一直起不来,不可能把所有的内存都去存指令,也就是说,如果备机一直未同步,指令可能会被覆盖掉。

Redis 增量复制是指 Slave 初始化后开始正常工作时 master 发生的写操作同步到 slave 的过程。增量复制的过程主要是 master 每执行一个写命令就会向 slave 发送相同的写命令。

图片

Redis 主从同步策略:
  • 主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
  • slave 在同步 master 数据时候,如果 slave 丢失连接不用怕,slave 在重新连接之后丢失重补。
  • 一般通过主从来实现读写分离,但是如果 master 挂掉后如何保证 Redis 的 HA 呢?引入 Sentinel 进行 master 的选择。

7.2、高可用之哨兵模式

图片

Redis-sentinel 本身是一个独立运行的进程,一般 sentinel 集群节点数至少三个且奇数个。它能监控多个 master-slave 集群,sentinel 节点发现 master 宕机后能进行自动切换。Sentinel 可以监视任意多个主服务器以及主服务器属下的从服务器,并在被监视的主服务器下线时,自动执行故障转移操作。这里需注意 sentinel 也有 single-point-of-failure 问题。

大致罗列下哨兵用途:

集群监控:循环监控 master 跟 slave 节点。

消息通知:当它发现有 redis 实例有故障的话,就会发送消息给管理员

故障转移:这里分为主观下线(单独一个哨兵发现 master 故障了)。客观下线(多个哨兵进行抉择发现达到 quorum 数时候开始进行切换)。

配置中心:如果发生了故障转移,它会通知将 master 的新地址写在配置中心告诉客户端。

7.3、Redis Cluster

RedisCluster 是 Redis 的分布式解决方案,在 3.0 版本后推出的方案,有效地解决了 Redis 分布式的需求。

图片

分区规则

图片

常见的分区规则

  • 节点取余:hash(key) % N
  • 一致性哈希:一致性哈希环
  • 虚拟槽哈希:CRC16[key] & 16383

RedisCluster 采用了虚拟槽分区方式,具题的实现细节如下:

  • 采用去中心化的思想,它使用虚拟槽 solt 分区覆盖到所有节点上,取数据一样的流程,节点之间使用轻量协议通信 Gossip 来减少带宽占用所以性能很高,

  • 自动实现负载均衡与高可用,自动实现 failover 并且支持动态扩展,官方已经玩到可以 1000 个节点 实现的复杂度低。

  • 每个 Master 也需要配置主从,并且内部也是采用哨兵模式,如果有半数节点发现某个异常节点会共同决定更改异常节点的状态。

  • 如果集群中的 master 没有 slave 节点,则 master 挂掉后整个集群就会进入 fail 状态,因为集群的 slot 映射不完整。如果集群超过半数以上的 master 挂掉,集群都会进入 fail 状态。

  • 官方推荐 集群部署至少要 3 台以上的 master 节点。

    8、Redis限流

    在开发高并发系统时,有三把利器用来保护系统:缓存、降级和限流。那么何为限流呢?顾名思义,限流就是限制流量,就像你宽带包了 1 个 G 的流量,用完了就没了。通过限流,我们可以很好地控制系统的 qps,从而达到保护系统的目的。

    8.1、基于 Redis 的 setnx、zset

    setnx

    比如我们需要在 10 秒内限定 20 个请求,那么我们在 setnx 的时候可以设置过期时间 10,当请求的 setnx 数量达到 20 时候即达到了限流效果。

    缺点:比如当统计 1-10 秒的时候,无法统计 2-11 秒之内,如果需要统计 N 秒内的 M 个请求,那么我们的 Redis 中需要保持 N 个 key 等等问题。

    zset

    其实限流涉及的最主要的就是滑动窗口,上面也提到 1-10 怎么变成 2-11。其实也就是起始值和末端值都各+1 即可。我们可以将请求打造成一个 zset 数组,当每一次请求进来的时候,value 保持唯一,可以用 UUID 生成,而 score 可以用当前时间戳表示,因为 score 我们可以用来计算当前时间戳之内有多少的请求数量。而 zset 数据结构也提供了 range 方法让我们可以很轻易的获取到 2 个时间戳内有多少请求,

    缺点:就是 zset 的数据结构会越来越大。

    8.2、漏桶算法

    漏桶算法思路:把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

    图片

    8.3、令牌桶算法

    令牌桶算法的原理:可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。

    图片

    细节流程大致:

    • 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
    • 根据限流大小,设置按照一定的速率往桶里添加令牌;
    • 设置桶最大可容纳值,当桶满时新添加的令牌就被丢弃或者拒绝;
    • 请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
    • 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。

    工程化:

    • 自定义注解、aop、Redis + Lua 实现限流。
      range 方法让我们可以很轻易的获取到 2 个时间戳内有多少请求,

    缺点:就是 zset 的数据结构会越来越大。

    8.2、漏桶算法

    漏桶算法思路:把水比作是请求,漏桶比作是系统处理能力极限,水先进入到漏桶里,漏桶里的水按一定速率流出,当流出的速率小于流入的速率时,由于漏桶容量有限,后续进入的水直接溢出(拒绝请求),以此实现限流。

    [外链图片转存中…(img-dIaZqnaC-1610522029503)]

    8.3、令牌桶算法

    令牌桶算法的原理:可以理解成医院的挂号看病,只有拿到号以后才可以进行诊病。

    [外链图片转存中…(img-YMKS4jJE-1610522029504)]

    细节流程大致:

    • 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
    • 根据限流大小,设置按照一定的速率往桶里添加令牌;
    • 设置桶最大可容纳值,当桶满时新添加的令牌就被丢弃或者拒绝;
    • 请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
    • 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。

    工程化:

    • 自定义注解、aop、Redis + Lua 实现限流。
    • 推荐 guava 的 RateLimiter 实现。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值