Redis - 分布式缓存
Redis 是 C 语言开发的一个开源的(遵从 BSD 协议)高性能键值对(key-value)的内存数据库,可以用作数据库、缓存、消息中间件等。它是一种 NoSQL(not-only sql,泛指非关系型数据库)的数据库。
Redis 作为一个内存数据库:
性能优秀,数据在内存中,读写速度非常快,支持并发 10W QPS。
单进程单线程,是线程安全的,采用 IO 多路复用机制。
丰富的数据类型,支持字符串(strings)、散列(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
支持数据持久化。
可以将内存中数据保存在磁盘中,重启时加载。
主从复制,哨兵,高可用。
可以用作分布式锁。
可以作为消息中间件使用,支持发布订阅。
# 一、Redis数据类型
# 1)使用场景
string类型的应用场景
最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存和spring session + redis实现session共享。
# 单值缓存
> SET key value
> GET key
# 对象缓存
> SET user:1 value(json格式数据)
> MSET user:1:name sleepyocean user:1:balance 1888
> MGET user:1:name user:1:balance
# 分布式锁
线程1: > SETNX product:10001 true // 返回1代表获取锁成功
线程2: > SETNX product:10001 // 返回0代表获取锁失败
... ... 执行业务操作
> DEL product:10001 // 执行完业务释放锁
> SET product:10001 true ex 10 nx // 防止程序意外终止导致死锁
# 计数器
> INCR article:readcount:{文章ID}
> GET article:readcount:{文章ID}
# 分布式系统全局序列号
> INCRBY orderId 1000 // redis批量生产序列号,提升性能
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
list类型的应用场景
使用List的数据结构,可以做简单的消息队列的功能。另外还有一个就是,可以利用lrange命令,做基于redis的分页功能,性能极佳,用户体验好。
hash类型的应用场景
这里value存放的是结构化的对象,比较方便的就是操作其中的某个字段。
set类型的应用场景
因为set堆放的是一堆不重复值的集合。所以可以做全局去重的功能。利用交集、并集、差集等操作,可以计算共同value,全部的value,自己独有的value等功能。
zset类型的应用场景
sorted set多了一个权重参数score,集合中的元素能够按score进行排列。可以做排行榜应用、取TOP-n操作、延时任务和范围查找。
# 2)基础操作
1)docker搭建redis环境,参见我的另篇博文:docker搭建基础中间件 (opens new window)
2)进入redis-cli
$ docker exec -it redis redis-cli
3)练习开始,输入操作指令
string
# set {key} {value}。设定key持有指定的字符串value,如果该key存在则进行覆盖操作,总是返回OK。
> set name sleepy
OK
# get {key}。获取key的value。如果与该key关联的value不是String类型,redis将返回错误信息,因为get命令只能用于获取String value;如果该key不存在,返回null。
> get name
"sleepy"
# getset {key} {value}。先获取该key的值,然后在设置该key的值。
> getset name ocean
"sleepy"
> get name
"ocean"
# incr {key}。将指定的key的value原子性的递增1. 如果该key不存在,其初始值为0,在incr之后其值为1。如果value的值不能转成整型,如hello,该操作将执行失败并返回相应的错误信息
> incr age
(integer) 1
# decr {key}。将指定的key的value原子性的递减1.如果该key不存在,其初始值为0,在incr之后其值为-1。如果value的值不能转成整型,如hello,该操作将执 行失败并返回相应的错误信息。
> decr age
(integer) 0
# incrby {key} {increment}。将指定的key的value原子性增加increment,如果该key不存在,器初始值为0,在incrby之后,该值为increment。如果该值不能转成 整型,如hello则失败并返回错误信息
> incrby price 10
(integer) 10
# decrby {key} {decrement}。将指定的key的value原子性减少decrement,如果该key不存在,器初始值为0,在decrby之后,该值为decrement。如果该值不能 转成整型,如hello则失败并返回错误信息
> decrby price 8
(integer) 2
# append {key} {value}。如果该key存在,则在原有的value后追加该值;如果该key 不存在,则重新创建一个key/value
> append name ,sleepy
(integer) 12
> get name
"ocean,sleepy"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
list
# lpush {key} {value1} {value2}...。在指定的key所关联的list的头部插入所有的values,如果该key不存在,该命令在插入的之前创建一个与该key关联的空链表,之后再向该链表的头部插入数据。插入成功,返回元素的个数。
> lpush date date3 date2 date1
(integer) 3
# rpush {key} {value1} {value2}...。在该list的尾部添加元素。
> rpush date date4 date5 date6
(integer) 6
# lrange {key} {start} {end}。获取链表中从start到end的元素的值,start、end可为负数,若为-1则表示链表尾部的元素,-2则表示倒数第二个,依次类推…
> lrange date 0 5
1) "date1"
2) "date2"
3) "date3"
4) "date4"
5) "date5"
6) "date6"
# lpushx {key} {value}。仅当参数中指定的key存在时(如果与key管理的list中没有值时,则该key是不存在的)在指定的key所关联的list的头部插入value。
> lpushx date date0
(integer) 7
> lrange date 0 6
1) "date0"
2) "date1"
3) "date2"
4) "date3"
5) "date4"
6) "date5"
7) "date6"
# rpushx {key} {value}。在该list的尾部添加元素。
> rpushx date date7
(integer) 8
> lrange date 0 7
1) "date0"
2) "date1"
3) "date2"
4) "date3"
5) "date4"
6) "date5"
7) "date6"
8) "date7"
# lpop {key}。返回并弹出指定的key关联的链表中的第一个元素,即头部元素。
> lpop date
"date0"
# rpop {key}。从尾部弹出元素。
> rpop date
"date7"
> lrange date 0 7
1) "date1"
2) "date2"
3) "date3"
4) "date4"
5) "date5"
6) "date6"
# rpoplpush {resource} {destination}。将「resource」链表中的尾部元素弹出并添加另一个key「destination」的头部。
> rpoplpush date redate
"date6"
> lrange date 0 7
1) "date1"
2) "date2"
3) "date3"
4) "date4"
5) "date5"
> lrange redate 0 7
1) "date6"
# llen {key}。返回指定的key关联的链表中的元素的数量。
> llen date
(integer) 5
# lset {key} {index} {value}。设置链表中的index的脚标的元素值,0代表链表的头元素,-1代表链表的尾元素。
> lset date 2 datenew2
OK
> lrange date 0 7
1) "date1"
2) "date2"
3) "datenew2"
4) "date4"
5) "date5"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
hash
# hset {key} {field1} {value1} {field2} {value2} ... ...
# 或 hmset {key} {field1} {value1} {field2} {value2} ... ...。为指定的key设定一个或多个field/value对
> hset person name oasis age 23
(integer) 2
# hget {key} {field}。返回指定的key中的field的值
> hget person name
"oasis"
# hmget {key} {field}。取key中的多个field值
> hmget person name age
1) "oasis"
2) "23"
# hkeys {key}。获取所有的key
> hkeys person
1) "name"
2) "age"
# hvals {key}。获取所有的value
> hvals person
1) "oasis"
2) "23"
# hgetall {key}。获取key中的所有field 中的所有field-value
> hgetall person
1) "name"
2) "oasis"
3) "age"
4) "23"
# hdel {key} {field1} {field2} ...。可以删除一个或多个字段,返回是被删除的字段个数
> hdel person age
(integer) 2
> hgetall person
1) "name"
2) "oasis"
# del key。删除整个list
> del person
(integer) 1
> hgetall person
(empty array)
# hincrby {key} {field} {increment}。设置key中field的值增加increment,如: age增加20
> hincrby person age 20
(integer) 20
> hgetall person
1) "age"
2) "20"
# hexists {key} {field}。判断指定的key中的field是否存在
> hexists person age
(integer) 1
> hexists person name
(integer) 0
# hlen {key}。获取key所包含的field的数量
> hlen person
(integer) 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
set
# sadd {key} {value1} {value2} ...
> sadd lang java c cpp python c
(integer) 4
# smembers {key}
> smembers lang
1) "c"
2) "python"
3) "java"
4) "cpp"
# srem {key} {member1} {member2} ...
> srem lang c
(integer) 1
> smembers lang
1) "python"
2) "java"
3) "cpp"
# sismember {key} {member}。判断参数中指定的成员是否在该set中,1表示存在,0表示不存在或者该key本身就不存在(无论集合中有多少元素都可以极速的返回结果)
> sismember lang java
(integer) 1
# 集合运算
> sadd lang1 java c cpp python c
(integer) 4
> sadd lang2 js html python
(integer) 3
# 差集运算
> sdiff lang1 lang2
1) "c"
2) "java"
3) "cpp"
# 交集运算
> sinter lang1 lang2
1) "python"
# 并集运算
> sunion lang1 lang2
1) "java"
2) "cpp"
3) "c"
4) "js"
5) "python"
6) "html"
# xxx-store {destination} {key1} {key2} ...。将集合运算存储到「destination」集合中
> sdiffstore sdifflang lang1 lang2
(integer) 3
> sinterstore sinterlang lang1 lang2
(integer) 1
> sunionstore sunionlang lang1 lang2
(integer) 6
# scard {key}。获取set中的成员数量
> scard lang1
(integer) 4
# srandmember {key} {count}。随机返回set中的「count」个成员
> srandmember lang1 2
1) "c"
2) "java"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
zset
# zadd {key} {score1} {member1} {socre2} {member2} ... ...。将所有成员以及该成员的分数存放到sorted-set中。如果该元素已经存在则会用新的分数替换原有的分数。返回值是新加入到集合中的元素个数。(根据分数升序排列)
> zadd toplang 3 c++ 4 golang 5 js 6 html 7 css 10 java 9 c 8 python 12 php
(integer) 9
# zrange {key} {start} {stop} [withsocres]。获取集合中角标为start-end的成员,[withscore]参数表明返回的成员包含其分数。
> zrange toplang 0 10
1) "c++"
2) "golang"
3) "js"
4) "html"
5) "css"
6) "python"
7) "c"
8) "java"
9) "php"
# zscore {key} {member}。返回指定成员的分数
> zscore toplang python
"8"
# zcard {key}
> zcard toplang
(integer) 3
# zrem {key} {member1} {member2} ...
> zrem toplang php
(integer) 1
> zcard toplang
(integer) 8
# zrangebysocre {key} {min} {max} [withscores] [limit {offset} {count}]。返回分数在[min,max]的成员并按照分数从低到高排序。[withscore]:显示分数;[limit offset count];offset,表明从脚标为offset的元素开始并返回count个成员
> zrangebyscore toplang 5 10 withscores limit 2 3
1) "css"
2) "7"
3) "python"
4) "8"
5) "c"
6) "9"
# zincrby {key} {increment} {member}。设置指定成员的增加分数。返回值是修改后的分数
> zincrby toplang 3 python
"11"
> zrange toplang 0 10 withscores
1) "c++"
2) "3"
3) "golang"
4) "4"
5) "js"
6) "5"
7) "html"
8) "6"
9) "css"
10) "7"
11) "c"
12) "9"
13) "java"
14) "10"
15) "python"
16) "11"
# zcount {key} {min} {max}。获取分数在[min,max]之间的成员个数
> zcount toplang 3 6
(integer) 4
# zrank {key} {member}。返回成员在集合中的排名(从小到大,从0开始)
> zrank toplang js
(integer) 2
# zrevrank {key} {member}。返回成员在集合中的排名(从大到小,从0开始)
> zrevrank toplang java
(integer) 1
> zrange toplang 0 10
1) "c++"
2) "golang"
3) "js"
4) "html"
5) "css"
6) "c"
7) "java"
8) "pyhton"
# zremrangebyrank {key} {start} {stop}。按照排名范围删除元素
> zremrangebyrank toplang 3 5
(integer) 3
> zrange toplang 0 10
1) "c++"
2) "golang"
3) "js"
4) "java"
5) "python"
> zrange toplang 0 10 withscores
1) "c++"
2) "3"
3) "golang"
4) "4"
5) "js"
6) "5"
7) "java"
8) "10"
9) "python"
10) "11"
# zremrangebyscore {key} {min} {max}。按照分数范围删除元素
> zremrangebyscore toplang 3 5
(integer) 3
127.0.0.1:6379> zrange toplang 0 10 withscores
1) "java"
2) "10"
3) "python"
4) "11"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# 3)通用操作
# keys {pattern}。获取所有与pattern匹配的key ,返回所有与该key匹配的keys。 *表示任意一个或者多个字符, ?表示任意一个字符
> keys *
1) "date1"
2) "person"
3) "age"
4) "price"
5) "toplang"
6) "lang2"
7) "1"
8) "0"
9) "redate"
10) "sunionlang"
11) "sdifflang"
12) "name"
13) "lang1"
14) "lang"
15) "date"
16) "sinterlang"
# del {key1} {key2} ...
> del date1 lang1 lang2 sunionlang sinterlang sdifflang
(integer) 6
> keys *
1) "person"
2) "age"
3) "price"
4) "toplang"
5) "1"
6) "0"
7) "redate"
8) "name"
9) "lang"
10) "date"
# exists {key}。判断该key是否存在,1代表存在,0代表不存在
> exists date1
(integer) 0
# rename {key} {newkey}
> rename person man
OK
> keys *
1) "age"
2) "price"
3) "toplang"
4) "1"
5) "0"
6) "redate"
7) "name"
8) "lang"
9) "date"
10) "man"
> expire redate 10
(integer) 1
> exists redate
(integer) 1
# 10s 以后再次查询
> exists redate
(integer) 0
# ttl {key}。获取该key所剩的超时时间,如果没有设置超时,返回-1,如果返回-2表示超时不存在
> ttl man
(integer) -1
> ttl redate
(integer) -2
> expire date 300
(integer) 1
> ttl date
(integer) 286
# persist {key}。持久化key
# type {key}。获取指定key的类型。该命令将以字符串的格式返回。返回的字符串为string 、list 、set 、hash 和 zset,如果key不存在返回none。
> type name
string
> type date
list
> type man
hash
> type lang
set
> type toplang
zset
> type redate
none
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
其他命令补充:
- select 编号:切换数据库(redis默认有16个数据库,默认使用第0个)
- dbsize:查看当前数据库存储数据的大小
- flushdb:清空当前数据库
- flushall:清空所有的数据库
# 4)三大特殊数据类型
a)geospatial(地理位置)
把某个具体的位置信息(经度,纬度,名称)添加到指定的key中,数据将会用一个sorted set存储,以便稍后能使用 GEORADIUS (opens new window)和 GEORADIUSBYMEMBER (opens new window)命令来根据半径来查询位置信息。
# 规则:两极无法直接添加,一般会下载城市数据,通过java程序一次性导入
# 添加城市的经度和纬度
> geoadd china:city 121.47 31.23 shanghai
# 获取指定城市的经度和纬度
> geopos china:city beijin
# 获取城市之间的距离(单位: m米、km千米、mi英里、ft英尺)
> geodist china:city beijin shanghai km
# 查找当前经纬度下,半径1000km的城市
> georadius china:city 110.00 30.00 1000km
# 查找当前经纬度下,半径1000km的城市和直线距离
> georadius china:city 110.00 30.00 1000km withdist
# 查找当前经纬度下,半径1000km的城市经纬度
> georadius china:city 110.00 30.00 1000km withcoord
# 找出指定城市1000km附近的城市
> georadiusbymember china:city beijin 1000km
# 返回一个或多个位置的geohash
> geohash china:city beijin chongqin
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
使用场景:地图
Redis.Version >= 3.2
b)hyperloglog
Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。
# 创建一组元素
> pfadd mykey a b c
# 统计mykey元素的基数数量
> pfcount mykey
# 合并mykey1和mykey2到mykey3
> pf merge mykey3 mykey1 mykey2
2
3
4
5
6
7
8
使用场景:统计一个网站的访问人数
Redis.Version >= 2.8.9
c)bitmap(位图)
bitmap就是通过最小的单位bit来进行0或者1的设置,表示某个元素对应的值或者状态。
# 设置值
> setbit sign 1 0
# 取值
> getbit sign 1
# 统计这周的打卡记录
> bitcount sign
2
3
4
5
6
7
8
使用场景:统计打卡,考勤
Redis.Version >= 2.2.0
# 二、Redis特性
# 1)淘汰策略
补充:Redis 4.0 加入了 LFU(least frequency use)淘汰策略,包括 volatile-lfu 和 allkeys-lfu,通过统计访问频率,将访问频率最少,即最不经常使用的 KV 淘汰。
# 2)事务
redis事务的本质:一组命令的集合,一个事务中所有的命令都会被序列化,在事务执行的过程中,会按照顺序执行
redis中的事务具有一致性、顺序性、排他性
redis单条命令保持原子性,但是事务不保证原子性
redis中没有隔离级别的概念
开启事务后,所有的命令在事务中并没有被执行,只有在发起执行命令的时候才会执行
redis事务步骤:
- 开启事务(multi)
- 命令入队
- 执行事务(exec)
- 取消事务(discard)
注意:事务执行的过程中可能存在异常
- 编译时异常(命令有错),事务中所有的命令都不会被执行
- 运行时异常,如果事务队列中存在语法性,那么执行命令的时候,其他命令可以正常执行
# 3)持久化
三种持久化方式: RDB持久化、AOF持久化和RDB-AOF混合持久化
- RDB(快照持久化):快照形式是直接把内存中的数据保存到一个 dump 的文件中,定时保存,保存策略。
redis.conf的配置示例:
#在900s内如果有1条数据被写入,则产生一次快照。
save 900 1
#在300s内如果有10条数据被写入,则产生一次快照
save 300 10
#在60s内如果有10000条数据被写入,则产生一次快照
save 60 10000
# stop-writes-on-bgsave-error 如果为yes则表示,当备份进程出错的时候,主进程就停止进行接受新的写入操作,这样是为了保护持久化的数据一致性的问题。
stop-writes-on-bgsave-error yes
2
3
4
5
6
7
8
9
10
RDB 持久化方式的优点:全量数据快照,文件小,恢复快
RDB 持久化方式的缺点:无法保存最近一次快照之后的数据。内存数据全量同步,数据量大的状况下,会由于 I/O 而严重影响性能。
- AOF持久化:把所有的对 Redis 的服务器进行修改的命令都存到一个文件里,命令的集合。
redis.conf的配置示例:
appendonly yes
# appendfsync no
# appendsync always 每次有数据修改发生时都会写入AOF文件。
# appendfsync everysec 每秒钟同步一次,该策略为AOF的缺省策略。
appendfsync everysec
2
3
4
5
6
AOF 持久化方式的优点:可读性高,适合保存增量数据,数据不易丢失。
AOF 持久化方式的缺点:文件体积大,恢复时间长。
- RDB-AOF 混合持久化方式:持久化策略首先将缓存中数据以 RDB 方式全量写入文件,再将写入后新增的数据以 AOF 的方式追加在 RDB 数据的后面,在下一次做 RDB 持久化的时候将 AOF 的数据重新以 RDB 的形式写入文件。
# 3)Redis高性能的原因
- Redis 完全基于内存,绝大部分请求是纯粹的内存操作,非常迅速,数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度是 O(1)。
- 数据结构简单,对数据操作也简单。
- 采用单线程,避免了不必要的上下文切换和竞争条件,不存在多线程导致的 CPU 切换,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有死锁问题导致的性能消耗。
- 使用多路复用 IO 模型,非阻塞 IO。
# 4)Redis缓存相关问题
- 缓存穿透(查不到数据)
- 现象解释:缓存穿透是指,用户想要查询一个数据,发现redis内存数据库中没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求持久层数据库,这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透
- 解决方案:
- 布隆过滤器:布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力
- 缓存空对象:当存储层不命中后,即使返回的空对象也将其缓存起来,同时设置一个过期时间,之后再访问这个数据会从缓存中获取,保护了后端的数据源(潜在问题:空值缓存过多)
- 缓存击穿(热点数据过期)
- 现象解释:缓存击穿,是指一个key非常的热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求在数据库上,就像在屏障上面开了一个洞。当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新的数据,并且写缓存会导致数据库瞬间压力过大
- 解决方案:
- 设置热点数据永不过期:从缓存层面来看,没有设置过期时间,就不会产生这个问题
- 加互斥锁:分布式锁,使用分布式锁,保证对于每个Key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此需要等待。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大
- 缓存雪崩(热点数据集中过期)
- 现象解释:缓存雪崩,是指在某一个时间段,缓存集中过期失效,redis宕机产生一系列的连锁反应,造成整个系统崩溃
- 缓存预热
- 现象解释:缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
- 解决方案:
- 直接写个缓存刷新页面,上线时手工操作下
- 数据量不大,可以在项目启动的时候自动进行加载
- 定时刷新缓存
- 缓存降级
- 现象解释:当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户
- 解决方案:降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。这里以参考日志级别设置预案
- 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级
- 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警
- 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级
- 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级
# 四、Redis集群架构
Redis 高可用架构有如下方案:
- Redis Sentinel 集群 + 内网 DNS + 自定义脚本
- Redis Sentinel 集群 + VIP + 自定义脚本
- 封装客户端直连 Redis Sentinel 端口。JedisSentinelPool,适合 Java
- Redis Sentinel 集群 + Keepalived/Haproxy
- Redis M/S + Keepalived
- Redis Cluster
- Twemproxy
- Codis
搭建高可用Redis集群架构实践请参考我的另一篇博文:Redis高可用集群搭建