sheng的学习笔记-redis框架原理

摘要:redis命令,使用场景,持久化,缓存穿透,缓存雪崩,缓存击穿,持久化(RDB,AOF),事务,锁,集群,主从复制原理,哨兵模式

目录


基础知识

官网:

中文官网:redis中文官方网站​​​​​​

英文官网:https://redis.io/

简介:

Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。 

安装:

Windows 下安装

下载地址:https://github.com/tporadowski/redis/releases。

Redis 支持 32 位和 64 位。这个需要根据你系统平台的实际情况选择,这里我们下载 Redis-x64-xxx.zip

sheng的学习笔记-redis框架原理_第1张图片

打开一个 cmd 窗口 使用 cd 命令切换目录到安装目录(或者双击exe文件也行)

redis-server.exe redis.windows.conf

 sheng的学习笔记-redis框架原理_第2张图片

这时候另启一个 cmd 窗口,原来的不要关闭,不然就无法访问服务端了。

切换到 redis 目录下运行:

redis-cli.exe -h 127.0.0.1 -p 6379

 使用ping命令,如果看到结果是pong,说明测试连接成功

sheng的学习笔记-redis框架原理_第3张图片

试一下set和get命令,正常sheng的学习笔记-redis框架原理_第4张图片

 搭建完成

Linux 源码安装

下载地址:Redis,下载最新稳定版本。

本教程使用的最新文档版本为 2.8.17,下载并安装:

# wget http://download.redis.io/releases/redis-6.0.8.tar.gz
# tar xzf redis-6.0.8.tar.gz
# cd redis-6.0.8
# make

执行完 make 命令后,redis-6.0.8 的 src 目录下会出现编译后的 redis 服务程序 redis-server,还有用于测试的客户端程序 redis-cli:

下面启动 redis 服务:

# cd src
# ./redis-server

注意这种方式启动 redis 使用的是默认配置。也可以通过启动参数告诉 redis 使用指定配置文件使用下面命令启动。

# cd src
# ./redis-server ../redis.conf

redis.conf 是一个默认的配置文件。我们可以根据需要使用自己的配置文件。

启动 redis 服务进程后,就可以使用测试客户端程序 redis-cli 和 redis 服务交互了。 比如:

# cd src
# ./redis-cli
redis> set foo bar
OK
redis> get foo
"bar"

常用命令:

127.0.0.1:6379> ping   #测试连接是否通过
PONG
127.0.0.1:6379> keys *   #查看所有的key
1) "key"
127.0.0.1:6379> set name sheng   #设置一个key和value
OK
127.0.0.1:6379> get name   #查看key
"sheng"
127.0.0.1:6379> keys *
1) "name"
2) "key"

127.0.0.1:6379> select 3   #切换到第三个数据库,redis默认有16个数据库,在配置文件中,不同数据库之间的数据隔离,不选择的话默认是0号数据库
OK
127.0.0.1:6379[3]> set test my
OK
127.0.0.1:6379[3]> dbsize  #查看数据库大小
(integer) 1
127.0.0.1:6379[3]> flushdb   #清除数据
OK

127.0.0.1:6379> flushall   #清除所有数据库数据
OK
127.0.0.1:6379[3]> dbsize
(integer) 0
127.0.0.1:6379[3]>
127.0.0.1:6379> shutdown   #关闭服务端
not connected>

常用工具

  • redis-benchmark性能测试工具

支持以下参数:
用法:redis-benchmark [-h <主机>] [-p <端口>] [-c <客户端>] [-n <请求]> [-k <布尔>]
-h      <主机名>服务器主机名(默认值为127.0.0.1)
-p      <端口>服务器端口(默认6379) # 作者喜欢的一个女明星名字9键就是6397  !!!∑(゚Д゚ノ)ノ
-s      服务器套接字(覆盖主机和端口)
-a      <密码> Redis身份验证的密码
-c      <客户端>并行连接数(默认为50)
-n      <请求>请求总数(默认为100000)
-d      <大小> SET / GET值的数据大小(以字节为单位)(默认为2)
-dbnum  选择指定的数据库号(默认为0)
-k      <布尔值> 1 =保持活动状态0 =重新连接(默认1)
-r      将随机键用于SET / GET / INCR,将随机值用于SADD 使用此选项,基准测试将扩展字符串__ rand_ int__在具有指定范围内的12位数字的参数中从0到keyspacelen-1。 每次命令替换都会更改
被执行。 默认测试使用它来击中指定范围。
-P     管道请求。 默认值1(无管道)。
-q     只显示查询/秒值
--csv  以CSV格式输出
-l     循环测试

-t     <测试>仅运行逗号分隔的测试列表。 测试名称与输出名称相同。

-I      空闲模式。 只需打开N个空闲连接并等待。

 测试100个并发连接  100000请求

./redis-benchmark.exe -h localhost -p 6379 -c 100 -n 100000

sheng的学习笔记-redis框架原理_第5张图片

 结果分析:

====== PING_INLINE ======
  100000 requests completed in 2.53 seconds   10万个请求用了2.53秒
  100 parallel clients   使用100个并行的客户端
  3 bytes payload    每次写入3个字节
  keep alive: 1    只有1个服务器处理这些请求,测试单机性能

7.86% <= 1 milliseconds
40.72% <= 2 milliseconds
70.89% <= 3 milliseconds
92.65% <= 4 milliseconds
99.34% <= 5 milliseconds
99.92% <= 6 milliseconds
99.99% <= 7 milliseconds
100.00% <= 7 milliseconds    总共用7毫秒完成写入
39494.47 requests per second    每秒写入约3.9万

数据类型

  • 常用命令:

记不住命令具体用法可以去官网点击命令

sheng的学习笔记-redis框架原理_第6张图片

127.0.0.1:6379> ping   #测试连接是否通过
PONG
127.0.0.1:6379> keys *   #查看所有的key
1) "key"
127.0.0.1:6379> set name sheng   #设置一个key和value
OK
127.0.0.1:6379> get name   #查看key
"sheng"
127.0.0.1:6379> keys *
1) "name"
2) "key"

127.0.0.1:6379> select 3   #切换到第三个数据库,redis默认有16个数据库,在配置文件中,不同数据库之间的数据隔离,不选择的话默认是0号数据库
OK
127.0.0.1:6379[3]> set test my
OK
127.0.0.1:6379[3]> dbsize  #查看数据库大小
(integer) 1
127.0.0.1:6379[3]> flushdb   #清除数据
OK

127.0.0.1:6379> flushall   #清除所有数据库数据
OK
127.0.0.1:6379[3]> dbsize
(integer) 0
127.0.0.1:6379[3]>
127.0.0.1:6379> shutdown   #关闭服务端
not connected>

127.0.0.1:6379> set name sheng   #设置一个key和value
OK


127.0.0.1:6379> get name  #获取key对应的value
"sheng"
127.0.0.1:6379> move name 1   #将当前数据库的 key 移动到给定的数据库 db 当中
(integer) 1
127.0.0.1:6379> get name  #上面将key移动到别的数据库中,所以这个数据库就没有了这个key
(nil)

127.0.0.1:6379> set name sheng
OK
127.0.0.1:6379> get name
"sheng"
127.0.0.1:6379> expire name 10   #设置超时时间,10秒后无效。用于设置热点数据(带国过期时间的)或者cookie数据,单点登录等
(integer) 1
127.0.0.1:6379> ttl name   #查看key的剩余的时间
(integer) 7
127.0.0.1:6379> ttl name
(integer) 2
127.0.0.1:6379> get name  #在ttl减到0以后key就没有了
(nil)
127.0.0.1:6379> ttl name
(integer) -2

127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> type name    #查看指定key的类型
string

  • String(字符串)

127.0.0.1:6379> exists name   #看是否存在
(integer) 1

127.0.0.1:6379> append name hello    #追加字符串,如果当前key不存在,相当于setkey 
(integer) 10  #返回字符串的长度
127.0.0.1:6379> get name
"shenghello"
127.0.0.1:6379> strlen name   #获取key对应value的长度
(integer) 10
127.0.0.1:6379> append appendkey test
(integer) 4
127.0.0.1:6379> get appendkey
"test"
127.0.0.1:6379> set views 0  
OK
127.0.0.1:6379> get views
"0"
127.0.0.1:6379> incr views   #自增1  可以用作浏览量 +1
(integer) 1
127.0.0.1:6379> incr views
(integer) 2
127.0.0.1:6379> get views
"2"
127.0.0.1:6379> decr views   #自减1
(integer) 1
127.0.0.1:6379> decr views
(integer) 0
127.0.0.1:6379> decr views
(integer) -1
127.0.0.1:6379> incrby views 10    #带步长的自增,步长是10
(integer) 9
127.0.0.1:6379> decrby views 5
(integer) 4
127.0.0.1:6379> get views
"4"

127.0.0.1:6379> set key1 "hello,sheng"
OK
127.0.0.1:6379> getrange key1 0 3   #截取字符串 【0~3】
"hell"
127.0.0.1:6379> getrange key1 0 -1   #获取所有的字符串,结尾是-1
"hello,sheng"

127.0.0.1:6379> setrange key1 1 xx   #替换指定位置的字符串,从指定位置,替换成指定字符串
(integer) 11
127.0.0.1:6379> get key1
"hxxlo,sheng"

127.0.0.1:6379> setex key3 30 "hello"   #设置过期时间30秒,30秒后过期
OK
127.0.0.1:6379> ttl key3
(integer) 27
127.0.0.1:6379> setnx mykey "redis"   #如果不存在就设置,设置成功返回1,设置不成功返回0,用于分布式锁
(integer) 1
127.0.0.1:6379> keys *
1) "mykey"
2) "key3"
3) "key1"
127.0.0.1:6379> ttl key3
(integer) 5
127.0.0.1:6379> ttl key3
(integer) 1
127.0.0.1:6379> ttl key3
(integer) -2
127.0.0.1:6379> get key3
(nil)
127.0.0.1:6379> setnx mykey "test"
(integer) 0     #因为mykey是已经存在的,所以此处返回0
127.0.0.1:6379> get mykey
"redis"
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3    #同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k2"
2) "k3"
3) "k1"
127.0.0.1:6379> msetnx k1 v1 k2 v2 k3 v3   #同时设置多个值,如果存在就不设置,是一个原子操作,要么同时成功,要么同时失败
(integer) 0
127.0.0.1:6379> msetnx k1 v1 k2 v2 k3 v3 k4 v4   #看下v4,因为前面的k1~k3存在,所以k4没有成功设置
(integer) 0 
127.0.0.1:6379> keys *
1) "k2"
2) "k3"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3   #同时获取多个值
1) "v1"
2) "v2"
3) "v3"

127.0.0.1:6379> set user:1 {name:zhangsan,age:3}   #设置一个user:1 对象,值是json字符保存一个对象

OK
127.0.0.1:6379> get user:1
"{name:zhangsan,age:3}"
127.0.0.1:6379> mset user:1:name lisi user:1:age 2   #使用批量设置方式操纵对象的值,这里的key是个巧妙的设计:user:{id}:{filed},可以使用set article:1000:views 0用于设置指定文章ID(1000)的浏览量
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "lisi"
2) "2"

127.0.0.1:6379> getset key1 test   #先get再set,先获取到原来的值,再设置新的值
(nil)
127.0.0.1:6379> get key1
"test"
127.0.0.1:6379> getset key1 test123
"test"
127.0.0.1:6379> get key1
"test123"

string类似使用场景:

value除了是我们的字符串还可以是数字,可以用于:

  • 计数器
  • 统计多单位的数量
  • 粉丝数
  • 对象缓存存储

  • List(列表)

使用命令

在redis中,基本上命令是以l开头的

127.0.0.1:6379> lpush list one  #将一个值或者多个值,插入到列表头部(左边)
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1   #获取list中的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> rpush list four   #将一个值或者多个值,插入到列表尾部(右边)
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "four"

127.0.0.1:6379> lpop list    #删除list左边的第一个元素
"three"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3) "four"
127.0.0.1:6379> rpop list   #删除list的最后一个元素(左边最后一个,右边第一个)
"four"
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"

127.0.0.1:6379> lindex list 0   #根据索引查看元素
"two"

127.0.0.1:6379> llen list   #返回列表长度
(integer) 2

===========================================================

127.0.0.1:6379> lpush list one  #可以插入多个相同的值,列表中已经有one的元素了
(integer) 3
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "two"
3) "one"
127.0.0.1:6379> lpush list two
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "two"
2) "one"
3) "two"
4) "one"
127.0.0.1:6379> lpush list three
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "two"
5) "one"
127.0.0.1:6379> lrem list 1 two  #删除指定的值,语法:lrem key 个数  指定值
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "one"
3) "two"
4) "one"
127.0.0.1:6379> lrem list 2 one
(integer) 2
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"

===========================================================

127.0.0.1:6379> rpush mylist "hello"
(integer) 1
127.0.0.1:6379> rpush mylist "hello1"
(integer) 2
127.0.0.1:6379> rpush mylist "hello2"
(integer) 3
127.0.0.1:6379> rpush mylist "hello3"
(integer) 4
127.0.0.1:6379> lrange mylist 0 -1
1) "hello"
2) "hello1"
3) "hello2"
4) "hello3"
127.0.0.1:6379> ltrim mylist 1 2   #截断列表
OK
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"

===========================================================

#重新设置一下mylist如下

127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"
3) "hello3"
127.0.0.1:6379> rpoplpush mylist myotherlist  #先移除列表最后一个元素,再插入到新的队列中,将hello3移除mylist队列并放到myotherlist队列中
"hello3"
127.0.0.1:6379> lrange myotherlist 0 -1
1) "hello3"
127.0.0.1:6379> lrange mylist 0 -1
1) "hello1"
2) "hello2"

===========================================================

127.0.0.1:6379> exists list  #查看列表是否存在
(integer) 0
127.0.0.1:6379> lset list 0 item 
#必须有列表,才能set,否则报错
(error) ERR no such key
127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "value1"
127.0.0.1:6379> lset list 0 item   
#将列表list中的指定下表的值替换成别的值,相当于更新
OK
127.0.0.1:6379> lrange list 0 -1
1) "item"

使用场景

使用场景:微博 TimeLine、消息队列
List 说白了就是链表(redis 使用双端链表实现的 List),相信学过数据结构知识的人都应该能理解其结构。使用 List 结构,我们可以轻松地实现最新消息排行等功能(比如新浪微博的 TimeLine )。List 的另一个应用就是消息队列,可以利用 List 的 *PUSH 操作,将任务存在 List 中,然后工作线程再用 POP 操作将任务取出进行执行。

  • set(集合)

值不能重复的集合,命令一般以s开头

使用命令

127.0.0.1:6379> sadd myset "hello"    #插入值
(integer) 1
127.0.0.1:6379> sadd myset "hello1"
(integer) 1
127.0.0.1:6379> sadd myset "hello2"
(integer) 1
127.0.0.1:6379> smembers myset   #查看成员变量
1) "hello1"
2) "hello2"
3) "hello"
127.0.0.1:6379> sismember myset "hello"   #成员变量是否存在
(integer) 1
127.0.0.1:6379> sismember myset "word"
(integer) 0
127.0.0.1:6379> scard myset    #获取集合元素个数
(integer) 3

127.0.0.1:6379> srem myset hello   #移除集合的元素
(integer) 1
127.0.0.1:6379> smembers myset
1) "hello1"
2) "hello2"

127.0.0.1:6379> smove myset myset2 hello1   #将一个列表的元素移除到另一个列表
(integer) 1
127.0.0.1:6379> smembers myset
1) "hello2"
127.0.0.1:6379> smembers myset2
1) "hello1"

set类似使用场景:

B站或者微博的共同关注功能可以用set

数字集合类的:差集,并集,交集

比如A用户将他关注的人放入一个set中,将其粉丝放到另一个set中,可以支持共同关注,共同爱好,推荐好友等

127.0.0.1:6379> sadd key1 a
(integer) 1
127.0.0.1:6379> sadd key1 b
(integer) 1
127.0.0.1:6379> sadd key1 c
(integer) 1
127.0.0.1:6379> sadd key2 c
(integer) 1
127.0.0.1:6379> sadd key2 d
(integer) 1
127.0.0.1:6379> sadd key2 e
(integer) 1
127.0.0.1:6379> sdiff key1 key2  #差集
1) "b"
2) "a"
127.0.0.1:6379> sinter key1 key2  #交集  共同好友之类的功能可以用
1) "c"
127.0.0.1:6379> sunion key1 key2  #并集
1) "d"
2) "c"
3) "a"
4) "b"
5) "e"

  • HASH

Map集合,是以  key-map的方式,本质上跟string很像,只是value是map格式

所以可以按照string的命令去记,只需要 h开头命令   hash  field  value 这样的格式

127.0.0.1:6379> hset myhash field2 value2   #设置hash的值
(integer) 1
127.0.0.1:6379> hget myhash field2   #获取hash的值
"value2"
127.0.0.1:6379> hmset myhash field1 hello field world    #批量设置hash的值
OK
127.0.0.1:6379> hmget myhash field1 field    #批量获取hash的值
1) "hello"
2) "world"
127.0.0.1:6379> hgetall myhash   #获取hash所有的key和value
1) "field1"
2) "hello"
3) "field2"
4) "value2"
5) "field"
6) "world"

127.0.0.1:6379> hdel myhash field1   #删除hash的指定的Key字段,对应的value也就没了
(integer) 1
127.0.0.1:6379> hgetall myhash
1) "field2"
2) "value2"
3) "field"
4) "world"

127.0.0.1:6379> hlen myhash   #获取hash有多少个键值对
(integer) 2

127.0.0.1:6379> hexists myhash field1  #看hash指定的值是否存在
(integer) 0
127.0.0.1:6379> hexists myhash field
(integer) 1
127.0.0.1:6379> hkeys myhash  #获取所有的key
1) "field2"
2) "field"
127.0.0.1:6379> hvals myhash  #获取所有的value
1) "value2"
2) "world"

hash类似使用场景:

hash更适合对象的存储,string更适合字符串的存储

hash可以存放一些用户信息的保存,尤其是经常变动的信息,比如:

127.0.0.1:6379> hset user:1 name myname
(integer) 1
127.0.0.1:6379> hget user:1 name
"myname"

  • Zset(有序集合)

在set的基础上增加一个值,set k1 v1   zset k1 score v1

127.0.0.1:6379> zadd salary 2500 zhangsan  #添加数据,新增薪水的set集合,zhangsan 工资2500
(integer) 1
127.0.0.1:6379> zadd salary 5000 kisi
(integer) 1
127.0.0.1:6379> zadd salary 500 wangwu
(integer) 1
127.0.0.1:6379> zrange salary 0 -1  #获取薪水集合所有的数据
1) "wangwu"
2) "zhangsan"
3) "kisi"
127.0.0.1:6379> zrangebyscore salary -inf +inf   #按照score排序,score的列存放的是薪水,就是按照薪水排序  -inf代表负无穷,+inf代表正无穷,如果只想要3000薪水一下的数据,将+inf改为3000就行了
1) "wangwu"
2) "zhangsan"
3) "kisi"
127.0.0.1:6379> zrangebyscore salary -inf +inf withscores  #顺序显示数据,并且显示score
1) "wangwu"
2) "500"
3) "zhangsan"
4) "2500"
5) "kisi"
6) "5000"
127.0.0.1:6379> zcount salary 0 300
(integer) 0
127.0.0.1:6379> zcount salary 0 3000   #获取score在0~3000的区间的个数,用于计数器之类的
(integer) 2

zset类似使用场景:

set排序,存放班级成绩表,工资表排序

普通消息权重是1,重要消息权重是2,带权重进行判断,比如b站的播放量进行排序,排行榜,热搜之类的

  • geospatial(地理位置)

输入经度维度信息,可以推算地理位置的信息,两地之间的距离,方圆几里之内的人,命令通常以geo开头

在网上根据经纬度,输入上海,北京,重庆,西安的坐标

127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai  #输入上海坐标
(integer) 1
127.0.0.1:6379> geoadd china:city 116.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing
(integer) 1
127.0.0.1:6379> geoadd china:city 125.14 42.92 xian
(integer) 1
127.0.0.1:6379> geopos china:city beijing  #查询北京坐标
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city beijing chongqing   #查询多个地址的坐标
1) 1) "116.39999896287918091"
   2) "39.90000009167092543"
2) 1) "106.49999767541885376"
   2) "29.52999957900659211"
127.0.0.1:6379> geodist china:city beijing shanghai km   #根据经纬度,查询两个节点的直线距离,单位是km
"1067.3788"
127.0.0.1:6379> georadius china:city 110 30 1000 km   #查询以110 30为圆心,1000km为半径的节点
1) "chongqing"
127.0.0.1:6379> georadius china:city 110 30 10000 km
1) "chongqing"
2) "shanghai"
3) "beijing"
4) "xian"

geospatial类似使用场景:

朋友的定位,附近的人,打车的距离计算

geospatial底层是用zset实现的,所以可以用zset来操作geo的数据,比如:

127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "shanghai"
3) "beijing"
4) "xian"

  • hyperloglog

Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

什么是基数?

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

127.0.0.1:6379> pfadd mykey a b c d e f g h i j  #新增HyperLogLog数据
(integer) 1
127.0.0.1:6379> pfcount mykey   #查看基数
(integer) 10
127.0.0.1:6379> pfadd mykey2 i j z x c v b n m
(integer) 1
127.0.0.1:6379> pfcount mykey2
(integer) 9
127.0.0.1:6379> pfmerge mykey3 mykey mykey2   #将mykey 和mykey2  合并成mykey3
OK
127.0.0.1:6379> pfcount mykey3   #合并之后的集合的不重复元素有15个
(integer) 15
127.0.0.1:6379>

HyperLogLog类似使用场景:

网页的UV(一个人访问一个网站多次,但还算一个人),传统的方式用set保存用户ID,然后统计SET的元素数量,但这种方式需要统计大量的用户ID,此时可以用HyperLogLog。

如果允许误差,用HyperLogLog,如果不允许误差,可以用set或者自己的数据类型

  • bitmaps

位存储,格式   0 1 0 0 0 1 

可以用下面的方式记录一个人一周打卡情况,sign后面是周几,最后一位表示是否打卡(0没打卡,1打卡)

127.0.0.1:6379> setbit sign 0 1   #设置周几是否打卡信息
(integer) 0
127.0.0.1:6379> setbit sign 1 0
(integer) 0
127.0.0.1:6379> setbit sign 2 0
(integer) 0
127.0.0.1:6379> setbit sign 3 1
(integer) 0
127.0.0.1:6379> setbit sign 4 1
(integer) 0
127.0.0.1:6379> setbit sign 5 0
(integer) 0
127.0.0.1:6379> setbit sign 6 0
(integer) 0

127.0.0.1:6379> getbit sign 3   #查找周三是否打卡了
(integer) 1
127.0.0.1:6379> bitcount sign  #统计打卡天数
(integer) 3

HyperLogLog类似使用场景:

统计用户:活跃,不活跃。登陆,没登录,打卡等

统计疫情人数,每个人是一位,感染了是1,没感染是0

 代码(jedis)

使用java操作redis,jedis是redis官网推荐的java连接工具,使用java操作redis中间件,

maven配置文件



    4.0.0

    org.example
    redis-app
    1.0-SNAPSHOT

    
        16
        16
    

    
        
        
            redis.clients
            jedis
            4.1.0
        
        
            com.alibaba
            fastjson
            1.2.62
        

    

测试代码

简单案例

import redis.clients.jedis.Jedis;

public class ExampleTest {
    public static void main(String[] args) {
        // 连接redis,获取jedis对象
        Jedis jedis = new Jedis("127.0.0.1",6379);
        // jedis的命令就是我们之前学习的所有指令,在redis客户端上操作的
        System.out.println("测试连接是否成功:" + jedis.ping());
        System.out.println("判断某个键值对是否存在" + jedis.exists("username"));
        System.out.println("新增username的值" + jedis.set("username","my name"));
        System.out.println("系统中所有的键值对如下" + jedis.keys("*"));
        System.out.println("删除键值对username" + jedis.del("username"));
    }
}

结果如下:

sheng的学习笔记-redis框架原理_第7张图片

 事务案例代码

import redis.clients.jedis.Jedis;
import redis.clients.jedis.Transaction;

// 事务案例代码
public class ExampleTxTest {
    public static void main(String[] args) {
        // 连接redis,获取jedis对象
        Jedis jedis = new Jedis("127.0.0.1",6379);
        //开启事务
        Transaction multi = jedis.multi();
        try {
            multi.set("user1", "user1");
            multi.set("user2", "user2");
            multi.exec();  // 提交事务
        }catch (Exception e){
            multi.discard();  //异常,放弃事务
            e.printStackTrace();
        }finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            multi.close();  //关闭连接
        }

    }
}

结果:

sheng的学习笔记-redis框架原理_第8张图片

哨兵代码

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.JedisSentinelPool;
import redis.clients.jedis.Transaction;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

// 事务案例代码
public class ExampleSentinelTest {
    public static void main(String[] args) {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        jedisPoolConfig.setMaxTotal(10);
        jedisPoolConfig.setMaxIdle(5);
        jedisPoolConfig.setMinIdle(5);
        // 哨兵信息
        Set sentinels = new HashSet<>(Arrays.asList("127.0.0.1:26379"));
        // 创建连接池
        JedisSentinelPool pool = new JedisSentinelPool("mymaster", sentinels,jedisPoolConfig);
        // 连接redis,获取jedis对象
        Jedis jedis = pool.getResource();
        //开启事务
        Transaction multi = jedis.multi();
        try {
            multi.set("user1", "user1");
            multi.set("user2", "user2");
            multi.exec();  // 提交事务
        }catch (Exception e){
            multi.discard();  //异常,放弃事务
            e.printStackTrace();
        }finally {
            System.out.println(jedis.get("user1"));
            System.out.println(jedis.get("user2"));
            multi.close();  //关闭连接
        }

    }
}

注意连接的是哨兵的IP和端口,所以主机和从机直接的切换,在客户端是无感知的,由哨兵统筹

结果如下:

sheng的学习笔记-redis框架原理_第9张图片

redis客户端和服务端流程图

6832e911747a4a72b3ac35e2c215a453.png

 

redis为什么这么快

  • 基于内存实现

Redis 是基于内存的数据库,那不可避免的就要与磁盘数据库做对比。对于磁盘数据库来说,是需要将数据读取到内存里的,这个过程会受到磁盘 I/O 的限制。

而对于内存数据库来说,本身数据就存在于内存里,也就没有了这方面的开销

  • 单线程操作

Redis 是单线程的。多线程在执行过程中需要进行 CPU 的上下文切换,这个操作比较耗时。Redis 又是基于内存实现的,对于内存来说,没有上下文切换效率就是最高的。多次读写都在一个CPU 上,对于内存来说就是最佳方案。

sheng的学习笔记-redis框架原理_第10张图片

接收到用户的请求后,全部推送到一个队列里,然后交给文件事件分派器,而它是单线程的工作方式。Redis 又是基于它工作的,所以说 Redis 是单线程的

升级后的redis变为多线程,这里的多线程指的是IO处理,但核心处理逻辑还是单线程,看下IO处理的演变,增加了IO的吞吐量

sheng的学习笔记-redis框架原理_第11张图片

到了Redis6.0之后:

sheng的学习笔记-redis框架原理_第12张图片

  • IO多路复用

使用io多路复用和reactor设计模式,可以增加IO处理速度

reactor设计模式:

sheng的学习笔记-Reactor 模式_coldstarry的博客-CSDN博客

IO多路复用可以参考

sheng的学习笔记-IO多路复用,NIO,BIO,AIO_coldstarry的博客-CSDN博客

IO复用只需要阻塞在select,poll或者epoll,可以同时处理和管理多个连接

  • 优点是同时管理多个网络IO,比NIO和BIO等模型的吞吐量都要大,但必须IO很多
  • 缺点是当 select、poll或者epoll 管理的连接数过少时,这种模型将退化成阻塞 IO 模型

539084fa1ab143c18a1b24cf0afb4712.png

  •  高效的数据结构

Redis 的底层数据结构一共有6种,分别是,简单动态字符串,双向链表,压缩列表,哈希表,跳表和整数数组,它们和数据类型的对应关系如下图所示:

sheng的学习笔记-redis框架原理_第13张图片

  • 简单动态字符串(simple dynamic string  SDS

 自己构建了一种简单动态字符串的抽象类型,并将SDS用作Redis的默认字符串表示

(1)字符串长度处理

sheng的学习笔记-redis框架原理_第14张图片

这个图是字符串在 C 语言中的存储方式,想要获取 「Redis」的长度,需要从头开始遍历,直到遇到 '\0' 为止。

sheng的学习笔记-redis框架原理_第15张图片

Redis 中怎么操作呢?用一个 len 字段记录当前字符串的长度。想要获取长度只需要获取 len 字段即可。你看,差距不言自明。前者遍历的时间复杂度为 O(n),Redis 中 O(1) 就能拿到,速度明显提升。

(2)内存重新分配

C 语言中涉及到修改字符串的时候会重新分配内存。修改地越频繁,内存分配也就越频繁。而内存分配是会消耗性能的,那么性能下降在所难免。

而 Redis 中会涉及到字符串频繁的修改操作,这种内存分配方式显然就不适合了。于是 SDS 实现了两种优化策略:

  • 空间预分配

对 SDS 修改及空间扩充时,除了分配所必须的空间外,还会额外分配未使用的空间。

具体分配规则是这样的:SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。

  • 惰性空间释放

当然,有空间分配对应的就有空间释放。

SDS 缩短时,并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。

(3)二进制安全

你已经知道了 Redis 可以存储各种数据类型,那么二进制数据肯定也不例外。但二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 '\0' 等。

前面我们提到过,C 中字符串遇到 '\0' 会结束,那 '\0' 之后的数据就读取不上了。但在 SDS 中,是根据 len 长度来判断字符串结束的。看,二进制安全的问题就解决了。

  • 双端链表

列表 List 更多是被当作队列或栈来使用的。队列和栈的特性一个先进先出,一个先进后出。双端链表很好的支持了这些特性。

sheng的学习笔记-redis框架原理_第16张图片

(1)前后节点

链表里每个节点都带有两个指针,prev 指向前节点,next 指向后节点。这样在时间复杂度为 O(1) 内就能获取到前后节点。

sheng的学习笔记-redis框架原理_第17张图片

(2)头尾节点

头节点里有 head 和 tail 两个参数,分别指向头节点和尾节点。这样的设计能够对双端节点的处理时间复杂度降至 O(1) ,对于队列和栈来说再适合不过。同时链表迭代时从两端都可以进行

sheng的学习笔记-redis框架原理_第18张图片

(3)链表长度

头节点里同时还有一个参数 len,和上边提到的 SDS 里类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到 len 值就可以了,这个时间复杂度是 O(1)。

  • 压缩列表

如果在一个链表节点中存储一个小数据,比如一个字节。那么对应的就要保存头节点,前后指针等额外的数据。

这样就浪费了空间,同时由于反复申请与释放也容易导致内存碎片化。这样内存的使用效率就太低了。

它是经过特殊编码,专门为了提升内存使用效率设计的。所有的操作都是通过指针与解码出来的偏移量进行的。

并且压缩列表的内存是连续分配的,遍历的速度很快。

sheng的学习笔记-redis框架原理_第19张图片

  • 字典

Redis 作为 K-V 型数据库,所有的键值都是用字典来存储的。

日常学习中使用的字典你应该不会陌生,想查找某个词通过某个字就可以直接定位到,速度非常快。这里所说的字典原理上是一样的,通过某个 key 可以直接获取到对应的value。

字典又称为哈希表,这点没什么可说的。哈希表的特性大家都很清楚,能够在 O(1) 时间复杂度内取出和插入关联的值。

  • 跳跃表

作为 Redis 中特有的数据结构-跳跃表,其在链表的基础上增加了多级索引来提升查找效率。

这是跳跃表的简单原理图,每一层都有一条有序的链表,最底层的链表包含了所有的元素。这样跳跃表就可以支持在 O(logN) 的时间复杂度里查找到对应的节点。

sheng的学习笔记-redis框架原理_第20张图片

下面这张是跳表真实的存储结构,和其它数据结构一样,都在头节点里记录了相应的信息,减少了一些不必要的系统开销。

sheng的学习笔记-redis框架原理_第21张图片

合理的数据编码

对于每一种数据类型来说,底层的支持可能是多种数据结构,什么时候使用哪种数据结构,这就涉及到了编码转化的问题。

那我们就来看看,不同的数据类型是如何进行编码转化的:

String:存储数字的话,采用int类型的编码,如果是非数字的话,采用 raw 编码;

List:字符串长度及元素个数小于一定范围使用 ziplist 编码,任意条件不满足,则转化为 linkedlist 编码;

Hash:hash 对象保存的键值对内的键和值字符串长度小于一定值及键值对;

Set:保存元素为整数及元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码;

Zset:zset 对象中保存的元素个数小于及成员长度小于一定值使用 ziplist 编码,任意条件不满足,则使用 skiplist 编码。

REDIS缓存穿透和雪崩和穿刺

缓存处理流程

前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。

      

  • 缓存穿透:key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
  • 缓存击穿:key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
  • 缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。

缓存穿透

描述:

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

解决方案:

  1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
  2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
    • 缺点:相对简单, 但是也容易破解, 比如 攻击者通过分析数据格式, 不重复的请求数据库不存在数据, 那这样方案就等于失效的
    • sheng的学习笔记-redis框架原理_第22张图片

  3. 可以设置一些过滤规则, 如布隆过滤器(这种方式更稳定,常用)

布隆过滤器:

过滤规则目前主流的一种的载体就是布隆过滤器. 布隆过滤器是一种概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”.

相比于传统的 List、Set、Map 等数据结构,布隆过滤器是一个bit数组, 它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的

sheng的学习笔记-redis框架原理_第23张图片


如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,使用多个 hash 函数对 key 进行 hash 算得一个整数索引值,然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作,例如针对值 “zhangsan” 和三个不同的哈希函数分别生成了哈希值 1、4、7

sheng的学习笔记-redis框架原理_第24张图片

我们现在再存一个值 “lisi”,如果哈希函数返回 4、5、8 的话,图继续变为:

sheng的学习笔记-redis框架原理_第25张图片

当我们想要判断布隆过滤器是否记录了某个数据时,布隆过滤器会先对该数据进行同样的哈希处理, 比如 “wangwu”的哈希函数返回了 2、5、8三个值,结果我们发现 2 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 “wangwu” 这个数据不存在。

但是同时我们会发现,4 这个 bit 位由于”zhangsan”和”lisi”的哈希函数都返回了这个 bit 位,因此它被覆盖了。那么随着布隆过滤器保存的数据不断增多, 重复的概率就会不断增大, 所以当我们过滤某个数据时, 如果发现其三个哈希值都在过滤器中进行了记录, 那么也只能说明过滤器中可能包含了该数据, 并不能绝对肯定, 因为可能是其他数据的哈希值对结果产生了影响.这也就解释了上文所说的 布隆过滤器只能说明“某样东西一定不存在或者可能存在”.至于为什么采用三种不同的哈希函数取值, 因为三个哈希值只要有一个不存在就说明数据一定不在过滤器中, 这样做是可以减小因哈希碰撞(两个数据的哈希值相同)产生的错误概率.

缓存击穿的解决办法

概述:

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮

方案一 后台刷新

后台定义一个job(定时任务)专门主动更新缓存数据.比如,一个缓存中的数据过期时间是30分钟,那么job每隔29分钟定时刷新数据(将从数据库中查到的数据更新到缓存中).

这种方案比较容易理解,但会增加系统复杂度。比较适合那些 key 相对固定,cache 粒度较大的业务,key 比较分散的则不太适合,实现起来也比较复杂。

方案二 检查更新

将缓存key的过期时间(绝对时间)一起保存到缓存中(可以拼接,可以添加新字段,可以采用单独的key保存..不管用什么方式,只要两者建立好关联关系就行).在每次执行get操作后,都将get出来的缓存过期时间与当前系统时间做一个对比,如果缓存过期时间-当前系统时间<=1分钟(自定义的一个值),则主动更新缓存.这样就能保证缓存中的数据始终是最新的(和方案一一样,让数据不过期.)

这种方案在特殊情况下也会有问题。假设缓存过期时间是12:00,而 11:59 
到 12:00这 1 分钟时间里恰好没有 get 请求过来,又恰好请求都在 11:30 分的时 
候高并发过来,那就悲剧了。这种情况比较极端,但并不是没有可能。因为“高 
并发”也可能是阶段性在某个时间点爆发。

方案三 分级缓存

采用 L1 (一级缓存)和 L2(二级缓存) 缓存方式,L1 缓存失效时间短,L2 缓存失效时间长。 请求优先从 L1 缓存获取数据,如果 L1缓存未命中则加锁,只有 1 个线程获取到锁,这个线程再从数据库中读取数据并将数据再更新到到 L1 缓存和 L2 缓存中,而其他线程依旧从 L2 缓存获取数据并返回。

这种方式,主要是通过避免缓存同时失效并结合锁机制实现。所以,当数据更 
新时,只能淘汰 L1 缓存,不能同时将 L1 和 L2 中的缓存同时淘汰。L2 缓存中 
可能会存在脏数据,需要业务能够容忍这种短时间的不一致。而且,这种方案 
可能会造成额外的缓存空间浪费。

方案四加锁

方法1

    // 方法1:
    public synchronized List getData01() {
        List result = new ArrayList();
        // 从缓存读取数据
        result = getDataFromCache();
        if (result.isEmpty()) {
            // 从数据库查询数据
            result = getDataFromDB();
            // 将查询到的数据写入缓存
            setDataToCache(result);
        }
        return result;
    }
这种方式确实能够防止缓存失效时高并发到数据库,但是缓存没有失效的时候,在从缓存中拿数据时需要排队取锁,这必然会大大的降低了系统的吞吐量.
方法2

// 方法2:
    static Object lock = new Object();
 
    public List getData02() {
        List result = new ArrayList();
        // 从缓存读取数据
        result = getDataFromCache();
        if (result.isEmpty()) {
            synchronized (lock) {
                // 从数据库查询数据
                result = getDataFromDB();
                // 将查询到的数据写入缓存
                setDataToCache(result);
            }
        }
        return result;
    }
这个方法在缓存命中的时候,系统的吞吐量不会受影响,但是当缓存失效时,请求还是会打到数据库,只不过不是高并发而是阻塞而已.但是,这样会造成用户体验不佳,并且还给数据库带来额外压力.
方法3

//方法3
    public List getData03() {
        List result = new ArrayList();
        // 从缓存读取数据
        result = getDataFromCache();
        if (result.isEmpty()) {
            synchronized (lock) {
            //双重判断,第二个以及之后的请求不必去找数据库,直接命中缓存
                // 查询缓存
                result = getDataFromCache();
                if (result.isEmpty()) {
                    // 从数据库查询数据
                    result = getDataFromDB();
                    // 将查询到的数据写入缓存
                    setDataToCache(result);
                }
            }
        }
        return result;
    }
双重判断虽然能够阻止高并发请求打到数据库,但是第二个以及之后的请求在命中缓存时,还是排队进行的.比如,当30个请求一起并发过来,在双重判断时,第一个请求去数据库查询并更新缓存数据,剩下的29个请求则是依次排队取缓存中取数据.请求排在后面的用户的体验会不爽.

方法4

static Lock reenLock = new ReentrantLock();
 
    public List getData04() throws InterruptedException {
        List result = new ArrayList();
        // 从缓存读取数据
        result = getDataFromCache();
        if (result.isEmpty()) {
            if (reenLock.tryLock()) {
                try {
                    System.out.println("我拿到锁了,从DB获取数据库后写入缓存");
                    // 从数据库查询数据
                    result = getDataFromDB();
                    // 将查询到的数据写入缓存
                    setDataToCache(result);
                } finally {
                    reenLock.unlock();// 释放锁
                }
 
            } else {
                result = getDataFromCache();// 先查一下缓存
                if (result.isEmpty()) {
                    System.out.println("我没拿到锁,缓存也没数据,先小憩一下");
                    Thread.sleep(100);// 小憩一会儿
                    return getData04();// 重试
                }
            }
        }
        return result;
    }
最后使用互斥锁的方式来实现,可以有效避免前面几种问题.

缓存雪崩:

描述:

当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力

加锁

加锁在缓存击穿中已经讲解过,一般并发量不是特别多的时候,使用最多的解决方案是加锁排队,加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这是过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法

分级缓存

缓存击穿中已经讲解过,看上面就有详细的方案,可以解决雪崩的问题

缓存标记

给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存,这个距离

  • 缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存。
  • 缓存数据:它的过期时间比缓存标记的时间延长1倍,例:标记缓存时间30分钟,数据缓存设置为60分钟。 这样,当缓存标记key过期后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存。

这样做后,就可以一定程度上提高系统吞吐量。

这种方式跟缓存击穿的方案二检查更新很像

public object GetProductListNew()
        {
            const int cacheTime = 30;
            const string cacheKey = "product_list";
            //缓存标记。
            const string cacheSign = cacheKey + "_sign";
            
            var sign = CacheHelper.Get(cacheSign);
            //获取缓存值
            var cacheValue = CacheHelper.Get(cacheKey);
            if (sign != null)
            {
                return cacheValue; //未过期,直接返回。
            }
            else
            {
                CacheHelper.Add(cacheSign, "1", cacheTime);
                ThreadPool.QueueUserWorkItem((arg) =>
                {
                    cacheValue = GetProductListFromDB(); //这里一般是 sql查询数据。
                    CacheHelper.Add(cacheKey, cacheValue, cacheTime*2); //日期设缓存时间的2倍,用于脏读。                
                });
                
                return cacheValue;
            }
        }

缓存预热

  缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样避免,用户请求的时候,再去加载相关的数据。

跟缓存击穿的后台刷新差不多,增加系统复杂度,固定一些的数据还能搞,业务的变化的数据不好整

为key设置不同的缓存失效时间

防止重启的场景,在重启后不同的Key有不同的缓存失效时间,不过某个关键的key如果设置的时间比较小,又被打穿了,这个也起不了作用,除非是很多个KEY都可能被打穿,但没有特别重要的KEY,可以用这种方式减少损失

redis持久化

Redis是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将Redis中的数据以某种形式(数据或命令)从内存保存到硬盘;当下次Redis重启时,利用持久化文件实现数据恢复。除此之外,为了进行灾难备份,可以将持久化文件拷贝到一个远程位置。持久化机制有RDB(Redis DataBase)和AOF(Append Only File)

持久化流程

既然redis的数据可以保存在磁盘上,那么这个流程是什么样的呢?

要有下面五个过程:

  1. 客户端向服务端发送写操作(数据在客户端的内存中)。
  2. 数据库服务端接收到写请求的数据(数据在服务端的内存中)。
  3. 服务端调用write这个系统调用,将数据往磁盘上写(数据在系统内存的缓冲区中)。
  4. 操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中)。
  5. 磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)。

这5个过程是在理想条件下一个正常的保存流程,但是在大多数情况下,我们的机器等等都会有各种各样的故障,这里划分了两种情况:

  1. Redis数据库发生故障,只要在上面的第三步执行完毕,那么就可以持久化保存,剩下的两步由操作系统替我们完成。
  2. 操作系统发生故障,必须上面5步都完成才可以。

RDB机制和原理

RDB(Redis DataBase)持久化是将当前进程中的数据生成快照保存到硬盘(因此也称作快照持久化),保存的文件后缀是rdb;当Redis重新启动时,可以读取快照文件恢复数据。

触发机制

redis提供了三种机制:save(手动触发)、bgsave(手动触发)、自动化

rdb之save原理(不推荐)

手动执行该命令,该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。

执行完成时候如果存在老的RDB文件,就把新的替代掉旧的。我们的客户端可能都是几万或者是几十万,这种方式显然不可取,线上环境要杜绝save的使用。具体流程如下:

sheng的学习笔记-redis框架原理_第26张图片

rdb之bgsave触发原理(手动执行推荐这种方式,自动执行也是走这个方式,重点关注)

手动执行该命令

Redis进程执行fork操作创建子进程,RDB持久化过程由子 进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。具体流程是:

  1. 执行bgsave命令,Redis父进程判断当前是否存在正在执行的子进 程,如RDB/AOF子进程,如果存在bgsave命令直接返回。
  2. 父进程执行fork操作创建子进程,fork操作过程中父进程会阻塞,通 过info stats命令查看latest_fork_usec选项,可以获取最近一个fork操作的耗时,单位为微秒
  3. 父进程fork完成后,bgsave命令返回“Background saving started”信息并不再阻塞父进程,可以继续响应其他命令。
  4. 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后 对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的 时间,对应info统计的rdb_last_save_time选项。
  5. 进程发送信号给父进程表示完成,父进程更新统计信息,具体见 info Persistence下的rdb_*相关选项。

sheng的学习笔记-redis框架原理_第27张图片

RDB文件的处理

保存:RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配 置指定。可以通过执行config set dir{newDir}和config set dbfilename{newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。

rdb之自动触发(默认)

  1. 使用save相关配置,如“save m n”。表示m秒内数据集存在n次修改 时,自动触发bgsave(原理图见上图)
  2. 如果从节点执行全量复制操作,主节点自动执行bgsave生成RDB文件并发送给从节点
  3. 执行debug reload命令重新加载Redis时,也会自动触发save操作。
  4. 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能则 自动执行bgsave。

配置文件截图:

sheng的学习笔记-redis框架原理_第28张图片

save命令

save m n

自动触发最常见的情况是在配置文件中通过save m n,指定当m秒内发生n次变化时,会触发bgsave。

其中save 900 1的含义是:当时间到900秒时,如果redis数据发生了至少1次变化,则执行bgsave;save 300 10和save 60 10000同理。当三个save条件满足任意一个时,都会引起bgsave的调用

save m n的实现原理

Redis的save m n,是通过serverCron函数、dirty计数器、和lastsave时间戳来实现的。

serverCron是Redis服务器的周期性操作函数,默认每隔100ms执行一次;该函数对服务器的状态进行维护,其中一项工作就是检查 save m n 配置的条件是否满足,如果满足就执行bgsave。

dirty计数器是Redis服务器维持的一个状态,记录了上一次执行bgsave/save命令后,服务器状态进行了多少次修改(包括增删改);而当save/bgsave执行完成后,会将dirty重新置为0。

例如,如果Redis执行了set mykey helloworld,则dirty值会+1;如果执行了sadd myset v1 v2 v3,则dirty值会+3;注意dirty记录的是服务器进行了多少次修改,而不是客户端执行了多少修改数据的命令。

lastsave时间戳也是Redis服务器维持的一个状态,记录的是上一次成功执行save/bgsave的时间。

save m n的原理如下:每隔100ms,执行serverCron函数;在serverCron函数中,遍历save m n配置的保存条件,只要有一个条件满足,就进行bgsave。对于每一个save m n条件,只有下面两条同时满足时才算满足:

RDB文件格式

RDB文件格式如下图所示(图片来源:《Redis设计与实现》):

其中各个字段的含义说明如下:

1)  REDIS:常量,保存着”REDIS”5个字符。

2)  db_version:RDB文件的版本号,注意不是Redis的版本号。

3)  SELECTDB 0 pairs:表示一个完整的数据库(0号数据库),同理SELECTDB 3 pairs表示完整的3号数据库;只有当数据库中有键值对时,RDB文件中才会有该数据库的信息(上图所示的Redis中只有0号和3号数据库有键值对);如果Redis中所有的数据库都没有键值对,则这一部分直接省略。其中:SELECTDB是一个常量,代表后面跟着的是数据库号码;0和3是数据库号码;pairs则存储了具体的键值对信息,包括key、value值,及其数据类型、内部编码、过期时间、压缩信息等等。

4)  EOF:常量,标志RDB文件正文内容结束。

5)  check_sum:前面所有内容的校验和;Redis在载入RBD文件时,会计算前面的校验和并与check_sum值比较,判断文件是否损坏。

RDB文件压缩

Redis默认采用LZF算法对RDB文件进行压缩。虽然压缩耗时,但是可以大大减小RDB文件的体积,因此压缩默认开启;可以通过命令关闭:

需要注意的是,RDB文件的压缩并不是针对整个文件进行的,而是对数据库中的字符串进行的,且只有在字符串达到一定长度(20字节)时才会进行。

AOF机制和原理

AOF(append only file)将Redis执行的每次写命令(读请求不记录到文件中)记录到单独的日志文件中(有点像MySQL的binlog);当Redis重启时再次执行AOF文件中的命令来恢复数据。

Redis服务器默认开启RDB,关闭AOF;要开启AOF,需要在配置文件中配置:appendonly yes

AOF流程

开启AOF功能需要设置配置:appendonly yes,默认不开启。AOF文件名 通过appendfilename配置设置,默认文件名是appendonly.aof。保存路径同 RDB持久化方式一致,通过dir配置指定。AOF的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load),流程图如下:

  1. 所有的写入命令会追加到aof_buf(缓冲区)中。
  2. AOF缓冲区根据对应的策略向硬盘做同步操作。
  3.  随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的。

sheng的学习笔记-redis框架原理_第29张图片

AOF流程-AOF命令追加(append)

        Redis先将写命令追加到缓冲区,而不是直接写入文件,主要是为了避免每次有写命令都直接写入硬盘,导致硬盘IO成为Redis负载的瓶颈。

命令追加的格式是Redis命令请求的协议格式,它是一种纯文本格式,具有兼容性好、可读性强、容易处理、操作简单避免二次开销等优点;具体格式略。在AOF文件中,除了用于指定数据库的select命令(如select 0 为选中0号数据库)是由Redis添加的,其他都是客户端发送来的写命令。

AOF流程-AOF文件写入(write)和文件同步(sync)

AOF为什么把命令追加到aof_buf中?Redis使用单线程响应命令,如 果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负 载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡

Redis提供了多种AOF缓存区的同步文件策略,策略涉及到操作系统的write函数和fsync函数,说明如下:

为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失;因此系统同时提供了fsync、fdatasync等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。

AOF缓存区的同步文件策略由参数appendfsync控制,各个值的含义如下:

  • always:命令写入aof_buf后立即调用系统fsync操作同步到AOF文件,fsync完成后线程返回。这种情况下,每次有写命令都要同步到AOF文件,硬盘IO成为性能瓶颈,Redis只能支持大约几百TPS写入,严重降低了Redis的性能;即便是使用固态硬盘(SSD),每秒大约也只能处理几万个命令,而且会大大降低SSD的寿命。
  • no:命令写入aof_buf后调用系统write操作,不对AOF文件做fsync同步;同步由操作系统负责,通常同步周期为30秒。这种情况下,文件同步的时间不可控,且缓冲区中堆积的数据会很多,数据安全性无法保证。
  • everysec:命令写入aof_buf后调用系统write操作,write完成后线程返回;fsync同步文件操作由专门的线程每秒调用一次everysec是前述两种策略的折中,是性能和数据安全性的平衡,因此是Redis的默认配置,也是我们推荐的配置。

AOF流程-AOF文件重写原理

重写后的AOF文件为什么可以变小?有如下原因:

  1. 进程内已经超时的数据不再写入文件。
  2. 旧的AOF文件含有无效命令,如del key1、hdel key2、srem keys、set a111、set a222等。重写使用进程内数据直接生成,这样新的AOF文件只保留最终数据的写入命令。
  3. 多条写命令可以合并为一个,如:lpush list a、lpush list b、lpush list c可以转化为:lpush list a b c。为了防止单条命令过大造成客户端缓冲区溢 出,对于list、set、hash、zset等类型操作,以64个元素为界拆分为多条。

AOF重写降低了文件占用空间,除此之外,另一个目的是:更小的AOF 文件可以更快地被Redis加载

AOF重写过程可以手动触发和自动触发:

  • ·手动触发:直接调用bgrewriteaof命令。
  • ·自动触发:根据auto-aof-rewrite-min-size和auto-aof-rewrite-percentage参数确定自动触发时机

·auto-aof-rewrite-min-size:表示运行AOF重写时文件最小体积,默认 为64MB。

·auto-aof-rewrite-percentage:代表当前AOF文件空间 (aof_current_size)和上一次重写后AOF文件空间(aof_base_size)的比值。

自动触发时机=aof_current_size>auto-aof-rewrite-minsize&&(aof_current_size-aof_base_size)/aof_base_size>=auto-aof-rewritepercentage

其中aof_current_size和aof_base_size可以在info Persistence统计信息中查看。

 流程如下:

文件重写的流程如下

  1.  Redis父进程首先判断当前是否存在正在执行 bgsave/bgrewriteaof的子进程,如果存在则bgrewriteaof命令直接返回,如果存在bgsave命令则等bgsave执行完成后再执行。前面曾介绍过,这个主要是基于性能方面的考虑。
  2.  父进程执行fork操作创建子进程,这个过程中父进程是阻塞的。
    1.  父进程fork后,bgrewriteaof命令返回”Background append only file rewrite started”信息并不再阻塞父进程,并可以响应其他命令。Redis的所有写命令依然写入AOF缓冲区,并根据appendfsync策略同步到硬盘,保证原有AOF机制的正确。
    2. 由于fork操作使用写时复制技术,子进程只能共享fork操作时的内存数据。由于父进程依然在响应命令,因此Redis使用AOF重写缓冲区(图中的aof_rewrite_buf)保存这部分数据,防止新AOF文件生成期间丢失这部分数据。也就是说,bgrewriteaof执行期间,Redis的写命令同时追加到aof_buf和aof_rewirte_buf两个缓冲区。
  3. 子进程根据内存快照,按照命令合并规则写入到新的AOF文件。
    1. 子进程写完新的AOF文件后,向父进程发信号,父进程更新统计信息,具体可以通过info persistence查看。
    2. 父进程把AOF重写缓冲区的数据写入到新的AOF文件,这样就保证了新AOF文件所保存的数据库状态和服务器当前状态一致。
  4. 使用新的AOF文件替换老文件,完成AOF重写。

sheng的学习笔记-redis框架原理_第30张图片

RDB动态切成AOF机制

可视化的指令,如果开始是RDB的机制,想改成AOF机制,改了配置文件后会加载AOF文件,但AOF是空的,所以数据就没了,只能先加载RDB文件恢复数据,动态修改配置,生成AOF文件

sheng的学习笔记-redis框架原理_第31张图片

aof文件格式:

sheng的学习笔记-redis框架原理_第32张图片

两种持久化方式的对比

RDB的优缺点

RDB的优点:

  • ·RDB是一个紧凑压缩的二进制文件,代表Redis在某个时间点上的数据 快照。非常适用于备份,全量复制等场景。比如每6小时执行bgsave备份, 并把RDB文件拷贝到远程机器或者文件系统中(如hdfs),用于灾难恢复。
  • ·Redis加载RDB恢复数据远远快于AOF的方式。
  • 如果对于完整性要求不高,可以考虑RDB
  • 生产环境可以定期将RDB文件备份,用于恢复数据

RDB的缺点:

  • ·RDB方式数据没办法做到实时持久化/秒级持久化。生成快照数据属于重量级操作,频繁执行成本过高。
  • ·RDB文件使用特定二进制格式保存,Redis版本演进过程中有多个格式 的RDB版本,存在老版本Redis服务无法兼容新版RDB格式的问题。
  • 需要一定的时间间隔持久化时间,如果redis意外宕机了,最后一段时间的修改数据就没有了
  • 针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决。

aof的优缺点

aof的优点:

  1. AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
  2. AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。
  3. AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
  4. AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

aof的缺点:

  1. 对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
  2. AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

故障后恢复

在redis宕机后,如果有持久化文件,可以通过加载持久化文件恢复数据

加载RDB恢复数据

RDB文件的载入工作是在服务器启动时自动执行的,并没有专门的命令。但是由于AOF的优先级更高,因此当AOF开启时,Redis会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会在Redis服务器启动时检测RDB文件,并自动载入。服务器载入RDB文件期间处于阻塞状态,直到载入完成为止。

Redis启动日志中可以看到自动载入的执行:

Redis载入RDB文件时,会对RDB文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败。

加载AOF恢复数据

如果aof文件有错位(被改了内容),redis会启动不起来,需要修复aof文件,可以用redis自带的redis-check-aof来修复aof文件

sheng的学习笔记-redis框架原理_第33张图片

事务

特性

  1. redis单条命令保证原子性的,但是事务不保证原子性
  2. 事务没有隔离级别概念,所有命令在事务中,没有直接执行,只有发起执行命令的时候才会执行
  3. redis事务本质是一组命令一次性执行,一个事务的所有命令都会被序列化,在事务执行的过程中会顺序执行,在事务执行期间,服务器不会中断事务而去执行其它客户端的命令请求,它会将事务中的所有命令执行完毕,然后才去处理其它客户端的命令请求。

-----队列  set  set  set  执行----------------

redis事务步骤:

  • 开启事务

        MULTI命令的执行标志着事务的开始,执行完该命令后,客户端状态的flags属性会打开REDIS_MULTI标识,表示该客户端从非事务状态切换至事务状态

  • 命令入队

        当一个客户端处于事务状态时,这个客户端发送的命令,服务器是否会立即执行,分为以下2种情况:

  • 如果客户端发送的命令为以上4个命令外的其它命令,服务器不会立即执行这个命令,而是将其放到事务队列里,然后向客户端返回QUEUED回复。
  • 如果客户端发送的命令为MULTIEXECWATCHDISCARD四个命令中的其中1个,服务器会立即执行这个命令。

原理图

sheng的学习笔记-redis框架原理_第34张图片

然后提下事务队列,每个Redis客户端都有自己的事务状态,事务状态存储在客户端状态的mstates属性里:

sheng的学习笔记-redis框架原理_第35张图片

事务状态包含1个事务队列和1个已入队命令的数量,如下所示:

sheng的学习笔记-redis框架原理_第36张图片

事务队列是一个multiCmd类型的数组,数组中的每个multiCmd结构保存了一个已入队命令的相关信息,比如:

  1. 指向命令实现函数的指针,如GET命令、SET命令
  2. 命令的参数
  3. 参数的数量

事务队列以先进先出(FIFO)的方式保存入队的命令。

  • 执行事务

当一个处于事务状态的客户端执行EXEC命令时,服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令(按先入先出顺序),然后将执行命令的结果一次性返回给客户端。

redis事务命令:

127.0.0.1:6379> multi    #开启事务
OK
127.0.0.1:6379> set k1 v1    #命令入队
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k1   #在执行事务之前,命令没有处理,在队列中
QUEUED
127.0.0.1:6379> exec   #执行事务
1) OK
2) OK
3) OK
4) "v1"
127.0.0.1:6379>

127.0.0.1:6379> get k1
"v1"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> discard    #放弃事务
OK
127.0.0.1:6379> get k4    #放弃的事务没有执行,查不到k4的数据
(nil)

redis事务异常

编译型异常

代码有问题,命令有错,事务所有的命令都不会执行

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> getset asdf    #语法错误
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec   #语法错误执行报错
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k4   #语法报错命令没有执行,k1等也没有执行
(nil)

127.0.0.1:6379> get k1
(nil)

运行时异常

比如1/0,如果事务中存在语法性错误,执行命令时,其他命令正常执行,错误命令抛出异常

flushall

127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr k1   #注意,k1的内容是字符串,不能自增1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> get k3
QUEUED
127.0.0.1:6379> exec   #执行时报错,但其他命令执行成功
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
4) "v3"
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379> get k3
"v3"
127.0.0.1:6379>

redis锁(watch)

WATCH命令用于监视任意数量的数据库键,并在EXEC命令执行时,检测被监视的键是否被修改,如果被修改了,服务器将拒绝执行事务,并向客户端返回空回复

模拟转账场景演示watch:

模拟一下转账的情况,余额money:100块,转出10块后,余额money减少10块,out增加10块

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> watch money   #监视money
OK
127.0.0.1:6379> multi   #开启事务
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby out 20
QUEUED
127.0.0.1:6379> exec   #正常情况,没有其他线程干预变量,执行事务成功
1) (integer) 80
2) (integer) 20

模拟一下高并发情况

  1. 在下面截图中,左边是事务,命令入队列后不执行exec
  2. 用右边的控制台模拟别的线程,修改了moey
  3. 左边的事务执行exec失败 

sheng的学习笔记-redis框架原理_第37张图片

watch(监视)money这个变量,在事务执行过程中变量的值被更改,事务就执行失败了

如果发现事务执行失败,先解除监视,然后重新开启监视和事务

127.0.0.1:6379> unwatch       #解除监控
OK
127.0.0.1:6379> watch money   #重新监控和开启事务,最终成功
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby money 1
QUEUED
127.0.0.1:6379> incrby out 1
QUEUED
127.0.0.1:6379> exec
1) (integer) 999
2) (integer) 21

watch原理

流程如下:

  1. 使用WATCH命令监视数据库键
  2. 监视机制的触发
  3. 判断事务是否安全
  • 使用WATCH命令监视数据库键

每个Redis数据库都保存着1个watched_keys字典,这个字典的键是某个被WATCH命令监视的数据库键,字典的值是一个链表,链表中记录了所有监视相应数据库键的客户端。

sheng的学习笔记-redis框架原理_第38张图片

举个例子,假如客户端1正在监视键“name”,客户端2正在监视键“age”,那么watched_keys字典存储的数据大概如下:

sheng的学习笔记-redis框架原理_第39张图片

如果此时客户端3执行了以下WATCH命令:

那么watched_keys字典存储的数据就变为:

sheng的学习笔记-redis框架原理_第40张图片

  • 监视机制的触发

那么问题来了,既然watched_keys字典存储了被WATCH命令监视的键,那么监视机制是如何被触发的呢?

答案是所有对数据库修改的命令,比如SETLPUSHSADD等,在执行之后都会对watched_keys字典进行检查,如果有客户端正在监视刚刚被命令修改的键,那么所有监视该键的客户端的REDIS_DIRTY_CAS标识将被打开,表示该客户端的事务安全性已经被破坏。

以上图为例,如果键“name”的值被修改,那么客户端1、客户端3的REDIS_DIRTY_CAS标识会被打开。

  • 判断事务是否安全

最后非常关键的一步是,当服务器接收到一个客户端发来的EXEC命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS标识来决定是否执行事务,判断的流程图如下所示:

sheng的学习笔记-redis框架原理_第41张图片

分布式锁

Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对Redis的连接并不存在竞争关系,基于此,Redis中可以使用SETNX命令实现分布式锁。

SETNX——SET if Not eXists(如果不存在,则设置):

setnx key value

将 key 的值设为 value ,当且仅当 key 不存在。 
若给定的 key 已经存在,则 SETNX 不做任何动作。如果需要解锁,使用 del key 命令就能释放锁

解决死锁

如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?

答:给锁设置一个过期时间,可以通过两种方法实现:通过命令 “setnx 键名 过期时间 “;或者通过设置锁的expire时间,让Redis去删除锁。

第一种实现方式: 
使用 setnx key “当前系统时间+锁持有的时间”和getset key “当前系统时间+锁持有的时间”组合的命令就可以实现。 
具体做法如下:

客户端2发送SETNX lock.test 想要获得锁,由于之前的客户端1还持有锁,所以Redis返回一个0 
客户端2发送GET lock.test 以检查锁是否超时了,如果没超时,则等待或重试。 
反之,如果已超时,客户端2通过下面的操作来尝试获得锁: 
GETSET lock.test 过期的时间 
通过GETSET,客户端2拿到的时间戳如果仍然是超时的,那就说明,客户端2如愿以偿拿到锁了。 
如果在客户端2之前,有个客户端3比客户端2快一步执行了上面的操作,那么客户端2拿到的时间戳是个未超时的值,这时,说明客户端2没有如期获得锁,需要再次等待或重试。 
尽管客户端2没拿到锁,但它改写了客户端3设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。

第二种就非常简单了: 
通过Redis中expire()给锁设定最大持有时间,如果超过,则Redis来帮我们释放锁。

1.客户端1使用setnx获得了锁,并且使用expire设定一个过期时间,假定是10ms

2.过了4ms后,客户端1不幸运的宕机了,此时客户端2想要通过setnx尝试获得锁,但是锁还没有过期,任然被客户端1所持有。

3.到了11ms时,锁过期了,Redis帮我们删除了锁,此时客户端2想要通过setnx尝试获得锁,此时就能成功获得锁。

在实际过程中,我们可以设定一个时间T,用来表示客户端在初次尝试获得锁失败以后,在多次尝试获得锁所花的时间。如果次时间为0,表示除此尝试获得锁失败以后就不会再去尝试获得锁了。

分布式锁示例

sheng的学习笔记-redis框架原理_第42张图片

发布订阅

一、订阅频道和信息发布

命令:

发布订阅中使用到的命令就只有三个:PUBLISH,SUBSCRIBE,PSUBSCRIBE

  • PUBLISH 用于发布消息
  • SUBSCRIBE 也叫频道订阅,用于订阅某一特定的频道
  • PSUBSCRIBE 也叫模式订阅,用于订阅某一组频道,使用glob的方式,比如xxx-*可以匹配xxx-a,和xxx-b,xxx-ddd等等
示意图
功能说明:Redis 的 SUBSCRIBE 命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。

订阅例子示意图:下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:

sheng的学习笔记-redis框架原理_第43张图片

发布例子示意图:当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:

sheng的学习笔记-redis框架原理_第44张图片

二、订阅频道结构原理解析

说明:每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。

sheng的学习笔记-redis框架原理_第45张图片

操作:当客户端调用 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。
示意图:如果客户端 client10086 执行命令 SUBSCRIBE channel1 channel2 channel3 ,那么前面展示的 pubsub_channels 将变成下面这个样子,通过遍历所有输入频道。

sheng的学习笔记-redis框架原理_第46张图片

三、发布信息到频道结构解析

原理说明:当调用 PUBLISH channel message 命令, 程序首先根据 channel 定位到字典的键, 然后将信息发送给字典值链表中的所有客户端。
例子示意图:对于以下这个 pubsub_channels 实例, 如果某个客户端执行命令 PUBLISH channel1 "hello moto" ,那么 client2 、 client5 和 client1 三个客户端都将接收到 "hello moto" 信息,通过遍历订阅频道的所有客户端

sheng的学习笔记-redis框架原理_第47张图片

四、退订频道

原理:使用 UNSUBSCRIBE 命令可以退订指定的频道, 这个命令执行的是订阅的反操作: 它从 pubsub_channels 字典的给定频道(键)中, 删除关于当前客户端的信息, 这样被退订频道的信息就不会再发送给这个客户端。

模式订阅: pubsub_patterns

上述是订阅频道,可以使用订阅模式的方式订阅一组频道

client-7订阅music.*

client-8订阅book.*

client-9订阅news.*

sheng的学习笔记-redis框架原理_第48张图片

使用场景,与MQ对比

redis: 轻量级,低延迟,高并发,低可靠性;
mq:重量级,高可靠,异步,不保证实时;

使用场景:

  • 实时消息系统
  • 实时聊天(频道可以当做聊天室,将消息回显给所有人即可)
  • 订阅,关注系统都可以

复杂一点的场景还是要用MQ的,redis不保证可靠性,redis的发布订阅了解一下就行了

集群

主从复制:

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点

默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。一般情况下一主二从,可以使用slaveof ip port命令设置主机,也可以使用slaveof no one将自己变为主机

主从复制的作用

主从复制的作用主要包括:

  1. 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  2. 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  3. 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  4. 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

 示意图:

sheng的学习笔记-redis框架原理_第49张图片

 搭建环境:

搭建个一主二从的环境。

只修改从机的配置,默认都是主机,开一个主机查看信息

127.0.0.1:6379> info replication
# Replication
role:master   #角色,默认是master
connected_slaves:0   #从机信息,目前是0
master_replid:04da00caa777a6f939ab175adb7c8944bdebb944
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

复制2个应用

复制两个配置文件(我的是windows)

sheng的学习笔记-redis框架原理_第50张图片

修改一下配置文件,一个机器上开启多个实例,如果不修改默认项,会出现不同实例的默认文件冲突:

port 6379   #原本是6379

logfile "server_log-6380.txt"    #原本是 server_log.txt

dbfilename dump6380.rdb      #原本是dump.rdb 

如果修改了后台启动,要修改pidfile

 然后启动3个服务,在windows用 下面的命令启动,如果直接双击exe无法选择配置文件,从机没法起来 

./redis-server.exe redis.windows-service.conf

./redis-server.exe redis.windows-service-replication-port-80.conf

./redis-server.exe redis.windows-service-replication-port-81.conf


 测试一下,全都起来了:sheng的学习笔记-redis框架原理_第51张图片

sheng的学习笔记-redis框架原理_第52张图片

 将2个应用配置改为从机(79是主机,80和81是从机)

可以使用命令  slaveof host port方式动态修改从机,看下效果,80在设置命令后,变为从机,79再次查看信息,已经有一个从机了,从机端口6380

命令:

./redis-cli.exe -p 6379

./redis-cli.exe -p 6380

./redis-cli.exe -p 6381

sheng的学习笔记-redis框架原理_第53张图片

 sheng的学习笔记-redis框架原理_第54张图片

 但是上面是命令修改,是暂时的,应该在配置文件中修改,才是永久的

修改从机配置文件:

replicaof 127.0.0.1 6379   # replicaof     

两个配置文件都修改后,重新启动服务端和客户端,结果主库的数据已经同步到备库中(虽然备库没有操作过那些指令),结果图:最左边的是主机,中间的和右边的是从机

sheng的学习笔记-redis框架原理_第55张图片

搭建哨兵模式

在redis目录创建哨兵的配置文件sentinel.conf(自己新增加的文件,在Redis安装目录下有一个sentinel.conf文件,linux可以copy一份进行修改,windows要改一下,不然启动报错),我的配置文件如下:

port 26379
sentinel myid 0c3beaeecde4eca7055c159df3b3095e2d3da854
sentinel deny-scripts-reconfig yes
sentinel monitor mymaster 127.0.0.1 6379 1

启动哨兵模式:

windows命令:      ./redis-server.exe ./sentinel.conf --sentinel      #注意   --sentinel   这个别忘了写,不然报错

linux命令:

./redis-sentinel ../sentinel.conf

结果如下:

sheng的学习笔记-redis框架原理_第56张图片

 启动客户端和服务端(在上面已经有了,我就不发截图了)

sheng的学习笔记-redis框架原理_第57张图片

 现在将主机干掉

sheng的学习笔记-redis框架原理_第58张图片

 哨兵自动检测到主机挂了,投票到80为主机

sheng的学习笔记-redis框架原理_第59张图片

主备切换

默认情况下,主机和从机是写死的,在配置完成后,主机如果挂了,从机还是从机(不会自动变成主机)。从机只能读不能写,在主机启动后,从机依然可以从主机获取到数据,所以需要哨兵模式自动处理

如果使用命令行配置的主从配置,在从机宕机后重启,从机就会变成主机,只有再次变成从机,才会从主机同步数据,并且在从机宕机的时间段内,主机的新增数据也会同步到从机中

主备同步原理:

全量同步

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

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

sheng的学习笔记-redis框架原理_第60张图片

完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。

增量同步

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

redis主从同步策略

主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。

主备同步-部分的重同步原理

完整的重同步创建RDB文件、发送RDB文件会消耗主服务器的CPU、内存和磁盘IO等资源,同时也会占用大量的网络带宽和流量。在实际中,有时完整的重同步是没有必要的,例如当从服务器与主服务器网络连接断开时间很短,数据的不一致可能就是为数不多的几条写命令,这时却要进行全量数据的复制,显然是资源的浪费。其实只需要将断开连接期间的数据进行同步就可以完成数据的一致性。

完整的重同步只应该用于首次复制,或者万不得已需要全量复制时才执行,由于全量复制在主节点数据量较大时效率太低,因此Redis2.8开始提供部分复制,用于处理网络中断时的数据同步

复制偏移量

主节点和从节点分别维护一个复制偏移量(offset),代表的是主节点向从节点传递的字节数;主节点每次向从节点传播N个字节数据时,主节点的offset增加N;从节点每次收到主节点传来的N个字节数据时,从节点的offset增加N。

offset用于判断主从节点的数据库状态是否一致:如果二者offset相同,则一致;如果offset不同,则不一致,此时可以根据两个offset找出从节点缺少的那部分数据。例如,如果主节点的offset是1000,而从节点的offset是500,那么部分复制就需要将offset为501-1000的数据传递给从节点。而offset为501-1000的数据存储的位置,就是下面要介绍的复制积压缓冲区。

复制积压缓冲区:

复制积压缓冲区是由主节点维护的、固定长度的、先进先出(FIFO)队列,默认大小1MB;当主节点开始有从节点时创建,其作用是备份主节点最近发送给从节点的数据。注意,无论主节点有一个还是多个从节点,都只需要一个复制积压缓冲区。

在命令传播阶段,主节点除了将写命令发送给从节点,还会发送一份给复制积压缓冲区,作为写命令的备份;除了存储写命令,复制积压缓冲区中还存储了其中的每个字节对应的复制偏移量(offset)。由于复制积压缓冲区定长且是先进先出,所以它保存的是主节点最近执行的写命令;时间较早的写命令会被挤出缓冲区。

由于该缓冲区长度固定且有限,因此可以备份的写命令也有限,当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。反过来说,为了提高网络中断时部分复制执行的概率,可以根据需要增大复制积压缓冲区的大小(通过配置repl-backlog-size);例如如果网络中断的平均时间是60s,而主节点平均每秒产生的写命令(特定协议格式)所占的字节数为100KB,则复制积压缓冲区的平均需求为6MB,保险起见,可以设置为12MB,来保证绝大多数断线情况都可以使用部分复制。

从节点将offset发送给主节点后,主节点根据offset和缓冲区大小决定能否执行部分复制:

  • 如果offset偏移量之后的数据,仍然都在复制积压缓冲区里,则执行部分复制;
  • 如果offset偏移量之后的数据已不在复制积压缓冲区中(数据已被挤出),则执行全量复制。

sheng的学习笔记-redis框架原理_第61张图片

服务器运行ID

每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。通过info Server命令,可以查看节点的runid:

主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制:

  • 如果从节点保存的runid与主节点现在的runid相同,说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
  • 如果从节点保存的runid与主节点现在的runid不同,说明从节点在断线前同步的Redis节点并不是当前的主节点,只能进行全量复制。

psync命令的执行:

Redis在2.8版本提供了PSYNC命令来带代替SYNC命令,为Redis主从复制提供了部分复制的能力,主从节点使用PSYNC命令jeuding全量复制还是部分复制的

psync命令的执行过程可以参见下图(图片来源:《Redis设计与实现》):

sheng的学习笔记-redis框架原理_第62张图片

部分的重同步过程:

  1. 首先,从节点根据当前状态,决定如何调用psync命令:        
    1.  如果从节点之前未执行过slaveof或最近执行了slaveof no one,则从节点发送命令为psync ? -1,从服务器会保存主服务器的运行ID,向主节点请求全量复制
    2. 如果从节点之前执行了slaveof,则发送命令为psync ,其中runid为上次复制的主节点的runid,offset为上次复制截止时从节点保存的复制偏移量(当从服务器断线后,重新连接主服务器场景)。
  2. 主节点根据收到的psync命令,如果主节点版本低于Redis2.8,则返回-ERR回复,此时从节点重新发送sync命令执行全量复制,如果是新版本,走往下走;
  3. 比较保存的主服务器运行ID与现在连接的主服务器运行ID,如果不同说明和之前连接的不是同一台服务器,需要执行完整的重同步
  4. 如果运行ID一致,尝试执行部分的重同步。从服务器会将自己的复制偏移量发送给主服务器。
  5. 主服务器收到从服务器发来的复制偏移量,在自己维护的复制积压缓冲区寻找,如果偏移量之后的数据仍旧存在于复制积压缓冲区中,执行部分的重同步。如果偏移量之后的数据不在复制积压缓冲区中,执行完整的重同步

主从超时检测

redis的主从超时检测主要从以下方面进行判断,分别是主监测从、从监测主。

  • 主监测从:slave定期发送replconf ack offset命令到master来报告自己的存活状况
  • 从监测主:master定期发送ping命令或者\n命令到slave来报告自己的存活状况

从监测主(ping)

每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。

PING发送的频率由repl-ping-slave-period参数控制,单位是秒,默认值是10s。

主监测从(REPLCONF ACK)

在命令传播阶段,从节点会向主节点发送REPLCONF ACK命令,频率是每秒1次;命令格式为:REPLCONF ACK {offset},其中offset指从节点保存的复制偏移量。REPLCONF ACK命令的作用包括:

(1)实时监测主从节点网络状态:该命令会被主节点用于复制超时的判断。此外,在主节点中使用info Replication,可以看到其从节点的状态中的lag值,代表的是主节点上次收到该REPLCONF ACK命令的时间间隔,在正常情况下,该值应该是0或1,如下图所示:

(2)检测命令丢失:从节点发送了自身的offset,主节点会与自己的offset对比,如果从节点数据缺失(如网络丢包),主节点会推送缺失的数据(这里也会利用复制积压缓冲区)。注意,offset和复制积压缓冲区,不仅可以用于部分复制,也可以用于处理命令丢失等情形;区别在于前者是在断线重连后进行的,而后者是在主从节点没有断线的情况下进行的。

(3)辅助保证从节点的数量和延迟:Redis主节点中使用min-slaves-to-write和min-slaves-max-lag参数,来保证主节点在不安全的情况下不会执行写命令;所谓不安全,是指从节点数量太少,或延迟过高。例如min-slaves-to-write和min-slaves-max-lag分别是3和10,含义是如果从节点数量小于3个,或所有从节点的延迟值都大于10s,则主节点拒绝执行写命令。而这里从节点延迟值的获取,就是通过主节点接收到REPLCONF ACK命令的时间来判断的,即前面所说的info Replication中的lag值。

哨兵模式原理(自动选举)

哨兵概述

主从切换技术方法是:当主服务器宕机后,手动将一台从服务器切换为主服务器,需要人工处理,费时费力并且响应速度慢,会造成一段时间内服务不可用,所以引入哨兵模式,自动处理

Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入。哨兵的核心功能是主节点的自动故障转移,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

当哨兵检测的主机挂了,会选举一个从机当主机,原本的主机再次启动后会变成从机。下面是Redis官方文档对于哨兵功能的描述:

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

它由两部分组成,哨兵节点和数据节点:

  • 哨兵节点:哨兵系统由一个或多个哨兵节点组成,哨兵节点是特殊的redis节点,不存储数据。
  • 数据节点:主节点和从节点都是数据节点。

sheng的学习笔记-redis框架原理_第63张图片

多哨兵模式

然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。

用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。


 

sheng的学习笔记-redis框架原理_第64张图片

哨兵模式原理

Sentinel 并不使用数据库,它不能存储数据,他也不会加载RDB文件或AOF文件。在启动之后,服务器会初始化一个sentinel.c/sentinelState结构,这个结构中保存了服务器中所有和Sentinel功能有关的状态

在初始化Sentinel之后,就会建立与被监控主服务器的网络连接,Sentinel 将成为主服务器的客户端,这个客户端可以向主服务器发送命令,并从中获取相关的服务器信息。Sentinel在监控主服务器后,它们之间会建立两个异步网络连接。

  • 一个是命令连接, 这个连接专用于向主服务器发送命令,并获取返回的信息, 以此来与主服务器一直保持通信状态。
  • 另一个是消息订阅连接,这个连接专用于订阅主服务器的_sentinel_hello频道消息。在目前Redis的消息订阅发布功能中,被发送的消息并不会被保存到Redis服务器中。在发送消息的过程中,如果订阅消息的客户端离线或者因为其他原因而接收不到消息。那么这条消息将会丢失。为了保证订阅的_sentinel_hello频道的消息在传输过程中不丢失,Sentinel专门建立了一个订阅该频道消息的网络连接。

三个定时任务

  1.   每10秒每个 sentinel 对master 和 slave 执行info 命令:该命令第一个是用来发现slave节点,第二个是确定主从关系.
  2.   每2秒每个 sentinel 通过 master 节点的 channel(名称为_sentinel_:hello) 交换信息(pub/sub):用来交互对节点的看法(后面会介绍的节点主观下线和客观下线)以及自身信息.
  3.   每1秒每个 sentinel 对其他 sentinel 和 redis 执行 ping 命令,用于心跳检测,作为节点存活的判断依据.

当一台 sentinel 发现一个 Redis 服务无法 ping 通时,就标记为 主观下线 sdown;同时另外的 sentinel 服务也发现该 Redis 服务宕机,也标记为 主观下线,当多台 sentinel (大于等于2,在配置的最后一个)时,都标记该Redis服务宕机,这时候就变为客观下线了,然后进行故障转移.

故障转移-哨兵选举

  故障转移是由 sentinel 领导者节点来完成的(只需要一个sentinel节点),关于 sentinel 领导者节点的选取也是每个 sentinel 向其他 sentinel 节点发送我要成为领导者的命令,超过半数sentinel 节点同意,并且也大于quorum ,那么他将成为领导者,如果有多个sentinel都成为了领导者,则会过段时间在进行选举.

故障转移-主机选举

  sentinel 领导者节点选举出来后,会通过如下几步进行故障转移:

  从 slave 节点中选出一个合适的 节点作为新的master节点.这里的合适包括如下几点:

  1. 选择 slave-priority(slave节点优先级)最高的slave节点,如果存在则返回,不存在则继续下一步判断.
  2. 选择复制偏移量最大的 slave 节点(复制的最完整),如果存在则返回,不存在则继续.
  3. 选择runId最小的slave节点(启动最早的节点)

故障转移

  1. 对上面选出来的 slave 节点执行 slaveof no one 命令让其成为新的 master 节点.
  2. 向剩余的 slave 节点发送命令,让他们成为新master 节点的 slave 节点,复制规则和前面设置的 parallel-syncs 参数有关.
  3. 更新原来master 节点配置为 slave 节点,并保持对其进行关注,一旦这个节点重新恢复正常后,会命令它去复制新的master节点信息.(注意:原来的master节点恢复后是作为slave的角色)

  可以从 sentinel 日志中出现的几个消息来进行查看故障转移:

  1.+switch-master:表示切换主节点(从节点晋升为主节点)

  2.+sdown:主观下线

  3.+odown:客观下线

  4.+convert-to-slave:切换从节点(原主节点降为从节点)

哨兵模式优缺点

优点:
1、哨兵集群,基于主从复制模式,所有的主从配置优点,它都有
2、主从可以切换,故障可以转移,高可用性的系统
3、哨兵模式就是主从模式的升级,手动到自动,更加健壮
缺点:
1、Redis不好在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦
2、哨兵模式的配置繁琐

参考文章:

主要是参考这个文章,里面讲解很详细

【狂神说Java】Redis最新超详细版教程通俗易懂_哔哩哔哩_bilibili

Redis系列(九):Redis的事务机制 - 申城异乡人 - 博客园

Redis事务机制和分布式锁 - 请叫我老焦 - 博客园

Redis 安装 | 菜鸟教程

深入学习Redis(2):持久化 - 编程迷思 - 博客园

Redis主从复制原理 - 简书

Redis持久化 - 简书

深入学习Redis(3):主从复制 - 编程迷思 - 博客园

Redis详解(九)------ 哨兵(Sentinel)模式详解_IT可乐-CSDN博客_redis哨兵模式

Redis总结(五)缓存雪崩和缓存穿透等问题 - 章为忠 - 博客园

REDIS缓存穿透,缓存击穿,缓存雪崩原因+解决方案 - 大码哥 - 博客园

应对缓存击穿的解决方法_xusanyao的博客-CSDN博客_缓存击穿

Redis的缓存穿透及解决方法——布隆过滤器BloomFilter_攻城狮Kevin-CSDN博客_redis缓存穿透 布隆

缓存穿透及解决方案 - 知乎

redis为什么快?_redis为什么速度快_乱糟的博客-CSDN博客 

Redis为什么会那么快? - 简书 

百度安全验证 

Redis为什么这么快?_redis为什么速度快_敖 丙的博客-CSDN博客 

应用 5:层峦叠嶂——redis布隆过滤器

书 《Redis设计与实现》

你可能感兴趣的:(框架分析,redis,数据库,缓存,分布式)