摘要: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)。
下载地址:https://github.com/tporadowski/redis/releases。
Redis 支持 32 位和 64 位。这个需要根据你系统平台的实际情况选择,这里我们下载 Redis-x64-xxx.zip。
打开一个 cmd 窗口 使用 cd 命令切换目录到安装目录(或者双击exe文件也行)
redis-server.exe redis.windows.conf
这时候另启一个 cmd 窗口,原来的不要关闭,不然就无法访问服务端了。
切换到 redis 目录下运行:
redis-cli.exe -h 127.0.0.1 -p 6379
使用ping命令,如果看到结果是pong,说明测试连接成功
搭建完成
下载地址: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 #清除数据
OK127.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 [-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 |
结果分析:
====== 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万
记不住命令具体用法可以去官网点击命令
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 #清除数据
OK127.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) -2127.0.0.1:6379> keys *
1) "age"
2) "name"
127.0.0.1:6379> type name #查看指定key的类型
string
127.0.0.1:6379> exists name #看是否存在
(integer) 1127.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"
value除了是我们的字符串还可以是数字,可以用于:
在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 操作将任务取出进行执行。
值不能重复的集合,命令一般以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) 3127.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"
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"
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) 2127.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更适合对象的存储,string更适合字符串的存储
hash可以存放一些用户信息的保存,尤其是经常变动的信息,比如:
127.0.0.1:6379> hset user:1 name myname
(integer) 1
127.0.0.1:6379> hget user:1 name
"myname"
在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
set排序,存放班级成绩表,工资表排序
普通消息权重是1,重要消息权重是2,带权重进行判断,比如b站的播放量进行排序,排行榜,热搜之类的
输入经度维度信息,可以推算地理位置的信息,两地之间的距离,方圆几里之内的人,命令通常以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底层是用zset实现的,所以可以用zset来操作geo的数据,比如:
127.0.0.1:6379> zrange china:city 0 -1
1) "chongqing"
2) "shanghai"
3) "beijing"
4) "xian"
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>
网页的UV(一个人访问一个网站多次,但还算一个人),传统的方式用set保存用户ID,然后统计SET的元素数量,但这种方式需要统计大量的用户ID,此时可以用HyperLogLog。
如果允许误差,用HyperLogLog,如果不允许误差,可以用set或者自己的数据类型
位存储,格式 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) 0127.0.0.1:6379> getbit sign 3 #查找周三是否打卡了
(integer) 1
127.0.0.1:6379> bitcount sign #统计打卡天数
(integer) 3
统计用户:活跃,不活跃。登陆,没登录,打卡等
统计疫情人数,每个人是一位,感染了是1,没感染是0
使用java操作redis,jedis是redis官网推荐的java连接工具,使用java操作redis中间件,
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"));
}
}
结果如下:
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(); //关闭连接
}
}
}
结果:
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和端口,所以主机和从机直接的切换,在客户端是无感知的,由哨兵统筹
结果如下:
Redis 是基于内存的数据库,那不可避免的就要与磁盘数据库做对比。对于磁盘数据库来说,是需要将数据读取到内存里的,这个过程会受到磁盘 I/O 的限制。
而对于内存数据库来说,本身数据就存在于内存里,也就没有了这方面的开销
Redis 是单线程的。多线程在执行过程中需要进行 CPU 的上下文切换,这个操作比较耗时。Redis 又是基于内存实现的,对于内存来说,没有上下文切换效率就是最高的。多次读写都在一个CPU 上,对于内存来说就是最佳方案。
接收到用户的请求后,全部推送到一个队列里,然后交给文件事件分派器,而它是单线程的工作方式。Redis 又是基于它工作的,所以说 Redis 是单线程的
升级后的redis变为多线程,这里的多线程指的是IO处理,但核心处理逻辑还是单线程,看下IO处理的演变,增加了IO的吞吐量
到了Redis6.0之后:
使用io多路复用和reactor设计模式,可以增加IO处理速度
reactor设计模式:
sheng的学习笔记-Reactor 模式_coldstarry的博客-CSDN博客
IO多路复用可以参考
sheng的学习笔记-IO多路复用,NIO,BIO,AIO_coldstarry的博客-CSDN博客
IO复用只需要阻塞在select,poll或者epoll,可以同时处理和管理多个连接。
Redis 的底层数据结构一共有6种,分别是,简单动态字符串,双向链表,压缩列表,哈希表,跳表和整数数组,它们和数据类型的对应关系如下图所示:
自己构建了一种简单动态字符串的抽象类型,并将SDS用作Redis的默认字符串表示
(1)字符串长度处理
这个图是字符串在 C 语言中的存储方式,想要获取 「Redis」的长度,需要从头开始遍历,直到遇到 '\0' 为止。
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 更多是被当作队列或栈来使用的。队列和栈的特性一个先进先出,一个先进后出。双端链表很好的支持了这些特性。
(1)前后节点
链表里每个节点都带有两个指针,prev 指向前节点,next 指向后节点。这样在时间复杂度为 O(1) 内就能获取到前后节点。
(2)头尾节点
头节点里有 head 和 tail 两个参数,分别指向头节点和尾节点。这样的设计能够对双端节点的处理时间复杂度降至 O(1) ,对于队列和栈来说再适合不过。同时链表迭代时从两端都可以进行
(3)链表长度
头节点里同时还有一个参数 len,和上边提到的 SDS 里类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到 len 值就可以了,这个时间复杂度是 O(1)。
如果在一个链表节点中存储一个小数据,比如一个字节。那么对应的就要保存头节点,前后指针等额外的数据。
这样就浪费了空间,同时由于反复申请与释放也容易导致内存碎片化。这样内存的使用效率就太低了。
它是经过特殊编码,专门为了提升内存使用效率设计的。所有的操作都是通过指针与解码出来的偏移量进行的。
并且压缩列表的内存是连续分配的,遍历的速度很快。
Redis 作为 K-V 型数据库,所有的键值都是用字典来存储的。
日常学习中使用的字典你应该不会陌生,想查找某个词通过某个字就可以直接定位到,速度非常快。这里所说的字典原理上是一样的,通过某个 key 可以直接获取到对应的value。
字典又称为哈希表,这点没什么可说的。哈希表的特性大家都很清楚,能够在 O(1) 时间复杂度内取出和插入关联的值。
作为 Redis 中特有的数据结构-跳跃表,其在链表的基础上增加了多级索引来提升查找效率。
这是跳跃表的简单原理图,每一层都有一条有序的链表,最底层的链表包含了所有的元素。这样跳跃表就可以支持在 O(logN) 的时间复杂度里查找到对应的节点。
下面这张是跳表真实的存储结构,和其它数据结构一样,都在头节点里记录了相应的信息,减少了一些不必要的系统开销。
合理的数据编码
对于每一种数据类型来说,底层的支持可能是多种数据结构,什么时候使用哪种数据结构,这就涉及到了编码转化的问题。
那我们就来看看,不同的数据类型是如何进行编码转化的:
String:存储数字的话,采用int类型的编码,如果是非数字的话,采用 raw 编码;
List:字符串长度及元素个数小于一定范围使用 ziplist 编码,任意条件不满足,则转化为 linkedlist 编码;
Hash:hash 对象保存的键值对内的键和值字符串长度小于一定值及键值对;
Set:保存元素为整数及元素个数小于一定范围使用 intset 编码,任意条件不满足,则使用 hashtable 编码;
Zset:zset 对象中保存的元素个数小于及成员长度小于一定值使用 ziplist 编码,任意条件不满足,则使用 skiplist 编码。
前台请求,后台先从缓存中取数据,取到直接返回结果,取不到时从数据库中取,数据库取到更新缓存,并返回结果,数据库也没取到,那直接返回空结果。
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
过滤规则目前主流的一种的载体就是布隆过滤器. 布隆过滤器是一种概率型数据结构,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”.
相比于传统的 List、Set、Map 等数据结构,布隆过滤器是一个bit数组, 它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
如果我们要映射一个值到布隆过滤器中,我们需要使用多个不同的哈希函数生成多个哈希值,使用多个 hash 函数对 key 进行 hash 算得一个整数索引值,然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作,例如针对值 “zhangsan” 和三个不同的哈希函数分别生成了哈希值 1、4、7
我们现在再存一个值 “lisi”,如果哈希函数返回 4、5、8 的话,图继续变为:
当我们想要判断布隆过滤器是否记录了某个数据时,布隆过滤器会先对该数据进行同样的哈希处理, 比如 “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
List
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
}
return result;
}
这种方式确实能够防止缓存失效时高并发到数据库,但是缓存没有失效的时候,在从缓存中拿数据时需要排队取锁,这必然会大大的降低了系统的吞吐量.
方法2
// 方法2:
static Object lock = new Object();
public List
List
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
synchronized (lock) {
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
}
}
return result;
}
这个方法在缓存命中的时候,系统的吞吐量不会受影响,但是当缓存失效时,请求还是会打到数据库,只不过不是高并发而是阻塞而已.但是,这样会造成用户体验不佳,并且还给数据库带来额外压力.
方法3
//方法3
public List
List
// 从缓存读取数据
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
List
// 从缓存读取数据
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个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法
缓存击穿中已经讲解过,看上面就有详细的方案,可以解决雪崩的问题
给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓存标记失效,则更新数据缓存,这个距离
这样做后,就可以一定程度上提高系统吞吐量。
这种方式跟缓存击穿的方案二检查更新很像
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,可以用这种方式减少损失
Redis是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将Redis中的数据以某种形式(数据或命令)从内存保存到硬盘;当下次Redis重启时,利用持久化文件实现数据恢复。除此之外,为了进行灾难备份,可以将持久化文件拷贝到一个远程位置。持久化机制有RDB(Redis DataBase)和AOF(Append Only File)
既然redis的数据可以保存在磁盘上,那么这个流程是什么样的呢?
要有下面五个过程:
这5个过程是在理想条件下一个正常的保存流程,但是在大多数情况下,我们的机器等等都会有各种各样的故障,这里划分了两种情况:
RDB(Redis DataBase)持久化是将当前进程中的数据生成快照保存到硬盘(因此也称作快照持久化),保存的文件后缀是rdb;当Redis重新启动时,可以读取快照文件恢复数据。
redis提供了三种机制:save(手动触发)、bgsave(手动触发)、自动化
手动执行该命令,该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止。
执行完成时候如果存在老的RDB文件,就把新的替代掉旧的。我们的客户端可能都是几万或者是几十万,这种方式显然不可取,线上环境要杜绝save的使用。具体流程如下:
手动执行该命令
Redis进程执行fork操作创建子进程,RDB持久化过程由子 进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短。具体流程是:
RDB文件的处理
保存:RDB文件保存在dir配置指定的目录下,文件名通过dbfilename配 置指定。可以通过执行config set dir{newDir}和config set dbfilename{newFileName}运行期动态执行,当下次运行时RDB文件会保存到新目录。
配置文件截图:
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文件格式如下图所示(图片来源:《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值比较,判断文件是否损坏。
Redis默认采用LZF算法对RDB文件进行压缩。虽然压缩耗时,但是可以大大减小RDB文件的体积,因此压缩默认开启;可以通过命令关闭:
需要注意的是,RDB文件的压缩并不是针对整个文件进行的,而是对数据库中的字符串进行的,且只有在字符串达到一定长度(20字节)时才会进行。
AOF(append only file)将Redis执行的每次写命令(读请求不记录到文件中)记录到单独的日志文件中(有点像MySQL的binlog);当Redis重启时再次执行AOF文件中的命令来恢复数据。
Redis服务器默认开启RDB,关闭AOF;要开启AOF,需要在配置文件中配置:appendonly yes
开启AOF功能需要设置配置:appendonly yes,默认不开启。AOF文件名 通过appendfilename配置设置,默认文件名是appendonly.aof。保存路径同 RDB持久化方式一致,通过dir配置指定。AOF的工作流程操作:命令写入 (append)、文件同步(sync)、文件重写(rewrite)、重启加载 (load),流程图如下:
Redis先将写命令追加到缓冲区,而不是直接写入文件,主要是为了避免每次有写命令都直接写入硬盘,导致硬盘IO成为Redis负载的瓶颈。
命令追加的格式是Redis命令请求的协议格式,它是一种纯文本格式,具有兼容性好、可读性强、容易处理、操作简单避免二次开销等优点;具体格式略。在AOF文件中,除了用于指定数据库的select命令(如select 0 为选中0号数据库)是由Redis添加的,其他都是客户端发送来的写命令。
AOF为什么把命令追加到aof_buf中?Redis使用单线程响应命令,如 果每次写AOF文件命令都直接追加到硬盘,那么性能完全取决于当前硬盘负 载。先写入缓冲区aof_buf中,还有另一个好处,Redis可以提供多种缓冲区同步硬盘的策略,在性能和安全性方面做出平衡
Redis提供了多种AOF缓存区的同步文件策略,策略涉及到操作系统的write函数和fsync函数,说明如下:
为了提高文件写入效率,在现代操作系统中,当用户调用write函数将数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区被填满或超过了指定时限后,才真正将缓冲区的数据写入到硬盘里。这样的操作虽然提高了效率,但也带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失;因此系统同时提供了fsync、fdatasync等同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保数据的安全性。
AOF缓存区的同步文件策略由参数appendfsync控制,各个值的含义如下:
重写后的AOF文件为什么可以变小?有如下原因:
AOF重写降低了文件占用空间,除此之外,另一个目的是:更小的AOF 文件可以更快地被Redis加载
AOF重写过程可以手动触发和自动触发:
·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统计信息中查看。
流程如下:
文件重写的流程如下:
可视化的指令,如果开始是RDB的机制,想改成AOF机制,改了配置文件后会加载AOF文件,但AOF是空的,所以数据就没了,只能先加载RDB文件恢复数据,动态修改配置,生成AOF文件
RDB的优点:
RDB的缺点:
aof的优点:
aof的缺点:
在redis宕机后,如果有持久化文件,可以通过加载持久化文件恢复数据
RDB文件的载入工作是在服务器启动时自动执行的,并没有专门的命令。但是由于AOF的优先级更高,因此当AOF开启时,Redis会优先载入AOF文件来恢复数据;只有当AOF关闭时,才会在Redis服务器启动时检测RDB文件,并自动载入。服务器载入RDB文件期间处于阻塞状态,直到载入完成为止。
Redis启动日志中可以看到自动载入的执行:
Redis载入RDB文件时,会对RDB文件进行校验,如果文件损坏,则日志中会打印错误,Redis启动失败。
如果aof文件有错位(被改了内容),redis会启动不起来,需要修复aof文件,可以用redis自带的redis-check-aof来修复aof文件
-----队列 set set set 执行----------------
MULTI
命令的执行标志着事务的开始,执行完该命令后,客户端状态的flags属性会打开REDIS_MULTI标识,表示该客户端从非事务状态切换至事务状态
当一个客户端处于事务状态时,这个客户端发送的命令,服务器是否会立即执行,分为以下2种情况:
QUEUED
回复。MULTI
、EXEC
、WATCH
、DISCARD
四个命令中的其中1个,服务器会立即执行这个命令。原理图:
然后提下事务队列,每个Redis客户端都有自己的事务状态,事务状态存储在客户端状态的mstates属性里:
事务状态包含1个事务队列和1个已入队命令的数量,如下所示:
事务队列是一个multiCmd类型的数组,数组中的每个multiCmd结构保存了一个已入队命令的相关信息,比如:
事务队列以先进先出(FIFO)的方式保存入队的命令。
当一个处于事务状态的客户端执行EXEC
命令时,服务器会遍历这个客户端的事务队列,执行队列中保存的所有命令(按先入先出顺序),然后将执行命令的结果一次性返回给客户端。
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)
代码有问题,命令有错,事务所有的命令都不会执行
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>
WATCH
命令用于监视任意数量的数据库键,并在EXEC
命令执行时,检测被监视的键是否被修改,如果被修改了,服务器将拒绝执行事务,并向客户端返回空回复
模拟一下转账的情况,余额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
模拟一下高并发情况
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
流程如下:
每个Redis数据库都保存着1个watched_keys
字典,这个字典的键是某个被WATCH命令监视的数据库键,字典的值是一个链表,链表中记录了所有监视相应数据库键的客户端。
举个例子,假如客户端1正在监视键“name”,客户端2正在监视键“age”,那么watched_keys
字典存储的数据大概如下:
如果此时客户端3执行了以下WATCH
命令:
那么watched_keys
字典存储的数据就变为:
那么问题来了,既然watched_keys
字典存储了被WATCH命令监视的键,那么监视机制是如何被触发的呢?
答案是所有对数据库修改的命令,比如SET
、LPUSH
、SADD
等,在执行之后都会对watched_keys
字典进行检查,如果有客户端正在监视刚刚被命令修改的键,那么所有监视该键的客户端的REDIS_DIRTY_CAS
标识将被打开,表示该客户端的事务安全性已经被破坏。
以上图为例,如果键“name”的值被修改,那么客户端1、客户端3的REDIS_DIRTY_CAS
标识会被打开。
最后非常关键的一步是,当服务器接收到一个客户端发来的EXEC
命令时,服务器会根据这个客户端是否打开了REDIS_DIRTY_CAS
标识来决定是否执行事务,判断的流程图如下所示:
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,表示除此尝试获得锁失败以后就不会再去尝试获得锁了。
发布订阅中使用到的命令就只有三个:PUBLISH,SUBSCRIBE,PSUBSCRIBE
示意图 功能说明:Redis 的 SUBSCRIBE 命令可以让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。 订阅例子示意图:下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系:
发布例子示意图:当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
说明:每个 Redis 服务器进程都维持着一个表示服务器状态的 redis.h/redisServer 结构, 结构的 pubsub_channels 属性是一个字典, 这个字典就用于保存订阅频道的信息,其中,字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。
操作:当客户端调用 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。 示意图:如果客户端 client10086 执行命令 SUBSCRIBE channel1 channel2 channel3 ,那么前面展示的 pubsub_channels 将变成下面这个样子,通过遍历所有输入频道。
原理说明:当调用 PUBLISH channel message 命令, 程序首先根据 channel 定位到字典的键, 然后将信息发送给字典值链表中的所有客户端。 例子示意图:对于以下这个 pubsub_channels 实例, 如果某个客户端执行命令 PUBLISH channel1 "hello moto" ,那么 client2 、 client5 和 client1 三个客户端都将接收到 "hello moto" 信息,通过遍历订阅频道的所有客户端
原理:使用 UNSUBSCRIBE 命令可以退订指定的频道, 这个命令执行的是订阅的反操作: 它从 pubsub_channels 字典的给定频道(键)中, 删除关于当前客户端的信息, 这样被退订频道的信息就不会再发送给这个客户端。
上述是订阅频道,可以使用订阅模式的方式订阅一组频道
client-7订阅music.*
client-8订阅book.*
client-9订阅news.*
redis: 轻量级,低延迟,高并发,低可靠性;
mq:重量级,高可靠,异步,不保证实时;
使用场景:
复杂一点的场景还是要用MQ的,redis不保证可靠性,redis的发布订阅了解一下就行了
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。
默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。一般情况下一主二从,可以使用slaveof ip port命令设置主机,也可以使用slaveof no one将自己变为主机
主从复制的作用主要包括:
搭建个一主二从的环境。
只修改从机的配置,默认都是主机,开一个主机查看信息
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
复制两个配置文件(我的是windows)
修改一下配置文件,一个机器上开启多个实例,如果不修改默认项,会出现不同实例的默认文件冲突:
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
可以使用命令 slaveof host port方式动态修改从机,看下效果,80在设置命令后,变为从机,79再次查看信息,已经有一个从机了,从机端口6380
命令:
./redis-cli.exe -p 6379
./redis-cli.exe -p 6380
./redis-cli.exe -p 6381
但是上面是命令修改,是暂时的,应该在配置文件中修改,才是永久的
修改从机配置文件:
replicaof 127.0.0.1 6379 # replicaof
两个配置文件都修改后,重新启动服务端和客户端,结果主库的数据已经同步到备库中(虽然备库没有操作过那些指令),结果图:最左边的是主机,中间的和右边的是从机
在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
结果如下:
启动客户端和服务端(在上面已经有了,我就不发截图了)
现在将主机干掉
哨兵自动检测到主机挂了,投票到80为主机
主备切换
默认情况下,主机和从机是写死的,在配置完成后,主机如果挂了,从机还是从机(不会自动变成主机)。从机只能读不能写,在主机启动后,从机依然可以从主机获取到数据,所以需要哨兵模式自动处理
如果使用命令行配置的主从配置,在从机宕机后重启,从机就会变成主机,只有再次变成从机,才会从主机同步数据,并且在从机宕机的时间段内,主机的新增数据也会同步到从机中
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
完成上面几个步骤后就完成了从服务器数据初始化的所有操作,从服务器此时可以接收来自用户的读请求。
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
主从刚刚连接的时候,进行全量同步;全同步结束后,进行增量同步。当然,如果有需要,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和缓冲区大小决定能否执行部分复制:
每个Redis节点(无论主从),在启动时都会自动生成一个随机ID(每次启动都不一样),由40个随机的十六进制字符组成;runid用来唯一识别一个Redis节点。通过info Server命令,可以查看节点的runid:
主从节点初次复制时,主节点将自己的runid发送给从节点,从节点将这个runid保存起来;当断线重连时,从节点会将这个runid发送给主节点;主节点根据runid判断能否进行部分复制:
Redis在2.8版本提供了PSYNC命令来带代替SYNC命令,为Redis主从复制提供了部分复制的能力,主从节点使用PSYNC命令jeuding全量复制还是部分复制的
psync命令的执行过程可以参见下图(图片来源:《Redis设计与实现》):
redis的主从超时检测主要从以下方面进行判断,分别是主监测从、从监测主。
每隔指定的时间,主节点会向从节点发送PING命令,这个PING命令的作用,主要是为了让从节点进行超时判断。
PING发送的频率由repl-ping-slave-period参数控制,单位是秒,默认值是10s。
在命令传播阶段,从节点会向主节点发送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官方文档对于哨兵功能的描述:
它由两部分组成,哨兵节点和数据节点:
然而一个哨兵进程对Redis服务器进行监控,可能会出现问题,为此,我们可以使用多个哨兵进行监控。各个哨兵之间还会进行监控,这样就形成了多哨兵模式。
用文字描述一下故障切换(failover)的过程。假设主服务器宕机,哨兵1先检测到这个结果,系统并不会马上进行failover过程,仅仅是哨兵1主观的认为主服务器不可用,这个现象成为主观下线。当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,那么哨兵之间就会进行一次投票,投票的结果由一个哨兵发起,进行failover操作。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。这样对于客户端而言,一切都是透明的。
Sentinel 并不使用数据库,它不能存储数据,他也不会加载RDB文件或AOF文件。在启动之后,服务器会初始化一个sentinel.c/sentinelState结构,这个结构中保存了服务器中所有和Sentinel功能有关的状态
在初始化Sentinel之后,就会建立与被监控主服务器的网络连接,Sentinel 将成为主服务器的客户端,这个客户端可以向主服务器发送命令,并从中获取相关的服务器信息。Sentinel在监控主服务器后,它们之间会建立两个异步网络连接。
当一台 sentinel 发现一个 Redis 服务无法 ping 通时,就标记为 主观下线 sdown;同时另外的 sentinel 服务也发现该 Redis 服务宕机,也标记为 主观下线,当多台 sentinel (大于等于2,在配置的最后一个)时,都标记该Redis服务宕机,这时候就变为客观下线了,然后进行故障转移.
故障转移是由 sentinel 领导者节点来完成的(只需要一个sentinel节点),关于 sentinel 领导者节点的选取也是每个 sentinel 向其他 sentinel 节点发送我要成为领导者的命令,超过半数sentinel 节点同意,并且也大于quorum ,那么他将成为领导者,如果有多个sentinel都成为了领导者,则会过段时间在进行选举.
sentinel 领导者节点选举出来后,会通过如下几步进行故障转移:
从 slave 节点中选出一个合适的 节点作为新的master节点.这里的合适包括如下几点:
可以从 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设计与实现》