Bpazy / blog

我的博客,欢迎关注和讨论
https://github.com/Bpazy/blog/issues
MIT License
41 stars 2 forks source link

杂谈 Redis #299

Open Bpazy opened 1 year ago

Bpazy commented 1 year ago

重点:

  1. 数据类型的选择
  2. 集群
  3. 集群扩容

Redis五大数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及Zset(sorted set:有序集合)。

String

Redis字符串存储字节序列,包括文本、序列化对象和二进制数组。因此,字符串是与Redis键关联的最简单类型的值。它们通常用于缓存,但也支持额外的功能,可以实现计数器和执行位操作。

image

Refer: https://blog.csdn.net/w15558056319/article/details/121223706

Bpazy commented 1 year ago

String 和 Hash 对比

在 redis 中,string 类型和 hash 类型,都可以用来存储对象信息(结构体数据)。 那么,string 和 hash 有什么区别?该如何抉择?

既然 string 可以实现相同的功能,为何还要用 hash 呢?在 redis 官网可看到优先使用 hash 的字眼,主要是基于以下三个因素:内存占用率时间复杂度使用的简便性

假设用户表 user 有三个字段 id, name 和 age。

string 类型

第一种存储方式:单独存储用户的每个属性(字段)

127.0.0.1:6379> set user:1:name jack
OK
127.0.0.1:6379> set user:1:age 20
OK

优点:简单直观,便于对用户的每个属性进行查询和更新。 缺点:占用了更多的 key 和内存资源,用户的属性过于分散。 在生产环境中,一般不会采用这种方式。

第二种存储方式:先将整个用户信息(对象或数组)转换为 json 字符串,然后再存储。

set user:1 json_str

优点:占用的 key 和内存资源少。 缺点:操作比较麻烦,每次都要使用序列化和反序列化,有一定开销。而且不便于单独操作用户的某个属性。 在生产环境中,可以采用这种方式。

hash 类型

hash 可以对结构体数据的每个属性(字段)进行单独存储和访问。

采用 hash 存储用户信息:

127.0.0.1:6379> hmset user:1 name jack age 20
OK

127.0.0.1:6379> hget user:1 name
"jack"
127.0.0.1:6379> hgetall user:1
1) "name"
2) "jack"
3) "age"
4) "20"

优点:简单直观,便于对用户的每个属性和整体信息进行查询和更新。 缺点:和 string 比,占用的内存稍微多一点。

总结:推荐优先使用 hash 来存储用户信息。

Bpazy commented 1 year ago

大 key 和大 value 问题总结

1. 大 value 问题

1.内存不均:单 value 较大时,可能会导致节点之间的内存使用不均匀,间接地影响 key 的部分和负载不均匀; 2.阻塞请求:Redis 为单线程,单 value 较大读写需要较长的处理时间,会阻塞后续的请求处理; 3.阻塞网络:单 value 较大时会占用服务器网卡较多带宽,可能会影响该服务器上的其他 Redis 实例或者应用。

2. 大 key 问题

我们需要知道 Redis是如何存储 key 和 value 的: 根结构为 RedisServer,其中包含 RedisDB(数据库)。而 RedisDB 实际上是使用 Dict(字典)结构对 Redis 中的 kv 进行存储的。这里的 key 即字符串,value 可以是 string/hash/list/set/zset 这五种对象之一。 image

Dict 字典结构中,存储数据的主题为 DictHt,即哈希表。而哈希表本质上是一个 DictEntry(哈希表节点)的数组,并且使用链表法解决哈希冲突问题(关于哈希冲突的解决方法可以参考大佬的文章 解决哈希冲突的常用方法分析)。

所以在这里实际存储时,key 和 value 都是存储在 DictEntry 中的。所以基本上来说,大 key 和大 value 带来的内存不均和网络 IO 压力都是一致的,只是 key 相较于 value 还多一个做 hashcode 和比较的过程(链表中进行遍历比较 key),会有更多的内存相关开销。

结论:

  1. 大 key 和大 value 的危害是一致的:内存不均、阻塞请求、阻塞网络。
  2. key 由于比 value 需要做更多的操作如 hashcode、链表中比较等操作,所以会比 value 更多一些内存相关开销。

如何解决大 kv 问题

Redis使用过程中经常会有各种大 key 的情况, 比如:

  1. 单个简单的 key 存储的 value 很大
  2. hash, set,zset,list 中存储过多的元素(以万为单位)

    到底多大的数据量才算是大key?

    没有固定的判别标准,通常认为字符串类型的key对应的value值占用空间大于1M,或者集合类型的k元素数量超过1万个,就算是大key。

    Redis大key问题的定义及评判准则并非一成不变,而应根据Redis的实际运用以及业务需求来综合评估。例如,在高并发且低延迟的场景中,仅10kb可能就已构成大key;然而在低并发、高容量的环境下,大key的界限可能在100kb。因此,在设计与运用Redis时,要依据业务需求与性能指标来确立合理的大key阈值。

由于 Redis 是单线程运行的,如果一次操作的 value 很大会对整个 Redis 的响应时间造成负面影响,所以,业务上能拆则拆,下面举几个典型的分拆方案。

业务场景: 通过 hash 的方式来存储每一天用户订单次数。那么 key = order_20200102, field = order_id, value = 10。那么如果一天有百万千万甚至上亿订单的时候,key 后面的值是很多,存储空间也很大,造成所谓的大 key。

大key的风险:

  1. 读写大 key 会导致超时严重,甚至阻塞服务。
  2. 如果删除大 key,DEL 命令可能阻塞 Redis 进程数十秒,使得其他请求阻塞,对应用程序和 Redis 集群可用性造成严重的影响。

Redis 使用会出现大key的场景:

  1. 单个简单 key 的存储的value过大;
  2. hash、set、zset、list 中存储过多的元素。

解决问题:

  1. 单个简单 key 的存储的 value 过大的解决方案:

将大 key 拆分成多个 key-value,使用 multiGet 方法获得值,这样的拆分主要是为了减少单台操作的压力,而是将压力平摊到集群各个实例中,降低单台机器的 IO 操作。

  1. hash、set、zset、list中存储过多的元素的解决方案:

1) 类似于第一种场景,使用第一种方案拆分; 2) 以 hash 为例,将原先的 hget、hset 方法改成(假如固定一个 hash 桶的数量为 10000),先计算 field 的 hash 值模取 10000,确定该 field 在哪一个 key 上,如:

将大 key 进行分割,为了均匀分割,可以对 field 进行 hash 并通过质数 N 取余,将余数加到 key 上面,我们取质数 N 为 997。那么新的 key 则可以设置为:

newKey = String.valueOf( Math.abs(order_id.hashcode() % 997) )
field = order_id
value = 10
hset (newKey, field, value) ;  
hget(newKey, field)
Bpazy commented 1 year ago

Redis 集群

高可用的两种架构模式:主从模式、哨兵模式、集群模式。其中主从、哨兵都有较为严重的缺点,集群模式则集合了两者有点,这里直接谈集群模式。

Cluster 即 集群模式,类似MySQL,Redis 集群也是一种分布式数据库方案,集群通过分片(sharding)模式来对数据进行管理,并具备分片间数据复制、故障转移和流量调度的能力。这种 分治模式很常见,我们在 微服务系列:拆分策略MySQL系列:分库分表 中实践过很多次了。

Redis集群的做法是 将数据划分为 16384(2的14次方)个哈希槽(slots),如果你有多个实例节点,那么每个实例节点将管理其中一部分的槽位,槽位的信息会存储在各自所归属的节点中。以下图为例,该集群有4个 Redis 节点,每个节点负责集群中的一部分数据,数据量可以不均匀。比如性能好的实例节点可以多分担一些压力。 image

一个Redis集群一共有16384个哈希槽,你可以有1 ~ n个节点来分配这些哈希槽,可以不均匀分配,每个节点可以处理0个 到至多 16384 个槽点。 当16384个哈希槽都有节点进行管理的时候,集群处于online 状态。同样的,如果有一个哈希槽没有被管理到,那么集群处于offline状态。

上面图中4个实例节点组成了一个集群,集群之间的信息通过 Gossip协议 进行交互,这样就可以在某一节点记录其他节点的哈希槽(slots)的分配情况。

单机的吞吐无法承受持续扩增的流量的时候,最好的办法是从横向(scale out) 和 纵向(scale up)两方面进行扩展,这个我们在 MySQL系列 和 微服务系列 的时候已经讨论过了。

那横向扩展和纵向扩展各有什么优缺点呢?

现实情况下,在面对千万级甚至亿级别的流量的时候,很多大厂都是在千百台的实例节点组成的集群上进行流量调度、服务治理的。所以,使用Cluster模式,是业内广泛采用的模式。

集群组建过程

image

集群数据分片原理

现在的Redis集群分片的做法,主要是使用了官方提供的 Redis Cluster 方案。这种方案就是的核心就是集群的实例节点与哈希槽(slots)之间的划分、映射与管理。下面我们来看看他具体的步骤。

哈希槽(slots)的划分

这个前面已经说过了,我们会将整个Redis数据库划分为16384个哈希槽,你的Redis集群可能有n个实例节点,每个节点可以处理0个 到至多 16384 个槽点,这些节点把 16384个槽位瓜分完成。 而你实际存储的Redis键值信息也必然归属于这 16384 个槽的其中一个。slots 与 Redis Key 的映射是通过以下两个步骤完成的:

  1. 使用 CRC16 算法计算键值对信息的Key,会得出一个 16 bit 的值。
  2. 将 第1步中得到的 16 bit 的值对 16384 取模,得到的值会在 0 ~ 16383 之间,映射到对应到哈希槽中。 当然,可能在一些特殊的情况下,你想把某些key固定到某个slot上面,也就是同一个实例节点上。这时候可以用hash tag能力,强制 key 所归属的槽位等于 tag 所在的槽位。其实现方式为在key中加个{},例如test_key{1}。使用hash tag后客户端在计算key的crc16时,只计算{}中数据。如果没使用hash tag,客户端会对整个key进行crc16计算。下面演示下hash tag使用:
    127.0.0.1:6380> cluster keyslot user:case{1}
    (integer) 1024
    127.0.0.1:6380> cluster keyslot user:favor
    (integer) 1023
    127.0.0.1:6380> cluster keyslot user:info{1}
    (integer) 1024

    如上,使用hash tag 后会对应到通一个hash slot:1024中。

哈希槽(slots)的映射

一种是初始化的时候均匀分配 ,使用 cluster create 创建,会将 16384 个slots 平均分配在我们的集群实例上,比如你有n个节点,那每个节点的槽位就是 16384 / n 个了 。 另一种是通过 CLUSTER MEET 命令将 node1、node2、ndoe3、node4 4个节点联通成一个集群,刚联通的时候因为还没分配哈希槽,还是处于offline状态。我们使用 cluster addslots 命令来指定。 指定的好处就是性能好的实例节点可以多分担一些压力。

可以通过 addslots 命令指定哈希槽范围,比如下图中,我们哈希槽是这么分配的:实例 1 管理 0 ~ 7120 哈希槽,实例 2 管理 7121~9945 哈希槽,实例 3 管理 9946 ~ 13005 哈希槽,实例 4 管理 13006 ~ 16383 哈希槽。

redis-cli -h 192.168.0.1 –p 6379 cluster addslots 0,7120
redis-cli -h 192.168.0.2 –p 6379 cluster addslots 7121,9945
redis-cli -h 192.168.0.3 –p 6379 cluster addslots 9946,13005
redis-cli -h 192.168.0.4 –p 6379 cluster addslots 13006,16383

slots 和 Redis 实例之间的映射关系如下: image

数据复制过程和故障转移

1. 数据复制 Cluster 是具备Master 和 Slave模式,Redis 集群中的每个实例节点都负责一些槽位,比如上图中的四个节点分管了不同的槽位区间。而每个Master至少需要一个Slave节点,Slave 节点是通过《Redis系列3:高可用之主从架构》方式同步主节点数据。 节点之间保持TCP通信,当Master发生了宕机, Redis Cluster自动会将对应的Slave节点选为Master,来继续提供服务。与纯主从模式不同的是,主从节点之间并没有读写分离, Slave 只用作 Master 宕机的高可用备份,所以更合理来说应该是主备模式。 如果主节点没有从节点,那么一旦发生故障时,集群将完全处于不可用状态。 但也允许配置 cluster-require-full-coverage 参数,及时部分节点不可用,其他节点正常提供服务,这是为了避免全盘宕机。 主从切换之后,故障恢复的主节点,会转化成新主节点的从节点。这种自愈模式对提高可用性非常有帮助。

2. 故障检测 一个节点认为某个节点宕机不能说明这个节点真的挂起了,无法提供服务了。只有占据多数的实例节点都认为某个节点挂起了,这时候cluster才进行下线和主从切换的工作。 Redis 集群的节点采用 Gossip 协议来广播信息,每个节点都会定期向其他节点发送ping命令,如果接受ping消息的节点在指定时间内没有回复pong,则会认为该节点失联了(PFail),则发送ping的节点就把接受ping的节点标记为主观下线。 如果集群半数以上的主节点都将主节点 xxx 标记为主观下线,则节点 xxx 将被标记为客观下线,然后向整个集群广播,让其它节点也知道该节点已经下线,并立即对下线的节点进行主从切换。

3. 主从故障转移 当一个从节点发现自己正在复制的主节点进入了已下线,则开始对下线主节点进行故障转移,故障转移的步骤如下:

跟哨兵类似,两者都是基于 Raft 算法来实现的,流程如图所示: image

client 访问数据集群的过程

1. 定位数据所在节点 我们前面说过了,Redis 中的每个实例节点会将自己负责的哈希槽信息 通过 Gossip 协议广播给集群中其他的实例,实现了slots分配信息的扩散。这样的话,每个实例都知道整个集群的哈希槽分配情况以及映射信息。

所以客户端想要快捷的连接到服务端,并对某个redis数据进行快捷访问,一般是经过以下步骤:

下图展示了 Redis 客户端如何定位数据所在节点: image

摘自 https://www.cnblogs.com/wzh2010/p/15886799.html 主从可参考: Redis系列3:高可用之主从架构 哨兵可参考: Redis系列4:高可用之Sentinel(哨兵模式)

Bpazy commented 1 year ago

Redis 集群总结