Redis

关于内存与硬盘的特点

硬盘内存

寻址速度

ms级别

ns级别(快了10w倍)

带宽

G/M

很大很大

可持久存储

硬盘的原理:

基础知识:

  1. 扇区,是指磁盘上划分的区域。

  2. 磁道按照512个字节为单位划分为等分,每个等分的弧锻叫做扇区。

  3. 磁盘驱动器在向磁盘读取和写入数据时,要以扇区为单位。

buffer io:

  1. 因为磁盘的读取数据单位只有512byte很小,所以索引数据时的全量索引会造成更多次数的读取操作,所以读取速度过慢

  2. 所以在操作系统读取磁盘时,会增加一个4k大小的buffer io,操作系统每读取一次io,都会至少读取4k的内容

  3. 这样就提高了硬盘的读取速度

数据读取的发展历程

  1. 基于IO的全量读取(比如cat/grep):随着文件内容增大,硬盘的IO将会成为瓶颈,硬盘的读取速度追不上程序的处理速度

  2. 数据库数据的读取:

    1. 数据库将要存储的数据按照4k大小的data page小格子存储,并给data page创建编号

    2. 提供了数据库索引的功能,索引也是data page存储的数据,存储的内容主要是对应索引指向的所有数据的data page编号

    3. 为了避免数据增删改会造成数据移动、甚至是重建数据库索引的情况,创建表时,每个字段的宽度都是要指定的

    4. 在查询时,会将B+T加载到内存中,根据B+T的区间和偏移,确定叶子结点的data page编号,根据编号最后读取数据

      image-20220216232429393
    5. 因为根据数据库索引直接查找到了对应的data page,所以数据库索引会提高查询的速度

  3. 内存数据库

    1. 数据在磁盘和内存的体积不同,相同的数据在内存中的占用往往更小

    2. 因为内存不存在带宽、寻址速度的限制,所以出现了内存数据库

    3. 数据全部存储在内存中,造价昂贵

  4. 内存缓存

    1. 因为内存数据库造价过于昂贵,所以出现了折中方案

    2. 将一部分频繁读取的数据存储到内存中,用作缓存,减轻磁盘数据库的压力

    3. 比如 memcached+oracleredis + mysql

Redis介绍

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。

它支持多种类型的数据结构,如 字符串(strings)散列(hashes)列表(lists)集合(sets)有序集合(sorted sets) 与范围查询, bitmapshyperloglogs地理空间(geospatial) 索引半径查询。

Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。

历史

Redis作者是 Salvatore Sanfilippo,来自意大利的西西里岛。👇

图片

2008年他在开发一个LLOOGG的网站时,需要一个高性能的队列功能,最开始使用MySQL来实现,无奈性能总是提不上去,所以决定自己实现一个专属于LLOOGG的数据库,他就是Redis的前身。后台 Sanfilippoj将 Redis1.0放到Github上大受欢迎。

BSD协议

Redis基于BSD开源协议, BSD开源协议 是一个给于使用者很大自由的协议。可以自由的使用,修改源代码,也可以将修改后的代码作为开源或者专有软件再发布。当你发布使用了BSD协议的代码,或者以BSD协议代码为基础做二次开发自己的产品时,需要满足三个条件:

  • 如果再发布的产品中包含源代码,则在源代码中必须带有原来代码中的BSD协议。

  • 如果再发布的只是二进制类库/软件,则需要在类库/软件的文档和版权声明中包含原来代码中的BSD协议。

  • 不可以用开源代码的作者/机构名字和原来产品的名字做市场推广。

BSD代码鼓励代码共享,但需要尊重代码作者的著作权。BSD由于 允许使用者修改重新发布代码,也 允许使用或在BSD代码上开发商业软件发布和销售,因此是对商业集成很友好的协议。

很多的公司企业在选用开源产品的时候都首选BSD协议,因为可以完全控制这些第三方的代码,在必要的时候可以修改或者 二次开发。

Redis vs Memcache

世界上的数据只有三种表现形式:

  1. k=v

  2. k=[v1, v2,...]

  3. k={x=y, a=[1, 2], b={a,1}}

所以任何数据都可以以json的形式发送或者记录。Memchache是一个 没有数据类型 的缓存系统。它想要表示数据,通常需要客户端自己将数据转换为json。当我需要修改json的某个字段时,需要获取整个字段值,修改完毕后生成新的json,再发送给memcache服务端:

而拥有 数据类型的redis 可以通过数据类型提供的方法,直接修改某部分信息(向list k1头部插入数据):

Redis的优势计算向数据移动

  • 客户端代码更简洁,不需要做数据序列化反序列化处理

  • 网络传输数据较少,客户端和服务端只通讯修改的地方

Redis架构图

Redis是单进程、单线程、单实例处理用户请求的架构,但是依然可以处理高并发下的请求,并且保证单个客户端发送的命令式按照顺序执行的:

image-20220216235124189

redis-cli连接redis

redis-cli 是一个redis的命令行客户端工具,默认集成在redis中,可以使用 redis-cli --help 查看使用帮助,默认链接本地的6379的redis-server服务。

  • 使用交互方式连接redis: redis-cli -h {host} -p {port}

  • 使用命令行方式连接redis并执行命令: redis-cli -h 127.0.0.1 -p 6379 get hello

可以使用 redis-cli --help 来查看帮助。

Redis的DB

Redis默认提供了16个DB库,从 0 ~ 15,默认为0号库。

接连接进入默认0号库:

# redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379>

连接进入指定的9号库:

# redis-cli -h 127.0.0.1 -p 6379 -n 9
127.0.0.1:6379[9]>

在内部切换库12:

127.0.0.1:6379[9]> SELECT 12
OK
127.0.0.1:6379[12]>

help 命令

在redis客户端中,使用help命令,可以查看redis的各种命令帮助:

127.0.0.1:6379> help
redis-cli 6.0.10
To get help about Redis commands type:
      # 列出组下的命令
      "help @<group>" to get a list of commands in <group>
      # 查看某个命令的帮助
      "help <command>" for help on <command>
      # 使用tab智能补全
      "help <tab>" to get a list of possible help topics
      # 使用quit退出redis客户端
      "quit" to exit

To set redis-cli preferences:
      ":set hints" enable online hints
      ":set nohints" disable online hints
Set your preferences in ~/.redisclirc

目前在redis6中的group包含:

- generic # 全局通用命令

# 5大基本类型
- string     # 数据类型string
- list       # 数据类型list
- set        # 数据类型set
- sorted_set # 数据类型sorted_set
- hash       # 数据类型hash

# 其他功能
- pubsub
- transactions
- connection
- server
- scripting
- hyperloglog
- cluster
- geo
- stream

示例,查看基础操作:

127.0.0.1:6379> help @generic

  COPY source destination [DB destination-db] [REPLACE]
  summary: Copy a key
  since: 6.2.0

  DEL key [key ...]
  summary: Delete a key
  since: 1.0.0

...

示例,查看字符串的操作:

127.0.0.1:6379> help @string

  APPEND key value
  summary: Append a value to a key
  since: 2.0.0

  BITCOUNT key [start end]
  summary: Count set bits in a string
  since: 2.6.0

  BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
  summary: Perform arbitrary bitfield integer operations on strings
  since: 3.2.0

示例,查看DEL命令的具体用法:

127.0.0.1:6379> help DEL

  DEL key [key ...]       # 命令模板
  summary: Delete a key   # 作用
  since: 1.0.0            # 起始版本
  group: generic          # 所属组

string

字符串类型,实际存储字节数据,其他数据结构都是基于字符串类型的基础上构建的,其值最大不可以超过512M

针对string,可以将命令分为三种:

  1. 面向字符串的操作

  2. 面向数值的操作

  3. 面向位图的操作

help @string

面向字符串的操作

# 追加字符串
127.0.0.1:6379> APPEND k1 baby
(integer) 6
127.0.0.1:6379> get k1
"hibaby"

# 获取范围的字符串
127.0.0.1:6379> set key1 "hello world"
OK
# 正向索引
127.0.0.1:6379> GETRANGE key1 0 4
"hello"
# 反向索引,-7代表倒数第七个字符
127.0.0.1:6379> GETRANGE key1 0 -7
"hello"

# 设置指定范围内的字符
127.0.0.1:6379> SETRANGE key1 6 zhangsan
(integer) 14
127.0.0.1:6379> get key1
"hello zhangsan"

# 获取字符串的字节数
127.0.0.1:6379> get k1
"hibaby"
127.0.0.1:6379> STRLEN k1
(integer) 6

面向数值的操作

127.0.0.1:6379> set a 100
OK

# +1
127.0.0.1:6379> incr a
(integer) 101

# -1
127.0.0.1:6379> decr a
(integer) 100

# +10
127.0.0.1:6379> incrby a 10
(integer) 110

# -10
127.0.0.1:6379> decrby a 10
(integer) 100

# +0.5
127.0.0.1:6379> incrbyfloat a 0.5
"100.5"

# -0.5
127.0.0.1:6379> incrbyfloat a -0.5
"100"

# + -10
127.0.0.1:6379> incrby a -10
(integer) 90

# - -10
127.0.0.1:6379> decrby a -10
(integer) 100

面向位图的操作

127.0.0.1:6379> setbit bitkey 7 1     --> 设置位图第7位的值为1  0000 0001
(integer) 0
127.0.0.1:6379> getbit bitkey 7       --> 获取位图第7位的值
(integer) 1
127.0.0.1:6379> getbit bitkey 6
(integer) 0

--------> 因为位图最高位为第7位,八位一个字节,所以只占用一个字节
127.0.0.1:6379> strlen bitkey
(integer) 1

--------> 第8位超过了一个字节,所以使用两个字节存储
127.0.0.1:6379> setbit bitkey 8 1
(integer) 0
127.0.0.1:6379> strlen bitkey
(integer) 2

127.0.0.1:6379> bitcount bitkey 0 15     --> 计算第0位到第15位中,所有1的数量
(integer) 2
127.0.0.1:6379> BITPOS bitkey 0 0 15     --> 在第0位到第15位,寻找0位第一次出现的offset
(integer) 0
127.0.0.1:6379> BITPOS bitkey 1 0 15     --> 在第0位到第15位,寻找1位第一次出现的offset
(integer) 7

---------> 位操作
127.0.0.1:6379> setbit a 0 1             --> 10000 0000
(integer) 0
127.0.0.1:6379> setbit a 1 1             --> 11000 0000
(integer) 0
127.0.0.1:6379> setbit b 0 1             --> 10000 0000
(integer) 0
127.0.0.1:6379> setbit b 2 1             --> 10100 0000
(integer) 0
127.0.0.1:6379> bitop and andkey a b     --> 按位与  1000 0000
(integer) 1
127.0.0.1:6379> bitop or orkey a b       --> 按位或  1110 0000
(integer) 1
127.0.0.1:6379> bitop xor xorkey a b     --> 异或    0110 0000
(integer) 1
127.0.0.1:6379> bitop not notkey a       --> 非      0011 1111
(integer) 1

getset

注意,此命令不能保证原子性,因为是先读后写。此方法仅仅是为了减少一次IO操作(与先GET 后 SET 比较):

127.0.0.1:6379> getset key1 abc
"100abc"
127.0.0.1:6379> get key1
"abc"

mset/mget 批量操作

m代表more, 一次设置多个值,一次获取多个值,原子操作

127.0.0.1:6379> mset a 1 b 2 c 3
OK
127.0.0.1:6379> mget a b c
1) "1"
2) "2"
3) "3"

nx/xx

  • nx 表示Not Exist,如果key已经存在,则不会设置

    • 通常用于分布式锁的场景,只允许一个第一个现成取到锁对象

    • set k1 hello nx

    • setnx k1 hello

    • msetnx k1 hello

  • xx 表示 只能更新,如果key不存在,则不会设置

    • set k2 hello xx

list

help @list

压入和弹出

可以实现堆栈、队列

左操作右操作

压入

LPUSH

RPUSH

弹出并获取

LPOP

RPOP

查询List长度:

127.0.0.1:6379> LPUSH abc a b c d e
(integer) 5  # e d c b a 
127.0.0.1:6379> LPOP abc
"e"          # d c b a
127.0.0.1:6379> RPUSH abc x y z
(integer) 7  # d c b a x y z
7.0.0.1:6379> RPOP abc
"z"          # d c b a x y

获取当前list的长度:

127.0.0.1:6379> LLEN abc
(integer) 6

索引操作

可以实现数组

# 获取所有元素
127.0.0.1:6379> LRANGE abc 0 -1
1) "d"
2) "c"
3) "b"
4) "a"
5) "x"
6) "y"

# 获取前两个元素
127.0.0.1:6379> LRANGE abc 0 1
1) "d"
2) "c"

# 设置指定的索引值
127.0.0.1:6379> LSET abc 0 O
OK

# 获取指定索引值
1 27.0.0.1:6379> lindex abc 0
"O"

# 从指定索引处移除指定的所有元素
127.0.0.1:6379> LRANGE abc 0 -1 
1) "O"
2) "c"
3) "b"
4) "a"
5) "x"
6) "y"
127.0.0.1:6379> LREM abc 0 a # 从0索引开始寻找所有的a,并移除所有的a元素
(integer) 1
127.0.0.1:6379> LRANGE abc 0 -1
1) "O"
2) "c"
3) "b"
4) "x"
5) "y"

# LTRIM 移除两端元素
127.0.0.1:6379> RPUSH cde  c d e
(integer) 3
127.0.0.1:6379> LTRIM cde 0 -1  # 删除第一个和最后一个两端的元素,因为两端没有元素了,所以结果不变
OK
127.0.0.1:6379> LRANGE cde 0 -1  
1) "c"
2) "d"
3) "e"
127.0.0.1:6379> LTRIM cde 1 -2  # 删除第二个和倒数第二个两端的元素
OK
127.0.0.1:6379> LRANGE cde 0 -1
1) "d"

# 插入元素,根据元素的位置插入
# 在字母x的前面再插入一个字母b
127.0.0.1:6379> LINSERT abc before x b
(integer) 6
127.0.0.1:6379> LRANGE abc 0 -1
1) "O"
2) "c"
3) "b"
4) "b"
5) "x"
6) "y"
# 再字母x的后面插入一个字母a
127.0.0.1:6379> LINSERT abc after x a
(integer) 7
127.0.0.1:6379> LRANGE abc 0 -1
1) "O"
2) "c"
3) "b"
4) "b"
5) "x"
6) "a"
7) "y"

阻塞的操作

主要用于实现同步单播阻塞队列,即如果不消费客户端将会持续阻塞,且每个元素只能消费一次。

左操作右操作

压入

LPUSH

RPUSH

阻塞弹出并获取

BLPOP

BRPOP

客户端1 使用BROP阻塞读取一个元素:

127.0.0.1:6379> FLUSHALL
OK
127.0.0.1:6379> BRPOP abc 0   # 数字0代表操作没有超时时间,会一直等待
... # 这里一直阻塞

客户端2 使用RPUSH向队列中推入一个元素:

127.0.0.1:6379> LPUSH abc a
(integer) 1

客户端发现有数据,停止阻塞:

127.0.0.1:6379> BRPOP abc 0
1) "abc"
2) "a"
(96.58s)

hash

一种键值对的value类型,通常用来存储对象。

127.0.0.1:6379> help @hash

存储/操作一个对象数据

定义zhangsan这个对象的信息:

# 定义hash zhangsan,添加一个name属性
127.0.0.1:6379> hset zhangsan name zhangsan
(integer) 1

# 给zhangsan添加两个属性age和address
127.0.0.1:6379> hmset zhangsan age 18 address bj
OK

查看zhangsan的信息:

# 查看zhangsan的所有信息
127.0.0.1:6379> hgetall zhangsan
1) "name"
2) "zhangsan"
3) "age"
4) "18"
5) "address"
6) "bj"

# 查看张三的所有属性名
127.0.0.1:6379> hkeys zhangsan
1) "name"
2) "age"
3) "address"

# 查看张三所有属性的值
127.0.0.1:6379> hvals zhangsan
1) "zhangsan"
2) "18"
3) "bj"

可针对数值进行计算

# 让zhangsan的年龄增加0.5岁
127.0.0.1:6379> HINCRBYFLOAT zhangsan age 0.5
"18.5"

# 让zhangsan的年龄减1岁
127.0.0.1:6379> HINCRBYFLOAT zhangsan age -1
"17.5"

set

set也用于存储元素,相比于list:

  1. list按照插入顺序进行排序,set则是无序的(以内部一种有序的方式实现无序)

  2. list维护索引,set则不维护

  3. list中可以有重复元素,set中的元素都是唯一的

set的命令都是以S开头的:

127.0.0.1:6379> help @set

存储/删除/查询操作

# 添加
127.0.0.1:6379> sadd abc  a b c d e a
(integer) 5

# 查看所有元素
127.0.0.1:6379> SMEMBERS abc
1) "c"
2) "b"
3) "d"
4) "a"
5) "e"

# 移除元素 a和b
127.0.0.1:6379> SREM abc a b
(integer) 2

交集/并集/差集操作

127.0.0.1:6379> sadd k1 1 2 3 4 5
(integer) 5
127.0.0.1:6379> sadd k2 4 5 6 7 8
(integer) 5

###### 交集
# 计算k1和k2的交集并输出
127.0.0.1:6379> SINTER k1 k2
1) "4"
2) "5"

# 计算k1和k2的交集并输出到另一个key k3中
127.0.0.1:6379> SINTERSTORE k3 k1 k2
(integer) 2
127.0.0.1:6379> SMEMBERS k3
1) "4"
2) "5"

###### 并集
127.0.0.1:6379> SUNION k1 k2
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"

####### 差集
# 左差集,以左为准
127.0.0.1:6379> SDIFF k1 k2
1) "1"
2) "2"
3) "3"
# 右差集,以右为准
127.0.0.1:6379> SDIFF k2 k1
1) "6"
2) "7"
3) "8"

随机事件

SRANDMEMBER key count
# count为正数,取出一个去重的随机结果集,由于去重,返回的结果集长度有可能小于count
# count为负数,取出一个长度为count的带重复的随机结果集,返回结果集的长度一定满足你要的数量


127.0.0.1:6379> SADD k1 a b c d e
(integer) 5

# 随机2个元素,不可重复
127.0.0.1:6379> SRANDMEMBER k1 2
1) "a"
2) "e

# 随机10个元素,不可重复
127.0.0.1:6379> SRANDMEMBER k1 10
1) "d"
2) "a"
3) "b"
4) "c"
5) "e"

# 随机2个元素,可重复
127.0.0.1:6379> SRANDMEMBER k1 -2
1) "d"
2) "a"

# 随机10个元素,可重复
127.0.0.1:6379> SRANDMEMBER k1 -10
 1) "c"
 2) "d"
 3) "c"
 4) "a"
 5) "b"
 6) "b"
 7) "d"
 8) "d"
 9) "c"
10) "c"

# 弹出一个随机元素
127.0.0.1:6379> SPOP k1
"e"

sorted_set

是一个有序的set,不同于基于放入顺序的有序的list,而是基于元素的一些属性进行排序。

127.0.0.1:6379> help @sorted_set

所有的sorted_set操作都已z开头,故sorted_set又称作zset。

存储元素时要指定socre,用于排序

在添加元素时,需要指定元素的score,按照score值左小右大的方式,对元素进行排序:

# 添加3种水果,以他们的糖分含量为分值
127.0.0.1:6379> zadd k1 8 apple 2 banana 3 orange
(integer) 3

# 可以看到分值较低的排在上面
127.0.0.1:6379> ZRANGE k1 0 -1
1) "banana"
2) "orange"
3) "apple"

取出元素

##### 根据sore的分值取出元素
127.0.0.1:6379> ZRANGEBYSCORE k1 3 10
1) "orange"
2) "apple"

##### 取出糖分最低的两种水果
127.0.0.1:6379> ZRange k1 0 1
1) "banana"
2) "orange"

###### 取出糖分最高的两种水果
127.0.0.1:6379> ZREVRANGE k1 0 1
1) "apple"
2) "orange"

###### 取出元素的分值
127.0.0.1:6379> ZSCORE k1 orange
"3"

###### 取出所有元素以及他的分值
127.0.0.1:6379> ZRANGE k1 0 -1 withscores
1) "banana"
2) "2"
3) "orange"
4) "3"
5) "apple"
6) "8"

###### 取出元素的分值排名
127.0.0.1:6379> ZRANK k1 orange
(integer) 1

修改元素分值

# 修改香蕉的分值
127.0.0.1:6379> ZINCRBY  k1 2.5 banana
"4.5"

127.0.0.1:6379> ZRANGE k1 0 -1 withscores
1) "orange"
2) "3"
3) "banana"
4) "4.5"
5) "apple"
6) "8"

集合操作

注意权重

127.0.0.1:6379> zadd k1 80 tom 60 sean 70 baby
(integer) 3
127.0.0.1:6379> zadd k2 60 tom 100 sean 40 jack
(integer) 3

# 并集
# ZUNION numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX] [WITHSCORES]
# ZUNIONSTORE destination numkeys key [key ...] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]

# 将k1、k2两个zset做集合并集操作,可以看到默认会将两个分值相加
127.0.0.1:6379> ZUNION 2 k1 k2  withscores
1) "jack"
2) "40"
3) "baby"
4) "70"
5) "tom"
6) "140"
7) "sean"
8) "160"

# 将k1、k2两个zset做集合的并集操作,并指定weight
# 加了权重的结果为 k1_weight * k1_score + k2_weight * k2_score
127.0.0.1:6379> ZUNION 2 k1 k2  weights 1 0.5 withscores
1) "jack"
2) "20"   = 0 + 0.5 * 40 
3) "baby"
4) "70"   = 70 + 0
5) "sean"
6) "110"  = 70 + 40/20
7) "tom"
8) "110"  = 60 + 100/2 


# 改变默认合并分值行为
# 默认为sum相加,可以以最大值为准
127.0.0.1:6379> ZUNION 2 k1 k2  AGGREGATE MAX  withscores
1) "jack"
2) "40"
3) "baby"
4) "70"
5) "tom"
6) "80"
7) "sean"
8) "100"

type与encoding

Redis的每个value都有其对应的类型,可以使用如下方式查看value的数据类型type:

127.0.0.1:6379> type k1
string

类型与操作方法时对应的,指定的类型只能进行指定的方法操作。

此外,每种type内部都有自己多种编码实现,比如string类型内部就有三种编码实现:

  • raw

  • int

  • embstr

二进制安全

在redis中,所有数据都是二进制安全的。redis只会保存原始的二进制流数据,不会对他们做处理。即使value的内部编码不同,也不会影响数据数据在redis内部的存储方式,所谓的内部编码只不过在对数据处理时,将其转换为其他编码以方便处理。

所以,redis存储的数据的格式、文本编码等信息,需要客户端自己在存入和读取时保持一致。

127.0.0.1:6379> strlen key1   // int 100
(integer) 3
127.0.0.1:6379> append key1 abc
(integer) 6
127.0.0.1:6379> get key1
"100abc"
127.0.0.1:6379> object encoding key1
"raw"

为什么需要encoding

  1. 这样可以自由改进每种类型的内部编码,从而不影响外部数据结构以及命令

  2. 多种内部编码可以在不同的场景下发挥各自的优势,比如string的int编码就可以很方便的实现INCR命令

查看内部编码

127.0.0.1:6379> object encoding key1
"embstr"
127.0.0.1:6379> object encoding key2
"int"

string

  • int:8字节长整型

  • embstr:小于等于39字节的字符串

  • raw:大于39字节的字符串

127.0.0.1:6379> set key1 100
OK
127.0.0.1:6379> set key2 hello
OK
127.0.0.1:6379> set key3 a12345678901234567890123456789012345678901234567890

127.0.0.1:6379> object encoding key1
"int"
127.0.0.1:6379> object encoding key2
"embstr"
127.0.0.1:6379> object encoding key3
"raw"

Pipelining 管道

redis的通信是建立在tcp基础上的,也就是说每一次命令(get、set)都需要经过tcp三次握手,而且redis一般都是部署在局域网内,网络开销非常小,针对频次较低的操作,网络开销都是可以忽略的。

每一次操作redis的时候我们都需要和服务端建立连接,针对量小的情况下网络延迟都是可以忽略的,但是针对大批量的业务,就会产生雪崩效应。假如一次操作耗时2ms,理论上100万次操作就会有2ms*100万ms延迟,中间加上服务器处理开销,耗时可能更多.对应客户端来讲,这种长时间的耗时是不能接受的。所以为了解决这个问题,redis的管道pipeline就派上用场了。

下面是一个Redis管道的示例:

# nc 会建立一个TCP长链接
printf "PING\r\nPING\r\nPING\r\n"; sleep 1 | nc localhost 6379
PING
PING
PING

使用管道建立了一次TCP连接,一次连接发送了3个PING命令,并一次全部响应。

Pub/Sub 发布订阅

help @pubsub

服务端先发布,客户端后订阅

发布方先发布:

127.0.0.1:6379> PUBLISH c1 hi
(integer) 0

订阅方后订阅:

127.0.0.1:6379> SUBSCRIBE c1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "c1"
3) (integer) 1

结果:客户端收不到订阅前的消息

客户端先订阅,服务端后发布

订阅方订阅:

127.0.0.1:6379> SUBSCRIBE c1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "c1"
3) (integer) 1

发布方发布:

127.0.0.1:6379> PUBLISH c1 hello
(integer) 1

订阅方:

1) "message"
2) "c1"
3) "hello"

结果:可正常接受实时消息

怎么看订阅之前的消息

  1. pub/sub只能保证实时消息下发到订阅端

  2. 怎么保证可查看历史性消息?

    1. 历史性消息需要持久化,可以将消息存储到磁盘数据库中

    2. 历史消息根据查询的压力可以分为两个档次

      1. 比如三天内的消息,查询压力可能较高

      2. 三天前、几周前、或者更早的消息,基本没有什么压力

    3. 根据查询压力,可以将最近三天的消息窗口缓存到sorted_set中,减轻缓存压力

    4. 将更早的消息直接放入数据库中

事务

127.0.0.1:6379> help @transactions
  1. 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。

  2. Redis是单进程的,事务中的命令无论什么情况都会连续按照顺序执行,不会被其他客户端发送来的命令请求所打断。

  3. 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。

  4. 事务下包含如下几个命令:

    MULTI     开启事务
    EXEC      执行客户端发送的所有命令,将事务要执行的命令连续按照顺序放到命令执行队列中
    DISCARD   取消事务
    WATCH     当前客户端监控某个key是否发生了变化,如果发生了变化,事务则不会执行

事务示例

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> set k1 aaa
QUEUED                         # 命令放入到缓存队列中了
127.0.0.1:6379(TX)> set k2 bbb
QUEUED
127.0.0.1:6379(TX)> exec       # 真正执行命令
1) OK
2) OK

watch示例

127.0.0.1:6379> watch k1    # 监控k1
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> get k1
QUEUED
127.0.0.1:6379(TX)> exec    # 执行命令,这时会检查watch时k1的值与当前k1的值是否相等
(nil)                       # 如果不相等,也就是k1在事务执行的这段时间发生了变化,就会返回nil,代表事务没有执行

在上一客户端执行watch后,另一个客户端修改了k1,造成了k1的值发生变化:

127.0.0.1:6379> set k1 aaaa
OK

执行失败事务不会回滚,而是会继续执行

  1. Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。

  2. 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。

作为缓存应用

redis作为数据库与缓存的区别

  1. 缓存不存放全量数据,只存储热点数据

  2. 业务逻辑会决定缓存存在的有效期

  3. 内存有限,随着时间变化,冷数据应该被淘汰

配置key的缓存时间

Redis作为缓存系统,其最重要的特点之一就是其缓存的失效系统,redis的key可以通过如下几条命令设置有效期:

# 永不失效
set key1 value1

# 一秒钟后失效
set key1 value1 EX 1

# 设置k1=v1 有效期10s
setex k1 10 v1

# 查看指定的键的剩余生存时间 单位s
127.0.0.1:6379> ttl  k1
(integer) 5

127.0.0.1:6379> set kk a
OK

# key不存在返回-2
127.0.0.1:6379> ttl k
(integer) -2

# 未设置缓存时间返回-1
127.0.0.1:6379> ttl kk
(integer) -1

当一些客户端尝试访问它时,key会被发现并主动的过期。

当然,这样是不够的,因为有些过期的keys,永远不会访问他们。 无论如何,这些keys应该过期,所以定时随机测试设置keys的过期时间。所有这些过期的keys将会从密钥空间删除。

  1. 主动:

  2. 被动:

配置冷数据淘汰策略

可修改redis的配置文件,配置如下项目:

# 指定redis进程最大使用的内存,一般设置为1G~10G之间
maxmemory <bytes>
# 当maxmemory限制达到的时候Redis会使用的行为由 Redis的maxmemory-policy配置指令来进行配置 
maxmemory-policy noeviction

共有以下几种值:

  1. noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL和几个例外)

    数据不可丢时,将redis作为数据库时使用次策略

  2. allkeys-lru(常用):尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。

    无论该数据有效期是否过期,都会去回收

  3. volatile-lru(常用):尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。

    只回收设置了有效期的key

  4. allkeys-random:回收随机的键使得新添加的数据有空间存放。

  5. volatile-random:回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。

  6. volatile-ttl:回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。

LRU是一种缓存淘汰算法,全拼为Least recently used,最近最少使用算法

LFU也是一种缓存淘汰算法,全拼为Least frequently used,最不常用算法,在一段时间内很久没有发生操作

TTL是 Time To Live的缩写,代表生存时间值

如何淘汰过期的key?

Redis keys过期有两种方式:

  1. 被动方式:客户端访问key时,被动的发现key过期,并处理

  2. 主动方式:周期轮训判定,每十秒运行一次

    1. 测试随机的20个keys进行相关过期检测。

    2. 删除所有已经过期的keys。

    3. 如果有多于25%的keys过期,重复步奏1.

应用场景

实现分布式锁

使用setnx

统计指定时间段内的用户登录次数

  • 使用string 位图实现

  • key为用户唯一标志

  • 使用365位存储365天内的登录状态

  • 使用bitcount统计用户在某一随机时间段内的登录次数

  • 使用bitlen统计总共登录的次数

  • 运算速度快、节省空间

统计最近的活跃用户(排除僵尸用户)

  • 使用string位图实现

  • key为日期

  • value的每一位代表用户的登录状态,1代表已经登录

  • 将要统计的日期的所有位图相或,可以得出这几个日期中所有至少登录一次的用户结果

  • 将要统计的日期的所有位图相与,可以得出这几个日期中每天都登陆的用户结果

统计点赞、收藏、访问量

  • 使用Hash实现

  • key为要统计的目标页面、目标文章等

  • value则记录该篇文章的浏览量、点赞量等

抽奖:选取幸运粉丝,赠送奖品

给我的微博的所有粉丝中抽取几名幸运粉丝赠送奖品。

  1. 将所有的粉丝增加到set中

  2. 使用SRANDMEMBER fans 奖品数量,假设奖品数量为3

  3. 如果一个粉丝只能获得一次奖励:SRANDMEMBER fans 3

  4. 如果一个粉丝可以重复获得奖励:SRANDMEMBER fans -3

抽奖:年会抽奖

  1. 奖项数 < 参与抽奖的人数

  2. 如果某个奖项已经被抽取了,那么这个奖项则不在抽奖列表内

将所有的奖品添加到set中,使用SPOP指令,每次弹出一个随机元素

员工是否可以重复抽奖,是否可以重复中奖,可以由应用程序控制。

抽奖:奖品多,人少

  1. 将所有的抽奖人员放入到set中

  2. 抽取10个奖品的归属人:SRANDMEMBER fans -10

直播间消息推送

使用pub/sub,参考PubSub章节。

实现CAS锁

使用事物的watch机制,配合代码循环设置分布式CAS锁。

常见缓存问题解决方案

缓存穿透

用户/黑客,频繁查询一个不存在的商品,导致每次缓存都不命中,导致每次查询都到达数据库层,然后拖垮数据库。

解决方案:记录所有的已有商品,拒绝无效查询

将所有的可命中的商品查询存储到缓存中,如果用户输入的条目无法命中,则不走数据库。

但是因为可查询的目标条目较多,内存缓存无法全部存储,这时候就需要布隆过滤器。

解决方案:布隆过滤器

image-20220217211425853
  1. 将已有的商品标记到bitmap中 => 1

  2. 没有标记的商品在bitmap中没有标记 => 0

  3. 因为有可能发生hash冲突,请求的商品可能被误标记

  4. 但是,一定概率会大量减少

  5. 所以布隆过滤器又被称为概率数据结构

布隆过滤器可以放在:

  1. client端实现bloom算法,自己承载bitmap

  2. client端实现bloom算法,bitmap交给redis

  3. client端什么都不做,将bloom算法与bitmap统一交给redis处理,涉及redis模块:bloom

docker下启动含有布隆过滤器模块的Redis:

docker run --name redis-redisbloom --rm redislabs/rebloom:latest

使用redis bloom:

# 客户端添加已有商品 a、b
127.0.0.1:6379> BF.ADD product a
(integer) 1
127.0.0.1:6379> BF.ADD product b
(integer) 1

# 用户再查询时,先去布隆过滤器中判断商品是否存在
# 商品存在,调用缓存,如果缓存不存在,则调用数据库,并缓存到缓存中
127.0.0.1:6379> BF.EXISTS product a
(integer) 1

# 商品不存在,避免走数据库
127.0.0.1:6379> BF.EXISTS product c
(integer) 0

解决方案:counting bloom 布隆过滤器升级

解决方案:布谷鸟过滤器

同布隆过滤器类似。

缓存击穿

同一时间内同时高并发请求一个缓存过期的数据,导致大量请求同一时间到达数据库。这成为缓存击穿。

解决方案:使用缓存锁

  1. 当目标缓存没有命中时,先使用setnx获取一个分布式锁,然后再去数据库中查询

  2. 当同一时刻的请求也命中了该缓存,也会去尝试获得对应的分布式锁,此时他会获取失败

  3. 这样就避免了大量的请求同时到达数据中

  4. 当请求未获得缓存锁时,可以每隔一段时间(比如1s)重新获得锁,直到获取成功,但是有如下几个问题:

    1. 问题1:第一个持有锁的人挂掉,锁没有被释放

      解决: 设置锁过期时间

    2. 问题2:数据库访问时间较长或者锁时间设置过短,导致数据库执行了但是缓存设置没有成功,所有请求都失败

      解决:使用两个线程,A线程从DB中取数据,B线程监控数据是否取出,然后重新设置锁的时间

缓存雪崩

大量的key同时失效,间接造成大量的访问到达数据库。

解决方案:时点性无关-随机过期时间

没有可以要求数据的缓存时间,可以对key的缓存时间进行随机。

解决方案:时点性有关-使用击穿方案

一些系统缓存一定要在指定的时间失效并且更新,比如银行的信贷信息一定要凌晨12点更新,否则就是过期数据。使用击穿方案解决。

Redis的持久化

持久化一半都分为两种:

  • 定时全量备份

    • 优点:恢复速度快

    • 缺点:会导致某部分时间的数据丢失

  • 实时日志

    • 优点:不会丢失数据,数据会被实时备份

    • 缺点:恢复速度较慢

RDB(快照)

快照、副本这种持久化是有时点性的:缓存备份的过程是需要一段时间的,磁盘中的文件在这段时间会有变化。所以就会导致副本前部分备份的数据是20分钟前的数据,而后部分是最近时间的数据,数据时点混乱。

Redis提供了两种解决方案:

  1. SAVE:阻塞redis,暂时不提供服务,直到备份完毕才恢复(不推荐)

  2. BGSAVE:创建子进程用于备份redis数据,根据linux父子进程特性,进程间的的数据时隔离的,子进程被创建后会拷贝所有父进程数据到子进程。这样就避免了数据时点混乱的问题。

    因为将数据完全拷贝,会占用双倍的内存空间,并且要花费很长时间拷贝,所以Redis采用系统调用fork

    1. fork会创建一个子进程

    2. 创建子进程时,不会立即拷贝父进程的数据,而是采用copy on write的方式拷贝,也就是说如果用到了这个数据才会进行拷贝

    3. 他不会真正将所有数据拷贝一份,只是使用虚拟地址去引用真实的物理内存地址

主动触发RDB

主动触发通常用语关机维护前的备份

save  # Redis Save 命令执行一个同步保存操作,将当前 Redis 实例的所有数据快照(snapshot)以 RDB 文件的形式保存到硬盘。
bgsave # BGSAVE 命令执行之后立即返回 OK ,然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。

使用配置文件自动配置

实际是bgsave的备份方式:

# save 时间 操作数
# 当时间超过900s或者发生了1个操作数,就备份
save 900 1
# 当时间超过300s或者发生了10个操作数,就备份
save 300 10
# 当时间超过60s或者发生了10000个操作数,就备份
save 60 10000

# 指定rdb备份的备份文件名称
# 不支持拉链,永远只会生成一个dump.db文件,只会覆盖,不会留存每次备份的数据
dbfilename dump.rdb

# 指定rdb备份的备份目录
dir /var/lib/redis/6379

恢复备份

将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可。

AOF(日志)

Append Only File,他会将redis的写操作记录到文件中。每次写操作都会触发。

RDB 和 AOF同时开启

RDB和AOF可以同时开启,但是只会使用AOF。

怎么保证AOF日志最小

时间越久,AOF日志就会越大,所以Redis在不同的版本提供了不同的日志重写功能:

  1. 在Redis4.0前:删除抵消的命令日志、合并重复的命令日志,最终会获得一个纯指令的日志文件

  2. 在Redis4.0后:先将老数据RDB到AOF文件中,将增量的指令append到AOF文件中(更快更轻便)

配置AOF

# 默认关闭aof
appendonly no

开启aof

appendonly yes
appendfilename  "appendonly.aof"
# 指定aof的备份文件位置
dir /var/lib/redis/6379

# 配置aof级别
appendfsync always   # 每次写操作都调用fsync flush数据到磁盘,性能最差,但是数据完整度最高
appendfsync everysec # 每隔几秒flush到磁盘
appendfsync no       # write后不会有fsync调用,由操作系统自动调度刷磁盘,性能最好,但是数据完整度最差

# 是否使用4.0后,rdb与aof混合的方式,默认开启,推荐开启
aof-use-rdb-preamble yes

# 配置什么时候触发重写
# redis会记录重写后的aof文件大小,然后重新记录,当再次增加64m * 1时,将会再次触发重写
auto-aof-rewrite-percentage 100  
auto-aof-rewrite-min-size   64mb # 如果备份文件超过64m就重写

分布式的Redis

主从复制

Redis的主从复制是标准的主从复制方式,一个主节点对应多个从节点,从节点的数据完全备份自主节点,主节点负责写入数据,而从节点只提供读服务,负责减轻读取的压力。

主从复制不提供高可用的方案,高可用方案由哨兵提供

配置主从复制

修改三台机器的配置文件中的如下配置选项:

# 设置为后台运行
daemonize yes
# 保存pid的文件,如果是在一台机器搭建主从,需要区分一下
pidfile /var/run/redis_6379.pid
# 绑定的主机地址,这里注释掉,开放ip连接
#bind 127.0.0.1
# 指定日志文件
logfile "6379.log"

另外在redis服务器上配置如下选项,表示跟随的主节点:

# 5.0版本及以后:
replicaof <masterport> <masterport>
# 5.0版本以前
slaveof <masterport> <masterport>

这里可以使用命令行配置,但是配置之后重启会失效

启动3台redis服务,可以使用info replication查看主从复制的信息:

192.168.249.20:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=192.168.249.22,port=6379,state=online,offset=700,lag=0
slave1:ip=192.168.249.21,port=6379,state=online,offset=700,lag=0
master_replid:b80a4720c0001efb62940f5ad6abaf9cdaf7a813
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:700
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:700

192.168.249.21:6379> info replication
# Replication
role:slave
master_host:192.168.249.20
master_port:6379
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_repl_offset:854
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:b80a4720c0001efb62940f5ad6abaf9cdaf7a813
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:854
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:57
repl_backlog_histlen:798

192.168.249.22:6379> info replication
# Replication
role:slave
master_host:192.168.249.20
master_port:6379
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:854
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:b80a4720c0001efb62940f5ad6abaf9cdaf7a813
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:854
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:854

验证主从复制同步

192.168.249.20:6379> set test 'Hello World'
OK

192.168.249.21:6379> get test
"Hello World"

192.168.249.22:6379> get test
"Hello World"

从服务器不能更新值,只能读取

192.168.249.21:6379> set test2 hello
(error) READONLY You can't write against a read only replica.

实现原理

  1. 建立连接

    1. slaveof命令执行之后,从服务器根据设置的master的ip地址和端口,创建连向主服务器的socket套接字连接,连接成功后,从服务器会为这个套接字关联一个专门的处理器,用于处理后续的复制工作

    2. 建立连接之后,从服务器会向主服务器发送ping命令,确认主服务器是否可用,以及当前是否可用接受处理命令。如果收到主服务器的pong回复说明是可用的,否则有可能是网络超时或主服务器阻塞,从服务器会断开连接发起重连

    3. 身份验证。如果主服务器设置了requirepass选项,那么从服务器必须配置masterauth选项,且保证密码一致才能通过验证

    4. 身份验证完成之后,从服务器会发送自己的监听端口,主服务器会保存下来

    5. 建立连接后可以通过如下命令查看redis集群的状态

      192.168.249.20:6379> info replication
      ...
      slave0:ip=192.168.249.22,port=6379,state=online,offset=700,lag=0
      slave1:ip=192.168.249.21,port=6379,state=online,offset=700,lag=0
      ...
  2. 数据同步

    1. 在主从服务器建立连接确认各自身份之后,就开始数据同步,从服务器向主服务器发送PSYNC命令,执行同步操作,并把自己的数据库状态更新至主服务器的数据库状态

    2. 当slave第一此链接到master上时,会触发完全重同步(runid代表master的节点id;offset代表将发送快照数据的偏移量,记录这个偏移量是因为执行完全重同步后会根据偏移量发送剩余的redis命令)

      img
    3. 当slaver与master断线后重新连接,将会执行部分重同步,重新同步失联情况缺失的数据(runid代表master的节点id,不是同一个id不能重新连接;offset代表从服务器上次同步的数据位置;复制积压缓冲区是一个默认1MB大小的FIFO队列,主要用于备份主库发送给从库的数据,然后异步发送给从库,在主库中只有一个复制积压缓冲器,所有从库共享他们;)

      img
  3. 命令传播:

    1. 当完成数据同步之后,主从服务器的数据暂时达到一致状态,当主服务器执行了客户端的写命令之后,主从的数据便不再一致

    2. 此时为了能够使主从服务器的数据保持一致性,主服务器会对从服务器执行命令传播操作,即每执行一个写命令就会向从服务器发送同样的写命令

    3. 在命令传播阶段,从服务器会默认以每秒一次的频率向主服务器发送心跳检测 :

      1. 从服务器发送命令REPLCONF ACK <replication_offset> 进行检测

      2. 检测主从服务器的网络连接状态

      3. 传入replication_offset,防止部分数据丢失

Redis的主从复制并不能保证一致性

  1. 假如master新写入一条数据,此时可能还未出发命令传播,所以说Redis无法保证实时一致性

  2. 假如master新写入一个数据,但是命令传播还未发出,这时master已经挂了,那么该条数据永远不会同步给slaver,所以说Redis主从复制连最终一致性都无法完全保证

  3. 完全重同步采用的是异步队列的方式,如果积压缓冲区中的数据还未发送给slaver,此时master就挂掉了,那么这里的数据就可能发生丢失

主从复制方案:哨兵

主从复制默认不支持高可用,redis提供了sentinel哨兵

  • 监控,监控主节点的健康情况

  • 提醒,当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知

  • 自动故障转移,哨兵通过选举重新设置一个主节点

配置哨兵

配置三个哨兵的配置文件,监控主节点 127.0.0.1:6379

# 哨兵程序端口
port 26379
# 指定监控的master节点,投票的权重值 2 ,达到两票就投票成功
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
# 哨兵程序端口
port 26380
# 指定监控的master节点,投票的权重值 2 ,达到两票就投票成功
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1
# 哨兵程序端口
port 26381
# 指定监控的master节点,投票的权重值 2 ,达到两票就投票成功
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 60000
sentinel failover-timeout mymaster 180000
sentinel parallel-syncs mymaster 1

启动这三个哨兵:

redis-server ./26379.conf --sentinal
redis-server ./26380.conf --sentinal
redis-server ./26381.conf --sentinal

或者使用:

redis-sentinal ./26379.conf 
redis-sentinal ./26380.conf 
redis-sentinal ./26381.conf

特点

  1. 哨兵是单独的进程,不同于server

  2. 哨兵只连接master

  3. 哨兵可以根据master获取master的所有slave信息

  4. 哨兵可以根据master发现其他的哨兵(通过master发布订阅的方式)

哨兵是怎么选举新的master上的

Redis中哨兵选举算法 锦鱼不忘旧时晨的博客-CSDN博客 redis哨兵选举算法

容量限制的问题

无论是主从复制还是哨兵,都是全量备份的,无法解决容量限制的问题。解决容量限制有如下几个方式:

按照业务划分

在客户端按照业务划分,将数据存储到不同的redis中。比如将redis拆分为 user、order等多个redis,每个业务使用对应的redis。

image-20220219174435108

使用随机算法划分

存储kv到某个随机redis中,他的缺点是客户端无法确定key在哪个redis中。通常用于消息队列的场景:

  1. 提供方使用lpush向随机redis添加kv

  2. 消费方连接所有redis,使用rpop消费list,不管队列是哪个redis

image-20220219174235284

使用hash+取模的方式划分

在客户端按照算法拆分,根据hash+取模的方式将数据分在不同的redis中,也就是基于取模的数据分片:

image-20220219174329787

缺点是模的值必须是固定的,如果发生了变化,数据落地的目标redis就有可能发生变化,造成数据混乱。

使用一致性hash算法划分

image-20220219174900099

Hash环:

img

Hash环有2次Hash:

  1. 把所有机器编号hash到这个环上

  2. 把key也hash到这个环上。然后在这个环上进行匹配,看这个key和哪台机器匹配。

首先计算出每台Cache服务器在环上的位置(图中的大圆圈);然后每来一个(key, value),计算出在环上的位置(图中的小圆圈),然后顺时针走,遇到的第1个机器,就是其要存储的机器。

这里的关键点是:当你增加/减少机器时,其他机器在环上的位置并不会发生改变。这样只有增加的那台机器、或者减少的那台机器附近的数据会失效,其他机器上的数据都还是有效的。

哈希环数据倾斜的问题

当你机器不多的时候,很可能出现几台机器在环上面贴的很近,不是在环上均匀分布。这将会导致大部分数据,都会集中在某1台机器上。

为了解决这个问题,可以引入“虚拟机器”的概念,也就是说:1台机器,我在环上面计算出多个位置。怎么弄呢? 假设用机器的ip来hash,我可以在ip后面加上几个编号, ip_1, ip_2, ip_3, … 把1台物理机器生个多个虚拟机器的编号。

数据首先映射到“虚拟机器上”,再从“虚拟机器”映射到物理机器上。因为虚拟机器可以很多,在环上面均匀分布,从而保证数据均匀分布到物理机器上面。

使用预分区(哈希槽)划分

前面说的方法都有一些问题,比如hash环会丢失一部分数据,所以使用预分区划分。

将所有的hash定义为槽位,假设定义10个槽位,分配各两台redis:

image-20220219184224945

现在加入第三台redis3,redis1和redis2会均分给redis3一部分槽位:

image-20220219184733713

需要部分迁移,但是相比全部迁移与丢失数据相比,已经很完善了。

连接成本高的问题

如果按照客户端拆分的方式,redis客户端去连接每个redis服务端,那么就会有如下的拓扑结构,将会增加redis的连接成本。

image-20220219180825239

设置负载均衡与代理

image-20220219181239500

代理压力大可以增加LVS

LVS是四层处理,不会握手,压力更小

image-20220219182441213

第三方redis代理工具

image-20220219192327837

分区方案:Redis Cluster

Redis 集群是一个提供在多个Redis间节点间共享数据的程序集。Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令. Redis 集群的优势:

  • 自动分割数据到不同的节点上。

  • 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。

解决容量限制的问题

采用预分区,默认提供了16384个hash槽。

解决连接成本较高的问题

image-20220219190739435

客户端在连接redis集群时,只需要连接集群内的任意一个节点。当发生操作时,连接的目标节点会自动计算目标key的所属节点位置,并重定向到目标节点。

这样就解决了连接成本高的问题。

故障转移

Redis集群的主节点内置了类似Redis Sentinel的节点故障检测和自动故障转移功能,当集群中的某个主节点下线时,集群中的其他在线主节点会注意到这一点,并对已下线的主节点进行故障转移。

img

集群进行故障转移的方法和Redis Sentinel进行故障转移的方法基本一样,不同的是,在集群里面,故障转移是由集群中其他在线的主节点负责进行的,所以集群不必另外使用Redis Sentinel。

缺点:不支持跨节点聚合、事务操作

Redis集群会直接拒绝不同节点的聚合、事务操作,因为会发生数据复制。

解决办法:可以通过hash tag,让需要进行聚合操作或者事务的key放在同一个集群节点上即可完成。

创建redis-cluster

最小集群大小为6个节点,其中有3个主节点,3个从节点:

img

三个从节点主要是对主节点的备份与对读操作的扩展,三个主节点主要是对数据进行分区。

最后更新于