redis实战
转载于:https://blog.csdn.net/piaoslowly/article/details/81563579
redis简介
Redis 是一个开源的 使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志 型、Key-Value 数据库。
存储操作
redis的key-value需要特别说明下,key只能为字符串类型,但value可以为字符串,hash表,列表(list),集合(set),有序集合(sorted set)等多种数据类型,这也是redis与memcached的区别。memcached也是key-value数据库,但是key,value都只能是字符串类型。
redis的key
Redis 的 key 是字符串类型,但是 key 中不能包括边界字符 ,由于 key 不是 binary safe 的字符串,所以像"my key"和"mykey\n"这样包含空格和换行的 key 是不允许的。
key的相关操作
- exits key检测指定key是否存在,返回1表示存在,0不存在
- del key1key2…keyN删除给定key,返回删除key的数目,0表示给定key都不存在
- type key 返回给定 key 值的类型。返回 none 表示 key 不存在,string 字符类型,list 链表 类型 set 无序集合类型…
- keys pattern 返回匹配指定模式的所有 key。如:keys * 返回所有key
- randomkey 返回从当前数据库中随机选择的一个 key,如果当前数据库是空的,返回空串 rename oldkey
- newkey 重命名一个 key,如果 newkey 存在,将会被覆盖,返回 1 表示成功, 0 失败。可能是 oldkey 不存在或者和 newkey 相同。
- renamenx oldkey newkey 同上,但是如果 newkey 存在返回失败。
- expire key seconds 为 key 指定过期时间,单位是秒。返回 1 成功,0 表示 key 已经设置过过 期时间或者不存在。
- ttl key 返回设置过过期时间key的剩余过期秒数。-1表示key不存在或者未设置过期时间。 select db-index 通过索引选择数据库,默认连接的数据库是 0,默认数据库数是 16 个。返回 1 表示成功,0 失败。
- movekeydb-index将key从当前数据库移动到指定数据库。返回 1表示成功。0表示key 不存在或者已经在指定数据库中 。
key的大小最大为512MB。
字符串
string 是最基本的类型,而且 string 类型是二进制安全的。意思是 redis 的 string 可以包含任何数据。比如 jpg 图片或者序列化的对象。从内部实现来看其实 string 可以看作 byte 3数组,最大上限是 1G 字节。
String操作命令
- set key value 设置 key 对应 string 类型的值,返回 1 表示成功,0 失败。
- setnx key value 如果 key 不存在,设置 key 对应 string 类型的值。如果 key 已经存在,返 回0。
- get key 获取 key 对应的 string 值,如果 key 不存在返回 nil
- getset key value 先获取 key 的值,再设置 key 的值。如果 key 不存在返回 nil。
- mget key1 key2 … keyN 一次获取多个 key 的值,如果对应 key 不存在,则对应返回 nil。
- mset key1 value1 … keyN valueN 一次设置多个 key 的值,成功返回 1 表示所有的值都设置 了,失败返回 0 表示没有任何值被设置。
- msetnx key1 value1 … keyN valueN 一次设置多个 key 的值,但是不会覆盖已经存在的 key
- incr key 对 key 的值做++操作,并返回新的值。注意 incr 一个不是 int 的 value 会返回错误,incr一个不存在的 key,则设置 key 值为 1。
- decr key 对 key 的值做–操作,decr 一个不存在 key,则设置 key 值为-1。
- incrby key integer 对 key 加上指定值 ,key 不存在时候会设置 key,并认为原来的 value 是0。
- decrby key integer 对key减去指定值。decrby完全是为了可读性,我们完全可以通过incrby 一个负值来实现同样效果,反之一样。
set说明
//设置值和获取值
127.0.0.1:7000> set b 1234
OK
127.0.0.1:7000> get b
"1234"
//再次设置值,之前的值会被修改
127.0.0.1:7000> set b 1dfff
OK
127.0.0.1:7000> get b
"1dfff"
//nx如果b已经存在,则直接返回失败,不存在则设置成功。
127.0.0.1:7000> set b fsdf nx
(nil)
127.0.0.1:7000> get b
"1dfff"
//xx如果b存在,则设置成功,如果b不存在则设置失败,这相当于修改一个值。
127.0.0.1:7000> set b ffff xx
OK
127.0.0.1:7000> get b
"ffff"
127.0.0.1:7000>
//设置过期时间,多次设置与最后一次为准。相当于修改
127.0.0.1:7000> set f ffff ex 10
OK
//获取过期还剩多少
127.0.0.1:7000> ttl f
(integer) 6
127.0.0.1:7000> ttl f
(integer) 3
127.0.0.1:7000>
- 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
原子操作
incr
如果遇到需要i++这类型的操作我们代码里面一般会如下:
伪代码如下:
int a= get key
a= a + 1
set key a;
- 1
- 2
- 3
- 4
这样是不安全的,因为多个线程同事操作时,会导致结果不一致。
上面分析了,i++ 操作,那么redis为我们提供了原子性的i++操作。
127.0.0.1:7000> set a 1
OK
127.0.0.1:7000> INCR a
(integer) 2
127.0.0.1:7000> incr a
(integer) 3
127.0.0.1:7000> get a
"3"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
设置一个a=1,现在需要将a+1,那么调用incr a会自动为我们做++操作,decr a会自动做减减操作。incrby a 2 这个表示a直接加2操作,而incr为每次自动+1,incrby为指定加多少。
bitmap
Redis从2.2.0版本开始新增了setbit,getbit,bitcount等几个bitmap相关命令。虽然是新命令,但是并没有新增新的数据类型,因为setbit等命令只不过是在set上的扩展,所以bigmap还是一种字符串类型
在一台2010MacBook Pro上,offset为232-1(分配512MB)需要~300ms,offset为230-1(分配128MB)需要~80ms,offset为228-1(分配32MB)需要~30ms,offset为226-1(分配8MB)需要8ms
指令 SETBIT key offset value
复杂度 O(1)
设置或者清空key的value(字符串)在offset处的bit值(只能只0或者1)。
offset的最大值为10亿,2的23-1方
hash(散列)
hash是一个string类型的field和value的映射表。添加,删除操作都是O(1)(平均)。hash特别适合用于存储对象。相对于将对象的每个字段存成单个string类型。将一个对象存储在hash类型中会占用更少的内存,并且可以更方便的存取整个对象。省内存的原因是新建一个hash对象时开始是用zipmap(又称为small hash)来存储的。这个zipmap其实并不是hash table,但是zipmap相比正常的hash实现可以节省不少hash本身需要的一些元数据存储开销。尽管zipmap的添加,删除,查找都是O(n),但是由于一般对象的field数量都不太多,所以使用zipmap也是很快的,也就是说添加删除平均还是O(1)。如果field或者value的大小超出一定限制后,redis会在内部自动将zipmap替换成正常的hash实现.这个限制可以在配置文件中指定。
hash-max-zipmap-entries64#配置字段最多64个
hash-max-zipmap-value512#配置value最大为512字节
hash 类型数据操作指令简介
- hset key field value 设置 hash field 为指定值,如果 key 不存在,则创建
- hget key field 获取指定的hash field。
- hmget key filed1…fieldN 获取全部指定的 hash filed。
- hmset key filed1 value1 … filedN valueN 同时设置 hash 的多个 field。
- hincrby key field integer 将指定的 hash filed 加上指定值。成功返回 hash filed 变更后的 值。
- hexists key field 检测指定 field 是否存在。
- hdel key field 删除指定的 hash field。
- hlen key 返回指定 hash 的 field 数量。
- hkeys key 返回 hash 的所有 field。 hvals key 返回 hash 的所有 value。 hgetall 返回hash的所有filed和value
//使用hash来存储数据。user是key,由于value是hash,所以还有一个key,value健值对。
127.0.0.1:7000> hset user id 1
(integer) 1
127.0.0.1:7000> hset user userName xiaobao
(integer) 1
127.0.0.1:7000> hset user password ssssss
(integer) 1
//获取key为user的hash表中的id的值。
127.0.0.1:7000> hget user id
"1"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
//获取hash表中的所有key
127.0.0.1:7000> hkeys user
1) "id"
2) "userName"
3) "password"
//获取hash表中所有的value
127.0.0.1:7000> HVALS user
1) "1"
2) "xiaobao"
3) "ssssss"
127.0.0.1:7000>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
list(列表)
list是一个链表结构,可以理解为每个子元素都是string类型的双向链表。主要功能是push、pop获取一个范围的所有值等。操作中 key理解为链表的名字。
List 类型数据操作指令简介
- lpush key string 在key对应list的头部添加字符串元素,返回 1表示成功,0表示key存 在且不是 list 类型。
- rpush key string 在 key 对应 list 的尾部添加字符串元素。
- llen key 返回 key 对应 list 的长度,如果 key 不存在返回 0,如果 key 对应类型不是 list 返回错误。
- lrange key start end 返回指定区间内的元素,下标从 0 开始,负值表示从后面计算,-1 表示 倒数第一个元素 ,key 不存在返回空列表。
- ltrimkeystartend 截取list指定区间内元素,成功返回1,key不存在返回错误。
- lset key index value 设置 list 中指定下标的元素值,成功返回 1,key 或者下标不存在返回 错误。
- lremkeycountvalue从 List 的头部(count正数)或尾部(count负数)删除一定数量(count) 匹配 value 的元素,返回删除的元素数量。count为0时候删除全部。
- lpop key 从list的头部删除并返回删除元素。如果key对应list不存在或者是空返回nil, 如果 key 对应值不是 list 返回错误。
- rpop key从list的尾部删除并返回删除元素。
- blpop key1 … keyN timeout 从左到右扫描,返回对第一个非空 list 进行 lpop 操作并返回, 比如 blpop list1 list2 list3 0 ,如果 list 不存在 list2,list3 都是非空则对 list2 做 lpop 并返回从 list2 中删除的元素。如果所有的 list 都是空或不存在,则会阻塞 timeout 秒,timeout为0表示一直阻塞。当阻塞时,如果有 client对key1…keyN中的任意key 进行 push 操作,则第一在这个 key 上被阻塞的 client 会立即返回。如果超时发生,则返回 nil。有点像 unix 的 select 或者 poll。
- brpop 同blpop,一个是从头部删除一个是从尾部删除。
//从key为list的左边push一个1.
127.0.0.1:7000> lpush list 1
(integer) 1
127.0.0.1:7000> lpush list 2
(integer) 2
//查看list队列长度
127.0.0.1:7000> llen list
(integer) 2
//从右边连续push 3 4 5 6
127.0.0.1:7000> rpush list 3 4 5 6
(integer) 6
//返回列表从0到6个元素
127.0.0.1:7000> LRANGE list 0 6
1) "2"
2) "1"
3) "3"
4) "4"
5) "5"
6) "6"
//从队列头弹出值并删除
127.0.0.1:7000> lpop list
"2"
127.0.0.1:7000> lpop list
"1"
127.0.0.1:7000> lpop list
"3"
127.0.0.1:7000> lpop list
"4"
- 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
整个集合就是一个入栈出栈的操作,记住集合它是一个双向链表就OK了。
set(集合)
set是无序集合,最大可以包含(2 的 32 次方-1)个元素,set 是不能有重复元素,如果存入重复元素不会报错,但是set里面还是只有一个。
set 的是通过 hash table 实现的, 所以添加,删除,查找的复杂度都是 O(1)。
hash table 会随着添加或者删除自动的调整大小,需要注意的是调整hash table大小时候需要同步(获取写锁)会阻塞其他读写操作,可能不久后就会改用跳表(skip list)来实现。跳表已经在sorted sets 中使用了。关于set集合类型除了基本的添加删除操作,其它有用的操作还包含集合的取并集 (union),交集(intersection), 差集(difference)。通过这些操作可以很容易的实现 SNS 中的好友推荐和 blog 的 tag 功能。
set 类型数据操作指令简介
- sadd key member 添加一个 string 元素到 key 对应 set 集合中,成功返回1,如果元素以及 在集合中则返回 0,key 对应的 set 不存在则返回错误。
- srem key member 从 key 对应 set 中移除指定元素,成功返回 1,如果 member 在集合中不 存在或者 key 不存在返回 0,如果 key 对应的不是 set 类型的值返回错误。
- spop key count 删除并返回key对应set中随机的一个元素,如果set是空或者key不存在返回nil。
- srandmember key count 同spop,随机取set中的一个元素,但是不删除元素。
- smembers key 返回key对应set的所有元素,结果是无序的。
- smove srckey dstkey member 从srckey对应set中移除member并添加到dstkey对应set中,整个操作是原子的。成功返回 1,如果 member 在 srckey 中不存在返回 0,如果 key 不是 set 类型返回错误。
- scard key返回set的元素个数,如果set是空或者key不存在返回 0。
- sismember key member 判断member是否在set中,存在返回1,0表示不存在或者key不存在。
- sinter key1 key2 … keyN 返回所有给定 key 的交集。
- sinterstore dstkey key1 … keyN 返回所有给定 key 的交集,并保存交集存到 dstkey 下。
- sunion key1 key2 … keyN 返回所有给定 key 的并集。
- sunionstore dstkey key1 … keyN 返回所有给定 key 的并集,并保存并集到 dstkey 下。
- sdiff key1 key2 … keyN 返回所有给定 key 的差集。
- sdiffstore dstkey key1 … keyN 返回所有给定 key 的差集,并保存差集到 dstkey 下。
//存入元素
127.0.0.1:7000> sadd set 1
(integer) 1
127.0.0.1:7000> sadd set 2
(integer) 1
//1已经存在了,再存入1则返回0了。
127.0.0.1:7000> sadd set 1
(integer) 0
//弹出key为set的总的2个元素,弹出后删除元素
127.0.0.1:7000> spop set 2
1) "1"
2) "2"
//没有元素的情况下返回空
127.0.0.1:7000> spop set 1
(empty list or set)
127.0.0.1:7000> sadd set 1 2 3 4 5 6 6 6
(integer) 6
//随机弹出10个元素,不删除
127.0.0.1:7000> srandmember set 10
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
127.0.0.1:7000> srandmember set 1
1) "6"
//返回set的所有元素,无序的
127.0.0.1:7000> smembers set3
1) "2"
2) "3"
3) "5"
- 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
127.0.0.1:7000> srandmember set 10
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
127.0.0.1:7000> sadd set1 2 3 5 10 20
(integer) 5
//求set,set2两个key的并集
127.0.0.1:7000> sunion set set1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "10"
8) "20"
//求set set1的交集
127.0.0.1:7000> sinter set set1
1) "2"
2) "3"
3) "5"
//求两个交集,返回的结果存储到set3里面
127.0.0.1:7000> sinterstore set3 set set1
(integer) 3
127.0.0.1:7000> SRANDMEMBER set3 10
1) "2"
2) "3"
3) "5"
- 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
有序集合
sorted set 是有序集合,它在 set 的基础上增加了一个顺序属性,这一属性在添加修 改元素的时候可以指定 ,每次指定后,会自动重新按新的值调整顺序 。可以理解了有两列 的 mysql 表,一列存 value,一列存顺序。操作中key理解为sorted set的名字。
Sorted Set 类型数据操作指令简介
- zadd key score member 添加元素到集合,元素在集合中存在则更新对应 score。
- zrem key member 删除指定元素,1 表示成功,如果元素不存在返回 0。
- zincrby key incr member 增加对应 member 的 score 值,然后移动元素并保持 skip list 保持有 序。返回更新后的 score 值。
- zrank key member 返回指定元素在集合中的排名(下标 ),集合中元素是按 score 从小到大 排序的。
- zrevrank key member 同上,但是集合中元素是按 score 从大到小排序。
- zrange key start end 类似 lrange 操作从集合中去指定区间的元素。返回的是有序结果
- zrevrange key start end 同上,返回结果是按 score 逆序的。
- zrangebyscore key min max 返回集合中 score 在给定区间的元素。
- zcount key min max 返回集合中 score 在给定区间的数量。
- zcard key 返回集合中元素个数。
- zscore key element 返回给定元素对应的 score。
- zremrangebyrank key min max 删除集合中排名在给定区间的元素 。
- zremrangebyscore key min max 删除集合中 score 在给定区间的元素。
//存储一个有序set,sortset为key,1表示分数,a是我们的值
127.0.0.1:7000> zadd sortset 1 a
(integer) 1
127.0.0.1:7000> zadd sortset 2 b
(integer) 1
//查看b的下标
127.0.0.1:7000> zrank sortset b
(integer) 1
//返回0,1个元素
127.0.0.1:7000> zrange sortset 0 1
1) "a"
2) "b"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
redis运维
- 统计生产上比较大的key
./redis-cli -p 7000 --bigkeys
- 查看key的详细信息
//查看key为d的详细信息,查看key的详细信息(从这里可以看出key占用的存储空间挺大的,需要存储很多信息)
127.0.0.1:7000> DEBUG OBJECT d
Value at:0x7fb7cf526240 refcount:1 encoding:embstr serializedlength:5 lru:6748523 lru_seconds_idle:537
- 1
- 2
- 3
- 分页查看redis中的key
//查看key
127.0.0.1:7000> SCAN 0
1) "17"
2) 1) "d"
2) "kye"
127.0.0.1:7000> SCAN 17
1) "0"
2) 1) "dfff"
2) "ff"
3) "ddd"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
第一次迭代使用 0 作为游标, 表示开始一次新的迭代。
第二次迭代使用的是第一次迭代时返回的游标, 也即是命令回复第一个元素的值17.
从上面的示例可以看到, SCAN 命令的回复是一个包含两个元素的数组, 第一个数组元素是用于进行下一次迭代的新游标, 而第二个数组元素则是一个数组, 这个数组中包含了所有被迭代的元素。 在第二次调用 SCAN 命令时, 命令返回了游标 0 , 这表示迭代已经结束, 整个数据集(collection)已经被完整遍历过了。
以 0 作为游标开始一次新的迭代, 一直调用 SCAN 命令, 直到命令返回游标 0 , 我们称这个过程为一次完整遍历(full iteration)。
- 模糊查找key
127.0.0.1:7000> scan 0 match t*
1) "0"
2) 1) "t"
2) "ttt"
127.0.0.1:7000> scan 0 match t* count 2
1) "3"
2) 1) "t"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
scan 0 match t* 与keys t* 还是有差别的,因为scan 是带分页的,而keys 是没有分页的,遍历了所有的key。
在线上不建议使用keys来查询key。
- 性能查询
redis-benchmark -n 10000
- 密码登陆
链接后没有权限操作:(error) NOAUTH Authentication required
redis-cli -h 127.0.0.1 -p 70000 -a password
如果忘记输入密码,可以在进入的时候使用
auth password
redis缓存淘汰策略
redis淘汰策略配置,在redis.conf文件查看:maxmemory-policy voltile-lru 支持热配置
redis 提供 6种数据淘汰策略:
- voltile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
- volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
- allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
- allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
- no-enviction(驱逐):禁止驱逐数据(默认策略)
选择策略规则
如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru
如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random
volatile-lru策略和volatile-random策略适合我们将一个Redis实例既应用于缓存和又应用于持久化存储的时候,然而我们也可以通过使用两个Redis实例来达到相同的效果,
将key设置过期时间实际上会消耗更多的内存,因此我们建议使用allkeys-lru策略从而更有效率的使用内存
https://blog.csdn.net/lizhi_java/article/details/68953179?locationNum=1&fps=1 <失效策略>
主从,哨兵模式
主从设置
进入到将要设置的从的客户端或者在配置文件里面下入
$ redis-cli -p 7000
127.0.0.1:7000> slaveof 192.168.110.101 6379
即可将当前7000这个redis服务设置为一个从服务。
取消从服务
127.0.0.1:7000> SLAVEOF no one
redis设置了主从服务器后,从会自动备份主的数据,但是从服务器对外提供只能读,不能写。
sentinel
哨兵是分布式应用,可以启动多个,指定不同端口即可。哨兵与哨兵之间不需要互相配置,哨兵会根据自己监控的主服务做判断,如果是监控的是同一个主服务,则他们会互相自动建立连接,互相通信。
哨兵配置监控时只需要配置master地址即可,但是不代表它不监控从节点。sentinel连接上主节点后,会自动从主节点获取到该节点的从节点信息然后进行监控。
对于redis-sentinel 程序, 你可以用以下命令来启动 Sentinel 系统:
redis-sentinel /path/to/sentinel.conf
对于 redis-server 程序, 你可以用以下命令来启动一个运行在 Sentinel 模式下的 Redis 服务器:
redis-server /path/to/sentinel.conf --sentinel
sentinel.conf配置如下:
sentinel monitor mymaster 127.0.0.1 7001 2
sentinel config-epoch mymaster 1
port 17000
- 1
- 2
- 3
- 创建了一个监控monitor, 自定义一个名称为mymaster,redis主服务器的IP地址,端口号,需要投票的数量为2.
- 设置哨兵的端口为17000
sentinel操作流程:
- 如果主服务器挂了,哨兵在指定的时间内检测到,会在从服务器中选一个出来做为主节点,然后把其他所有节点全部设置为从节点。
- 然后在把之前挂掉的主给从sentinel中剔除去。
注意:
- 第一行最后面的那个数字为2,需要至少启动2个sentinel,才能完成投票,如果只启动一个sentinel,但是配置为2,当主redis挂掉的时候,sentinel会发起投票,因为只有一个sentinel,也就只有一票,这个时候sentinel就不能做主从切换了。
- 对于客户端来说,是不能直接连接sentinel只能连接主或者从。
Java客户端连接sentinel对redis操作。
@Configuration
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
@Autowired
private RedisProperties redisProperties;
@Bean
public RedisConnectionFactory jedisConnectionFactory() {
RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration();
sentinelConfig.master(redisProperties.getSentinel().getMaster());
sentinelConfig.setSentinels(createSentinels(redisProperties.getSentinel()));
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(sentinelConfig);
jedisConnectionFactory.setPassword(redisProperties.getPassword());
return jedisConnectionFactory;
}
@Bean
@DependsOn("jedisConnectionFactory")
public StringRedisTemplate redisTemplate(JedisConnectionFactory jedisConnectionFactory) {
return new StringRedisTemplate(jedisConnectionFactory);
}
private List<RedisNode> createSentinels(RedisProperties.Sentinel sentinel) {
List<RedisNode> nodes = new ArrayList();
for (String node : StringUtils
.commaDelimitedListToStringArray(sentinel.getNodes())) {
try {
String[] parts = StringUtils.split(node, ":");
Assert.state(parts.length == 2, "Must be defined as 'host:port'");
nodes.add(new RedisNode(parts[0], Integer.valueOf(parts[1])));
} catch (RuntimeException ex) {
throw new IllegalStateException(
"Invalid redis sentinel " + "property '" + node + "'", ex);
}
}
return nodes;
}
}
- 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
可以看出来,操作sentinel时,不是直接操作sentinel的,而是通过sentinel找到主服务的IP地址和端口,然后再根据IP地址和端口去连接reids。
持久化
通常 Redis 将数据存储在内存中或虚拟内存中,它是通过以下两种方式实现对数据的持久化。
持久化即使存储到硬盘上,如果存储到内存中,电脑重启后内存就会被清空。
RDB(快照模式)
这种方式就是将内存中数据以快照的方式写入到二进制文件中,默认的文件名为dump.rdb。
客户端也可以使用save或者bgsave命令通知redis做一次快照持久化。save操作是在主线程中保存快照的,由于redis是用一个主线程来处理所有客户端的请求,这种方式会阻塞所有客户端请求,所以不推荐使用,bgsave是fork一个子进程来做的,就是后台执行,不阻塞执行。另一点需要注意的是,每次快照持久化都是将内存数据完整写入到磁盘一次,并不是增量的只同步增量数据。如果数据量大的话,写操作会比较多,必然会引起大量的磁盘IO操作,可能会严重影响性能。
redis会自动备份,而不需要我们自己手动的去调用save来保存备份。
通过配置文件可以看出:vi /usr/local/etc/redis.conf
save 900 1 //900秒里面发生1次操作则执行一次save。
save 300 10 //300秒了发生了10次操作则执行一次save
save 60 10000 //60秒里面发生了1万次操作则执行一次save。
注意:由于快照方式是在一定间隔时间做一次的,所以如果 redis意外当机的话,就会丢失最后一次快照后的所有数据修改。
AOF(日志追加)
这种方式redis会将每一个收到的写命令都通过write函数追加到文件中(默认appendonly.aof)。当redis重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当然由于操作系统会在内核中缓存write做的修改,所以可能不是立即写到磁盘上。这样的持久化还是有可能会丢失部分修改。不过我们可以通过配置文件告诉redis我们想要通过fsync函数强制操作系统写入到磁盘的时机。有三种方式如下(默认是:每秒fsync一次)
appendonly yes //启用日志追加持久化方式
//appendfsync always //每次收到写命令就立即强制写入磁盘,最慢的,但是保证完全的持久化,不推荐使用
appendfsync everysec //每秒钟强制写入磁盘一次,在性能和持久化方面做了很好的折中,推荐
//appendfsync no //完全依赖操作系统,性能最好,持久化没保证
AOF重写
AOF会自动优化文件。
比如:
> set key aaa
> set key bbb
> set key ddd
- 1
- 2
- 3
上面三个操作最终库里面数据为:key=ddd,那就简单了额,AOF会只需要存储set key ddd命令就OK了,其他两条命令可以忽略了,这样就可以达到优化存储的效果。
AOF 重写和 RDB 创建快照一样,都巧妙地利用了写时复制机制。
以下是 AOF 重写的执行步骤:
- Redis 执行 fork() ,现在同时拥有父进程和子进程。
- 子进程开始将新 AOF 文件的内容写入到临时文件。
- 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾: 这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
- 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。
- 现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。
恢复模式
数据恢复是内存数据被清空了,需要从硬盘中的备份文件重新加载到内存中。
在电脑重启后,如果两种持久化模式都开启了,则会优先从AOF中恢复数据,然后才是RDB模式。
两种方式的优缺点
RDB优点:
- RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
- RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心
RDB缺点:
- 如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。
- 每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。 在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。 虽然 AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。
AOF优点
- 使用 AOF 持久化会让 Redis 变得非常耐久(much more durable):你可以设置不同的 fsync 策略,比如无 fsync ,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次,在这种配置下,Redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据( fsync 会在后台线程执行,所以主线程可以继续努力地处理命令请求)。
- AOF 文件是一个只进行追加操作的日志文件(append only log), 因此对 AOF 文件的写入不需要进行 seek , 即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机,等等), redis-check-aof 工具也可以轻易地修复这种问题。
- Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写: 重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。 整个重写操作是绝对安全的,因为 Redis 在创建新 AOF 文件的过程中,会继续将命令追加到现有的 AOF 文件里面,即使重写过程中发生停机,现有的 AOF 文件也不会丢失。 而一旦新 AOF 文件创建完毕,Redis 就会从旧 AOF 文件切换到新 AOF 文件,并开始对新 AOF 文件进行追加操作。
- AOF 文件有序地保存了对数据库执行的所有写入操作, 这些写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。 导出(export) AOF 文件也非常简单: 举个例子, 如果你不小心执行了 FLUSHALL 命令, 但只要 AOF 文件未被重写, 那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
AOF缺点
- 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。
- 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB 。 在一般情况下, 每秒 fsync 的性能依然非常高, 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。 不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。
- AOF 在过去曾经发生过这样的 bug : 因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。 (举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug 。) 测试套件里为这种情况添加了测试: 它们会自动生成随机的、复杂的数据集, 并通过重新载入这些数据来确保一切正常。 虽然这种 bug 在 AOF 文件中并不常见, 但是对比来说, RDB 几乎是不可能出现这种 bug 的。
事务
Redis 事务可以一次执行多个命令, 并且带有以下两个重要的保证:
- 批量操作在发送 EXEC 命令前被放入队列缓存。
- 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行。
- 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。
一个事务从开始到执行会经历以下三个阶段:
- 开始事务。
- 命令入队。
- 执行事务。
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379> SET book-name "Mastering C++ in 21 days"
QUEUED
redis 127.0.0.1:6379> GET book-name
QUEUED
redis 127.0.0.1:6379> SADD tag "C++" "Programming" "Mastering Series"
QUEUED
redis 127.0.0.1:6379> SMEMBERS tag
QUEUED
redis 127.0.0.1:6379> EXEC
1) OK
2) "Mastering C++ in 21 days"
3) (integer) 3
4) 1) "Mastering Series"
2) "C++"
3) "Programming"
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
DISCARD :取消事务,放弃执行事务块内的所有命令。
redis keyspace键通知
keyspace有啥用? 请看下面回答
- 设置了生存时间的Key,在过期时能不能给一个通知?
- 如何使用 Redis 来实现定时任务?
- key监听事件,key的删除,添加.之类等等.
在 Redis 里面有一些事件,比如键到期、键被删除等。然后我们可以通过配置一些东西来让 Redis 一旦触发这些事件的时候就往特定的 Channel 推一条消息。
大致的流程就是我们给 Redis 的某一个 db 设置过期事件,使其键一旦过期就会往特定频道推消息,我在自己的客户端这边就一直消费这个频道就好了。
以后一来一条定时任务,我们就把这个任务状态压缩成一个键,并且过期时间为距这个任务执行的时间差。那么当键一旦到期,就到了任务该执行的时间,Redis 自然会把过期消息推去,我们的客户端就能接收到了。这样一来就起到了定时任务的作用。
第一步:需要开启事件通知
开启所有的事件
redis-cli> config set notify-keyspace-events KEA
开启keyspace Events
redis-cli> config set notify-keyspace-events KA
开启keyspace 所有List 操作的 Events
redis-cli> config set notify-keyspace-events Kl
redis-cli> config set notify-keyspace-events Ex // 其中Ex表示键事件通知里面的key过期事件,每当有过期键被删除时,会发送通知
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
或者在redis.conf配置文件中,找到notify-keyspace-events “”表示什么都不做,后面加上值即为开启.修改后需要重启redis服务
注意:默认情况下config在生产环境是不开启的,所以在生产上使用config会提示不存在的问题. config在redis.conf配置文件里面,去掉config前面的注解就可以使用了.
类型 | 备注 |
---|---|
K | 键空间通知,所有通知以 __keyspace@__ 为前缀 |
E | 键事件通知,所有通知以 __keyevent@__ 为前缀 |
g | DEL , EXPIRE ,RENAME 等类型无关的通用命令的通知 |
$ | 字符串命令的通知 |
l | 列表命令的通知 |
s | 集合命令的通知 |
h | 哈希命令的通知 |
z | 有序集合命令的通知 |
x | 过期事件:每当有过期键被删除时发送 |
e | 驱逐(evict)事件:每当有键因为 maxmemory 政策而被删除时发送 |
A | 参数 g$lshzxe 的别名 |
第二步:订阅通知事件
127.0.0.1:7000> psubscribe __keyevent@0__:expired
- 1
- 0表示第0个数据库,expired表示订阅过期事件.
- __keyevent@0__:expired 整个值为过期事件的topic.
- 订阅到的数据只有key,没有value哦.
集群
Redis 集群是一个提供在多个Redis间节点间共享数据的程序集。
Redis集群并不支持处理多个keys的命令,因为这需要在不同的节点间移动数据,从而达不到像Redis那样的性能,在高负载的情况下可能会导致不可预料的错误.
Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令.
Redis 集群的优势:
- 自动分割数据到不同的节点上。
- 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。
Redis 集群的数据分片
Redis 集群没有使用一致性hash, 而是引入了哈希槽的概念.
Redis 集群有16384个(2的14次方)哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:
- 节点 A 包含 0 到 5500号哈希槽.
- 节点 B 包含5501 到 11000 号哈希槽.
- 节点 C 包含11001 到 16384号哈希槽.
这种结构很容易添加或者删除节点. 比如如果我想新添加个节点D, 我需要从节点 A, B, C中得部分槽到D上. 如果我像移除节点A,需要将A中得槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可. 由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.
为了使得集群在一部分节点下线或者无法与集群的大多数(majority)节点进行通讯的情况下, 仍然可以正常运作, Redis 集群对节点使用了主从复制功能: 集群中的每个节点都有 1 个至 N 个复制品(replica), 其中一个复制品为主节点(master), 而其余的 N-1 个复制品为从节点(slave)。
注意:节点 A 、B 、C 的例子中, 如果节点 B 下线了, 那么集群将无法正常运行, 因为集群找不到节点来处理 5501 号至 11000号的哈希槽。A,C节点也将不可用了哦。
为了解决上面说的B下线了, 导致整个集群不可以用了,我们需要使用集群的另外一个功能,主从复制功能。
为A,B,C,添加一个或者多个从节点A1,B1,C1,这样B节点挂了,B1节点会自动升级为主节点,接替B的工作,为集群工作。这个很像哨兵模式,其实就是哨兵模式。
在redis的集群模块中,添加了哨兵模式,主节点挂了从节点会自动切换为主节点。
整个集群模式如图:
架构细节:
- 所有的redis节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽.
- 节点的fail是通过集群中超过半数的节点检测失效时才生效.
- 客户端与redis节点直连,不需要中间proxy层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
- redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster 负责维护node<->slot<->value
Redis 一致性保证
Redis 并不能保证数据的强一致性. 这意味这在实际中集群在特定的条件下可能会丢失写操作.
第一个原因是因为集群是用了异步复制. 写操作过程:
- 客户端向主节点B写入一条命令.
- 主节点B向客户端回复命令状态.
- 主节点将写操作复制给他得从节点 B1, B2 和 B3.
主节点对命令的复制工作发生在返回命令回复之后, 因为如果每次处理命令请求都需要等待复制操作完成的话, 那么主节点处理命令请求的速度将极大地降低 —— 我们必须在性能和一致性之间做出权衡。
注意:Redis 集群可能会在将来提供同步写的方法。 Redis 集群另外一种可能会丢失命令的情况是集群出现了网络分区, 并且一个客户端与至少包括一个主节点在内的少数实例被孤立。
举个例子 假设集群包含 A 、 B 、 C 、 A1 、 B1 、 C1 六个节点, 其中 A 、B 、C 为主节点, A1 、B1 、C1 为A,B,C的从节点, 还有一个客户端 Z1 假设集群中发生网络分区,那么集群可能会分为两方,大部分的一方包含节点 A 、C 、A1 、B1 和 C1 ,小部分的一方则包含节点 B 和客户端 Z1 .
Z1仍然能够向主节点B中写入, 如果网络分区发生时间较短,那么集群将会继续正常运作,如果分区的时间足够让大部分的一方将B1选举为新的master,那么Z1写入B中得数据便丢失了.
注意, 在网络分裂出现期间, 客户端 Z1 可以向主节点 B 发送写命令的最大时间是有限制的, 这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项:
port 7000 //redis端口
cluster-enabled yes //开启集群
cluster-config-file nodes.conf //集群配置文件
cluster-node-timeout 5000 //集群节点超时设置
appendonly yes //开启aof持久化
- 1
- 2
- 3
- 4
- 5
mac集群搭建
要让集群正常工作至少需要3个主节点,在这里我们要创建6个redis节点,其中三个为主节点,三个为从节点,对应的redis节点的ip和端口对应关系如下(为了简单演示都在同一台机器上面)
安装redis
brew install redis
由于配置了.bash_profile变量,所以可以在任何地方直接启动redis
1.直接启动
redis-server
2.带配置文件启动
redis-server /Users/baowenwei/service/redis/7000/redis7000.conf
- 创建6个文件夹7000,7001,…;
- 在每个文件夹里面创建一个配置文件,redis7000.conf,redis7001.conf
- 在每个redis.conf里面配置如下:
7000.conf
port 7000
dbfilename dump.rdb
dir /Users/baowenwei/service/redis/7000
cluster-enabled yes
- 1
- 2
- 3
- 4
7001.conf
port 7001
dbfilename dump.rdb
dir /Users/baowenwei/service/redis/7001
cluster-enabled yes
- 1
- 2
- 3
- 4
…
- 由于集群的脚本是使用ruby写的,所以还需要安装ruby。
brew install ruby.
- 安装ruby后,就可以使用ruby安装ruby操作redis的redis包了。
gam install redis -v 3.2 //安装完ruby后,gam命令就自己有了。
- 由于我使用的是brew在mac上面安装的redis,所以没有源码包,现在需要下载源码包,从官网下载源码包到本地。
- 进入源码包里面cd ~/service/redis/redis-3.2.11(根据个人自己的路径来看),执行
make //编译打包的意思。
编译后可以从src里面看到redis-trib.rb这个文件了。
使用cp redis-trib.rb /usr/local/bin/redis-trib ///usr/local/bin这个目录已经配置到环境变量的pash下面了, 这样就可以在任何地方直接使用redis-trib了。
- 以此启动7000—7005节点。
redis-server /Users/baowenwei/service/redis/7000/redis7000.conf
- 创建集群
redis-trib create --replicas 1 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005
create表示创建,选项 --replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。。
- 进入客户端操作了。
$ redis-cli -c -p 7002
127.0.0.1:7002> set 777 ddd
-> Redirected to slot [6787] located at 127.0.0.1:7001
OK
127.0.0.1:7001> set ffff sddd
-> Redirected to slot [14956] located at 127.0.0.1:7002
OK
127.0.0.1:7002> set ffff sddd
OK
127.0.0.1:7002>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
可以看到777这个key计算后应该存放到7001下面的卡曹,这时候会切换到7001节点上去,然后你需要重新执行一边命令,之前的并没有执行成功哦。
注意事项:在第一次创建集群前,不能有dump.rdb文件
redis穿透(转载)
缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,但是出于容错的考虑,如果从存储层查不到数据则不写入缓存层。
防穿透
穿透过程
- 缓存层不命中
- 存储层不命中,所以不将空结果写回缓存
- 返回空结果
缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义,这就是redis穿透。
解决方案1:
缓存空对象会有两个问题:
第一,空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间 (如果是攻击,问题更严重 ),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
第二,缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
解决方案2:
布隆过滤器拦截
缓存热点 key 重建优化
开发人员使用缓存 + 过期时间的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是有两个问题如果同时出现,可能就会对应用造成致命的危害:
当前 key 是一个热点 key( 例如一个热门的娱乐新闻),并发量非常大。
重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。
在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。
热点 key 失效后大量线程重建缓存要解决这个问题也不是很复杂,但是不能为了解决这个问题给系统带来更多的麻烦,所以需要制定如下目标:
- 减少重建缓存的次数
- 数据尽可能一致
- 较少的潜在危险
互斥锁 (mutex key)
此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可
(1) 从 Redis 获取数据,如果值不为空,则直接返回值,否则执行下面的步骤
(2) 如果 set(nx 和 ex) 结果为 true,说明此时没有其他线程重建缓存,那么当前线程执行缓存构建逻辑。
(2.2) 如果 setnx(nx 和 ex) 结果为 false,说明此时已经有其他线程正在执行构建缓存的工作,那么当前线程将休息指定时间 ( 例如这里是 50 毫秒,取决于构建缓存的速度 ) 后,重新执行函数,直到获取到数据。
redis雪崩(转载)
雪崩是指如果redis突然挂掉了,所有请求都打到了db上面,这个时候有可能会把db给打垮掉。
预防和解决缓存雪崩问题,可以从以下三个方面进行着手。
-
保证缓存层服务高可用性。
和飞机都有多个引擎一样,如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务,例如前面介绍过的 Redis Sentinel 和 Redis Cluster 都实现了高可用。 -
依赖隔离组件为后端限流并降级。
无论是缓存层还是存储层都会有出错的概率,可以将它们视同为资源。作为并发量较大的系统,假如有一个资源不可用,可能会造成线程全部 hang 在这个资源上,造成整个系统不可用。降级在高并发系统中是非常正常的:比如推荐服务中,如果个性化推荐服务不可用,可以降级补充热点数据,不至于造成前端页面是开天窗。
在实际项目中,我们需要对重要的资源 ( 例如 Redis、 MySQL、 HBase、外部接口 ) 都进行隔离,让每种资源都单独运行在自己的线程池中,即使个别资源出现了问题,对其他服务没有影响。但是线程池如何管理,比如如何关闭资源池,开启资源池,资源池阀值管理,这些做起来还是相当复杂的,这里推荐一个 Java 依赖隔离工具 Hystrix(https://github.com/Netflix/Hystrix),如下图所示。
参考地址
http://it.dataguru.cn/article-10994-1.html 防穿透,雪崩
http://doc.redisfans.com/index.html redis命令中文版
https://www.cnblogs.com/gomysql/p/4395504.html集群搭建
http://www.redis.cn/topics/cluster-tutorial.html 集群搭建
https://segmentfault.com/a/1190000008188655#articleHeader4 bitmap讲解