Redis

NoSQL

NoSQL = Not Only SQL

NoSQL特点

  1. 方便扩展(数据之间没有关系,很好扩展)
  2. 高性能
  3. 数据类型是多样性的
  4. 传统的 RDBMS和NoSQL对比
1
2
3
4
5
6
7
8
传统的 RDBMS
- 结构化组织
- SQL
- 数据和关系都存在单独的表中 row col
- 操作操作,数据定义语言
- 严格的一致性
- 基础的事务
- .....
1
2
3
4
5
6
7
8
Nosql
- 不仅仅是数据
- 没有固定的查询语言
- 键值对存储,列存储,文档存储,图形数据库(社交关系)
- 最终一致性,
- CAP定理和BASE (异地多活) 初级架构师!(狂神理念:只要学不死,就往死里学!)
- 高性能,高可用,高可扩
- ....

NoSQL的四大分类

分类 Examples举例 典型应用场景 数据模型 优点 缺点
key-value Tyrant, Redis, Voldemort, Oracle BDB 内容缓存,主要用于处理大量数据的高访问负载 Key-Value对应的键值对,通常用hash table来实现 查找速度快 数据无结构化,通常只被当作字符串或者二进制数据
列存储数据库 Cassandra, HBase, Riak 分布式的文件系统 以列簇式存储,将同一列数据存在一起 查找速度快,可扩展性强,更容易进行分布式扩展 功能相对局限
文档型数据库 CouchDB, MongoDb Web应用 Key-Value对应的键值对,Value为结构化数据 数据结构要求不严格,表结构可变,不需要像关系型数据库一样需要预先定义表结构 查询性能不高,而且缺乏统一的查询语法。
图形数据库 Neo4J, InfoGrid, Infinite Graph 社交网络,推荐系统等。专注于构建关系图谱 图结构 利用图结构相关算法。比如最短路径寻址,N度关系查找等 很多时候需要对整个图做计算才能得出需要的信息,不容易做分布式集群方案

Redis入门

Redis(Remote Dictionary Server ),即远程字典服务,

是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

Redis 能干嘛?

  1. 内存存储、持久化,内存中是断电即失、所以说持久化很重要(rdb、aof)

  2. 效率高,可以用于高速缓存

  3. 发布订阅系统

  4. 地图信息分析

  5. 计时器、计数器(浏览量!)

特性

  1. 多样的数据类型
  2. 持久化
  3. 集群
  4. 事务

学习中需要用到的东西

  1. 狂神的公众号:狂神说
  2. 官网:https://redis.io/
  3. 中文网:http://www.redis.cn/

基础知识

redis默认有16个数据库,默认使用的是第0个

SELECT:切换数据库,DBSIZE查看当前db大小

1
2
3
4
Redis:0>select 1
"OK"
Redis:1>dbsize
"0"

FLUSHDB:清除当前数据库

FLUSHALL:清除全部数据库的内容

Redis是单线程的

明白Redis是很快的,官方表示,Redis是基于内存操作,CPU不是Redis性能瓶颈,Redis的瓶颈是根据机器的内存和网络带宽,既然可以使用单线程来实现,就使用单线程了!所有就使用了单线程了!

Redis 是C 语言写的,官方提供的数据为 100000+ 的QPS,完全不比同样是使用 key-vale的Memecache差!

Redis 为什么单线程还这么快?

1、误区1:高性能的服务器一定是多线程的?

2、误区2:多线程(CPU上下文会切换!)一定比单线程效率高!

先去CPU>内存>硬盘的速度要有所了解!

核心:redis 是将所有的数据全部放在内存中的,所以说使用单线程去操作效率就是最高的,多线程(CPU上下文会切换:耗时的操作!!!),对于内存系统来说,如果没有上下文切换效率就是最高的!多次读写都是在一个CPU上的,在内存情况下,这个就是最佳的方案!

五大数据类型

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

Redis-Key

官方中文文档:http://www.redis.cn/commands.html

基本命令:

  1. keys pattern( 使用 * 表示查询所有的key)
  2. del key[key2 key3…] (删除键值对)
  3. move key db(移动key到指定db)
  4. exists key (不存在返回0,存在返回1)
  5. type key (返回value 的类型)

Strings(字符串)

  1. 介绍

    1. 字符串类型是Redis中最为基础的数据存储类型

    2. 二进制安全,意味着一个Redis字符串能包含任意类型的数据

    3. 最多可以容纳的数据长度是512M字节。

    4. 有趣用法:

      • 利用INCR命令簇(INCR, DECR, INCRBY)来把字符串当作原子计数器使用。

      • 利用SETRANGEGETRANGE命令,可以把字符串当成线性数组,随机访问只要O(1)复杂度

  2. set

    1. 设置或覆盖一个值:SET key value [EX seconds] [PX milliseconds] [NX|XX]

      1
      2
      3
      4
      5
      - EX *seconds* – 设置键key的过期时间,单位时秒
      - PX *milliseconds* – 设置键key的过期时间,单位时毫秒
      - NX – 只有键key不存在的时候才会设置key的值
      - XX – 只有键key存在的时候才会设置key的值
      注意: 由于SET命令加上选项已经可以完全取代SETNX, SETEX, PSETEX的功能,所以在将来的版本中,redis可能会不推荐使用并且最终抛弃这几个命令。
    2. 设置多个值:mset key value [key value …]

    3. 设置values到对应keys上: msetnx key value [key value …],是个原子操作,要么一起成功(返回1),要么一起失败(返回0)

    1. 获取指定key的值:get key,只处理String类型的值
    2. 获取多个key的值:mget key [key …]
    3. 返回指定范围的value的子串:getrange key start end
    4. 获取指定字符串的长度:strlen key
  3. 存值并返回原值:getset key value (get andthen set),如果原本不存在值,则返回null

    1. 从指定的offset处开始,覆盖value的长度:setrange key offset value
    2. 追加值:append key value,如果当前key不存在,就相当于set key

使用示例

set、get、append:

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> setnx name atomsk # 创建值
OK
127.0.0.1:6379> get name
"atomsk"
127.0.0.1:6379> set name fanerlin # 覆盖值
OK
127.0.0.1:6379> get name
"fanerlin"
127.0.0.1:6379> append name 85 # 追加值
(integer) 10
127.0.0.1:6379> get name
"fanerlin85"

mset、mget、setnx、msetnx、getset:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
127.0.0.1:6379> mset k1 v1 k2 v2
OK
127.0.0.1:6379> mget k1 k2
1) "v1"
2) "v2"
127.0.0.1:6379> setnx k2 v2 # 失败,因为k2已存在
(integer) 0
127.0.0.1:6379> setnx k3 v3
(integer) 1
127.0.0.1:6379> msetnx k3 v3 k4 v4 # 失败,因为k3已存在
(integer) 0
127.0.0.1:6379> msetnx k4 v4 k5 v5 # 成功,可见上面的k4创建失败了
(integer) 1
127.0.0.1:6379> getset k5 v5n # 返回的是修改前的值
"v5"
127.0.0.1:6379> get k5
"v5n"
  1. incrdecrincrbydecrby (i++、i– 和 i+= 步长、 i-= 步长)
1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> set views 10
OK
127.0.0.1:6379> incr views
(integer) 11
127.0.0.1:6379> decr views
(integer) 10
127.0.0.1:6379> incrby views 10
(integer) 20
127.0.0.1:6379> decrby views 10
(integer) 10
  1. getrange字符串截取和setrange字符串替换
1
2
3
4
5
6
7
8
127.0.0.1:6379> set str abcde
OK
127.0.0.1:6379> getrange str 0 2 # 截取字符串 [0,2]
"abc"
127.0.0.1:6379> setrange str 3 abc # 从指定位置开始替换字符串
(integer) 6
127.0.0.1:6379> get str
"abcabc"
  1. 对象操作
1
2
3
4
5
6
7
8
9
10
11
12
# 这里的key是一个巧妙的设计: user:{id}:{filed} , 如此设计在Redis中是完全OK了!
127.0.0.1:6379> mset user:1:name atomsk user:1:age 22
OK
127.0.0.1:6379> mget user:1:name user:1:age
1) "atomsk"
2) "22"
127.0.0.1:6379> set user:2 {name:fanerlin,age:85} # 用json字符来保存一个对象
OK
127.0.0.1:6379> get user:2
"{name:fanerlin,age:85}"
127.0.0.1:6379> get user:1
(nil)

使用Redis Desktop Manager查看:

![image-20200804134314476](redis/Sting Object.png)

Lists(列表)

  1. 介绍
    1. List类型是按照插入顺序排序的字符串列表。(可以左右操作)
    2. 有序(插入顺序),有索引,可重复
    3. 有趣用法:
      • 在社交网络中建立一个时间线模型,使用LPUSH去添加新的元素到用户时间线中,使用LRANGE去检索一些最近插入的条目。
      • 你可以同时使用LPUSHLTRIM去创建一个永远不会超过指定元素数目的列表并同时记住最后的N个元素。
    1. Lpush list value1[value2…]
    2. Rpush list value1[value2…]
    3. Lpushx list value,当list存在时才插入value到list表头
    4. Rpushx list value,当list存在时才插入value到list表尾
    1. 查询出指定区间元素:lrange list start end (闭区间),示例:lrange list 0 -1 (查询所有)
    2. 通过索引获得值:lindex list index , 从0开始,从左到右,右边第一个索引-1
    3. 查列表的长度:llen list
    1. Lpop list ,移除列表的第一个元素
    2. Rpop list ,移除列表的最后一个元素
    3. lrem list count element,移除列表指定个数的指定元素,优先移除左边元素
    1. ltram list start end,通过下标截取list,会改变原来的list
    2. lset list index value,修改指定索引上的值,修改不存在list或者索引越界会报错
    3. linsert key BEFORE|AFTER 位置 value,在指定位置前|后插入值
    4. RpopLpush sourceList targetList,Rpop出一个列表的元素,并将其Lpush到其他列表中!

使用示例

  1. Lpush、Rpush、lrange、lrem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
127.0.0.1:6379> lpush list one two three
(integer) 3
127.0.0.1:6379> rpush list three
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "three"
2) "two"
3) "one"
4) "three"
127.0.0.1:6379> lrem list 1 three # 移除一个three
(integer) 1
127.0.0.1:6379> lrange list 0 -1 # 优先移除掉了左边的three
1) "two"
2) "one"
3) "three"
  1. ltram、lset
1
2
3
4
5
6
7
8
9
10
11
12
13
14
127.0.0.1:6379> ltrim list 1 2 # 截取保留区间[1,2]的值
OK
127.0.0.1:6379> lrange list 0 -1
3) "one"
4) "three"
127.0.0.1:6379> lset list -1 four # 修改最后一个值为 four
OK
127.0.0.1:6379> lrange list 0 -1
3) "one"
4) "four"
127.0.0.1:6379> lset list123 1 one # 修改不存在的list
(error) ERR no such key
127.0.0.1:6379> lset list 5 five # 修改不存在的索引
(error) ERR index out of range
  1. linsert
1
2
3
4
5
6
7
8
9
127.0.0.1:6379> linsert list after one two
(integer) 3
127.0.0.1:6379> linsert list before four three
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "one"
2) "two"
3) "three"
4) "four"

小结

  • 他实际上是一个链表, left,right,before|after Node ,都可以插入值
  • 如果移除了所有值,空链表,也代表不存在!
  • 在两边插入或者改动值,效率最高!改动中间元素,相对来说效率会低一点~

消息队列 (Lpush Rpop), 栈( Lpush Lpop)!

Sets(集合)

  1. 介绍

    1. 无序的字符串合集,可以以O(1) 的时间复杂度(无论集合中有多少元素时间复杂度都为常量)完成 添加,删除以及测试元素是否存在的操作。
    2. 有趣用法:
      • 用集合跟踪一个独特的事。想要知道所有访问某个博客文章的独立IP?只要每次都用SADD来处理一个页面访问。那么你可以肯定重复的IP是不会插入的。
      • Redis集合能很好的表示关系。你可以创建一个tagging系统,然后用集合来代表单个tag。接下来你可以用SADD命令把所有拥有tag的对象的所有ID添加进集合,这样来表示这个特定的tag。如果你想要同时有3个不同tag的所有对象的所有ID,那么你需要使用SINTER.
    1. 增加一个或多个元素成员:sadd set value[value2…],如果成员已存在则忽略
    1. 查所有成员:smembers
    2. 随机抽选出一个或多个成员:srandmember set [count]
    3. 判断是否是set的成员:sismember set value
    4. 查成员数量:scard set
    1. 随机删除一个或多个成员:spop set [count]
    2. 删除指定成员:srem set members[member1,member2…]
    1. 移动指定成员到目标set:smove sourceSet targetSet value
  2. 集合操作

    1. 差集:Sdiff key [key …],返回一个集合与给定集合的差集的元素.
    2. 交集:Sinter key [key …],返回指定所有的集合的成员的交集.
    3. 并集:Sunion key [key …],返回给定的多个集合的并集中的所有成员.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
127.0.0.1:6379> sadd set1 a b c
(integer) 3
127.0.0.1:6379> sadd set2 b c d
(integer) 3
127.0.0.1:6379> sdiff set1 set2
1) "a"
127.0.0.1:6379> sdiff set2 set1
1) "d"
127.0.0.1:6379> sinter set1 set2
1) "c"
2) "b"
127.0.0.1:6379> sunion set1 set2
1) "c"
2) "b"
3) "d"
4) "a"

Hashes(哈希)

  1. Redis Hashes是字符串字段和字符串值之间的映射,可以看成具有String Key和对象数据(filed value)的Map容器
    1. 设置一个或多个值:hset key field1 value1 [field2 value2 …],返回1是新增,0是修改
    2. hmsethset用法和作用一样
  2. 指定字段不存在时,设置值:hsetnx
    1. 查一个字段的值:hget key field
    2. 查多个字段的值:hmget key field1 [field2 …]
    3. 查出所有字段的名字:hkeys key
    4. 查出所有字段的值:hvals key
    5. 查所有的字段和值:hgetall key
    6. 获取hash表的字段数量:hlen key
    7. 获取指定字段的长度:hstrlen key field
  3. 判断指定字段是否存在:hexists key field,存在返回1,不存在0
  4. 删除一个或多个字段:hdel key field1 [field2 …]
  5. 增加|减少字段的值:hincrbyhdecrby key field value

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
127.0.0.1:6379> hset user name atomsk age 22
(integer) 2
127.0.0.1:6379> hgetall user
1) "name"
2) "atomsk"
3) "age"
4) "22"
127.0.0.1:6379> hkeys user
1) "name"
2) "age"
127.0.0.1:6379> hvals user
1) "atomsk"
2) "22"
127.0.0.1:6379> hlen user
(integer) 2
127.0.0.1:6379> hstrlen user name
(integer) 6
127.0.0.1:6379> hexists user email
(integer) 0

hash变更的数据 user name age,尤其是是用户信息之类的,经常变动的信息! hash 更适合于对象的存储,String更加适合字符串存储!

Sorted sets(有序集合)

  1. 介绍

    1. 和集合的差别是,每个有序集合的成员都关联着一个评分(score),集合成员按score从低到高排序
    2. 一个数据(score,value) 其中score可重复,value不可重复
    3. 有趣用法:
      • 在一个巨型在线游戏中建立一个排行榜,每当有新的记录产生时,使用ZADD 来更新它。你可以用ZRANGE轻松地获取排名靠前的用户, 你也可以提供一个用户名,然后用ZRANK获取他在排行榜中的名次。 同时使用ZRANKZRANGE你可以获得与指定用户有相同分数的用户名单。 所有这些操作都非常迅速。
      • 有序集合通常用来索引存储在Redis中的数据。 例如:如果你有很多的hash来表示用户,那么你可以使用一个有序集合,这个集合的年龄字段用来当作评分,用户ID当作值。用ZRANGEBYSCORE可以简单快速地检索到给定年龄段的所有用户。
  2. 通用指令

    1. 将指定成员添加到ss里:zadd key [NX|XX] [CH] [INCR] score member [score member …]

      1
      2
      3
      4
      XX: 仅仅更新存在的成员,不添加新成员。
      NX: 不更新存在的成员。只添加新成员。
      CH: 修改返回值为发生变化的成员总数,原始是返回新添加成员的总数 (CH 是 changed 的意思)。更改的元素是新添加的成员,已经存在的成员更新分数。 所以在命令中指定的成员有相同的分数将不被计算在内。注:在通常情况下,ZADD返回值只计算新添加成员的数量。
      INCR: 当ZADD指定这个选项时,成员的操作就等同ZINCRBY命令,对成员的分数进行递增操作。
    2. 返回指定索引范围的元素:zrange key start stop[withscores],从高到低:zrevrange

    3. 返回成员数量:zcard key

    4. 移除指定成员:zrem key member [member …]

  3. *score *分数相关指令

    1. 返回成员的score值:zscore key member

    2. score从低到高排序:zrangebyscore key min max [withscores] [limit offset count],min和max指定socre的范围,使用limit进行分页,从高到低:zrevrangebyscore

      1
      2
      3
      ##区间及无限
      min和max可以是-inf和+inf,这样一来,你就可以在不知道有序集的最低和最高score值的情况下,使用ZRANGEBYSCORE这类命令。
      默认情况下,区间的取值使用闭区间,你也可以通过给参数前增加 ( 符号来使用可选的开区间。
    3. 获取指定score区间的成员数量:zcount key min max

    4. 移除指定score区间的成员:zremrangebyscore key min max

    5. 删除一个或多个score最高|最低的成员:zpopmax|zpopmin key [count]

  4. lex 相关指令(成员分数相同时才能使用):

    1. 返回指定成员区间内的成员,按成员字典正序排序:zrangebylex key min max [LIMIT offset count],按倒序:zrevrangebylex
    2. 获取指定成员之间的成员数量:zlexcount zset [member1 [member2
    3. 删除名称按字典由低到高排序成员之间所有成员:zremrangebylex key min max
  5. rank 排名相关指令(score值最小的成员排名为0):

    1. 返回指定成员的排名,按从小到大排:zrank key member,从大到小排:zrevrank
    2. 移除指定排名区间的成员:zremrangebyrank key start stop
  6. 集合操作

    1. 并集:zunionstore destination numkeys key [key …] [WEIGHTS weight] [AGGREGATE SUM|MIN|MAX]

      1
      2
      3
      把numkeys个集合的并集放到destination中
      weights,乘法因子,可选,有几个key就得有几个weight,让score在传递给聚合函数前相乘
      aggregate,SUM,保留score为所有score之和,MAX|MIN,选择保留最大或者最小score
    2. 交集:zinterstore,用法和上面一样

使用示例

  1. 通用命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    127.0.0.1:6379> zadd zset 10 one 20 two 30 three
    (integer) 3
    127.0.0.1:6379> zrange zset 1 2 withscores # 返回索引[1,2]的成员
    1) "two"
    2) "20"
    3) "three"
    4) "30"
    127.0.0.1:6379> zcard zset
    (integer) 3
    127.0.0.1:6379> zrem zset one # 移除成员 one
    (integer) 1
  2. score相关命令

  3. lex相关命令

  4. rank相关命令

  5. 集合操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    redis 127.0.0.1:6379> ZRANGE programmer 0 -1 WITHSCORES
    1) "peter"
    2) "2000"
    redis 127.0.0.1:6379> ZRANGE manager 0 -1 WITHSCORES
    1) "bob"
    2) "4000"
    # 公司决定加薪。。。除了程序员。。。
    redis 127.0.0.1:6379> ZUNIONSTORE salary 2 programmer manager WEIGHTS 1 3
    (integer) 2
    redis 127.0.0.1:6379> ZRANGE salary 0 -1 WITHSCORES
    1) "peter"
    2) "2000"
    3) "bob"
    4) "12000"

特殊数据类型

Geospatial 地理位置

Hyperloglog 基数统计

Bitmap 位图

事务

Redis 事务:事务中的所有命令都会序列化、按顺序地执行。

Redis事务不保证原子性,原因:

  • 有命令入队失败(如出现编译型异常)时,事务中所有命令都不会被执行,这种情况保证原子性,但是
  • 命令存在语法问题(如 incr 一个非数值型字符串)但入队成功,在执行失败时,不会影响到其他命令的正常执行

事务执行流程:

  • 使用muliti开启事务

  • 执行命令,让命令进入事务队列

  • 使用exec执行事务

    或者

  • 使用discard清空事务队列, 并放弃执行事务。

WATCH

WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。

使用watch命令实现乐观锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> watch money # 监视key money
OK
127.0.0.1:6379> multi # 如果数据期间没有发生变动,事务就正常执行成功!
OK
127.0.0.1:6379> decrby money 20
QUEUED
127.0.0.1:6379> incrby pay 20
QUEUED
127.0.0.1:6379> exec
1) (integer) 80
2) (integer) 20

取消对键的监控:

  1. 使用unwatch手动取消对所有键的监视
  2. 执行exec后, 不管事务是否成功执行, 对所有键的监视都会被取消。
  3. 执行discard

脚本和事务

从定义上来说, Redis 中的脚本本身就是一种事务, 所以任何在事务里可以完成的事, 在脚本里面也能完成。 并且一般来说, 使用脚本要来得更简单,并且速度更快。

Jedis

导入依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.73</version>
</dependency>

常用API

方法名和命令一样

https://gitee.com/Atomsk/practice/tree/redis/redis-01-jedis

事务

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
@Test
public void txTest() {
Jedis jedis = new Jedis("127.0.0.1",6379);
System.out.println(jedis.ping());
jedis.flushDB();
JSONObject jsonObject = new JSONObject();
jsonObject.put("hello","world");
jsonObject.put("name","kuangshen");
String result = jsonObject.toJSONString();
// 开启事务
Transaction multi = jedis.multi();
try {
multi.set("user1",result);
int i = 1/0 ; // 代码抛出异常事务,执行失败!
multi.set("user2",result);
multi.exec(); // 执行事务!
} catch (Exception e) {
multi.discard(); // 放弃事务
e.printStackTrace();
} finally {
System.out.println(jedis.get("user1"));
System.out.println(jedis.get("user2"));
jedis.close(); // 关闭连接
}
}

SpringBoot整合

SpringBoot 操作数据:spring-data jpa jdbc mongodb redis!

说明: 在 SpringBoot2.x 之后,原来使用的jedis 被替换为了 lettuce?

jedis : 采用的直连,多个线程操作的话,是不安全的,如果想要避免不安全的,使用 jedis pool 连接池! 更像 BIO 模式

lettuce : 采用netty,实例可以再多个线程中进行共享,不存在线程不安全的情况!可以减少线程数据了,更像 NIO 模式
源码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
@ConditionalOnMissingBean(name = {"redisTemplate"})
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
//默认的 RedisTemplate 没有过多的设置,redis 对象都是需要序列化!
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}

@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
  1. 导入依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
  2. 配置

    1
    2
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
  3. 测试使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    void testTemplate() {
    RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
    connection.flushDb();
    redisTemplate.opsForValue().set("k1", "v1");
    System.out.println(redisTemplate.opsForValue().get("k1"));
    }

使用原生的RedisTemplate会有编码问题,因为使用的都是默认的序列化方式

所以自己配置一个RedisTemplate:

自定义RedisTemplate

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
@Configuration
//@author 狂神说
public class RedisConfig {
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// 我们为了自己开发方便,一般直接使用 <String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(factory);

// Json序列化配置
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

// key采用String的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value序列化方式采用jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();

return template;
}
}

然后把自动注入改成以下即可

1
2
@Autowired
private RedisTemplate<String,Object> redisTemplate

RedisUtil

企业开发中,一般不使用原生RedisTemplate的API,会将自己自定义的Template再进行封装,使用Redis工具类:

https://gitee.com/Atomsk/practice/blob/redis/redis-02-springboot/src/main/java/com/atomsk/utils/RedisUtils.java

配置详解

网络相关

1
2
3
4
5
6
7
8
9
10
11
#bind 127.0.0.1   # 绑定的ip,使用docker时不配置
protected-mode yes # 保护模式
port 6379  # 端口设置

# 在一个并发量高的环境中,你需要指定一个比较大的backlog值来避免慢连接的情况
# 注意,linux内核会默认 使用/proc/sys/net/core/somaxconn 的值来削减 backlog的实际值,
# 因此你需要确保提升 somaxconn 和 tcp_max_syn_backlog 这两个值来确保此处的backlog生效
tcp-backlog 511

# 关闭掉空闲N秒的连接(0则是不处理空闲连接)
timeout 0

General

1
2
3
4
5
6
7
8
9
10
11
12
13
daemonize no  # 以守护进程的方式运行,如果在使用docker管理redis时开启,会导致redis启动失败,因为docker的redis默认没有pidfile文件
pidfile /var/run/redis_6379.pid  # 如果以后台的方式运行,我们就需要指定一个 pid 文件

databases 16  # 数据库的数量,默认是 16 个数据库
always-show-logo no  # 是否总是显示LOGO

# 指定日志的记录级别的
# debug (尽可能多的日志信息,用于开发和测试之中)
# verbose (少但是有用的信息, 没有debug级别那么混乱)
# notice (适量的信息,用于生产环境)
# warning (只有非常重要和关键的信息会被记录)
loglevel notice
logfile "" # 日志的文件位置名

持久化相关

redis 是内存数据库,如果没有持久化,那么数据断电即失

RDB配置

AOF配置

主从复制

Replication配置

安全 Security

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> config get requirepass  # 获取redis的密码
1) "requirepass"
2) ""
127.0.0.1:6379> config set requirepass "123456"  # 设置redis的密码
OK
127.0.0.1:6379> config get requirepass  # 发现所有的命令都没有权限了
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456  # 使用密码进行登录!
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"

限制 Clients

1
2
3
4
5
6
7
8
9
maxclients 10000  # 设置能连接上redis的最大客户端的数量
maxmemory <bytes>  # redis 配置最大的内存容量
maxmemory-policy noeviction  # 内存容量超过maxmemory后的处理策略 
volatile-lru:利用LRU算法移除设置过过期时间的key。
volatile-random:随机移除设置过过期时间的key。
volatile-ttl:移除即将过期的key,根据最近过期时间来删除(辅以TTL)
allkeys-lru:利用LRU算法移除任何key。
allkeys-random:随机移除任何key。
noeviction:不移除任何key,只是返回一个写错误。

持久化

Redis 是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以 Redis 提供了持久化功能!

RDB(Redis Database)

什么是RDB

默认的redis数据库,主从复制中的从机,即数据库备份

在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时将快照文件直接读到内存里。

  • Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程是不进行任何IO操作的。这就确保了极高的性能。
  • 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,RDB方式要比AOF方式更加的高效。
  • 默认的持久化方式就是RDB,一般情况下不需要修改

db保存的文件是 dump.rdb ,可以在配置文件中进行配置,有时候在生产环境我们会将这个文件进行备份!

RDB配置

1
2
3
4
5
6
7
8
9
10
# 当存在最少一个key 变更时,900秒(15分钟)后保存到硬盘
save 900 1
save 300 10
save 60 10000

stop-writes-on-bgsave-error yes  # 持久化如果出错,是否还需要继续工作!
dbfilename dump.rdb # rdb文件名
rdbcompression yes # 是否压缩 rdb 文件,需要消耗一些cpu资源!
rdbchecksum yes # 保存rdb文件的时候,进行错误的检查校验!
dir ./  # rdb 文件保存的目录!

触发机制

  1. save的规则满足的情况下,会自动触发rdb规则
  2. 执行 flushall 命令,也会触发我们的rdb规则!
  3. 退出redis,也会产生 rdb 文件!

机制触发后会自动生成一个 dump.rdb文件

恢复方式

  1. 只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查dump.rdb 恢复其中的数据!

  2. 查看需要存在的位置

1
2
3
127.0.0.1:6379> config get dir
1) "dir"
2) "/usr/local/bin"  # 如果在这个目录下存在 dump.rdb 文件,启动就会自动恢复其中的数据

优点和缺点

优点

  1. 适合大规模的数据恢复!
  2. 对数据的完整性要求不高!

缺点

  1. 需要一定的时间间隔进行操作,如果redis意外宕机了,最后一次的修改数据就没有的了!
  2. fork进程的时候,会占用一定的内容空间!

AOF(Append Only File)

将我们的所有命令都记录下来,history,恢复的时候就把这个文件全部在执行一遍!

以日志的形式来记录每个写操作,将Redis执行过的所有指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

Aof保存的是 appendonly.aof 文件

AOF配置

AOF默认是不开启的,把 appendonly 改为yes就开启了 aof,重启 redis 就可以生效了,可以和RDB方式一起使用

1
2
appendonly no   # 默认不开启
appendfilename "appendonly.aof"  # 持久化的文件的名字

持久化策略

appendfsync always 同步持久化,每次发生数据变更会被立即记录到磁盘,性能差但数据完整性比较好

appendfsync everysec ,默认选项,异步操作,每秒记录,如果一秒钟内宕机,会有数据丢失

appendfsync no 将缓存回写的策略交给系统,linux 默认是30秒将缓冲区的数据回写硬盘的

重写规则

AOF采用文件追加的方式持久化数据,所以文件会越来越大,为了避免这种情况发生,增加了重写机制

当AOF文件的大小超过了配置所设置的阙值时,Redis就会启动AOF文件压缩,只保留可以恢复数据的最小指令集

触发机制:Redis会记录上次重写时的AOF文件大小,默认配置时当目前aof文件大小超过上一次重写的aof文件大小的百分之 100 ,且文件大于 64M 时触发

1
2
3
4
5
6
7
8
9
10
11
12
13
# 在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说,执行fsync会造成阻塞过长时间
# 为了减少延迟可以改为yes,为了更安全的持久化保持默认的no
no-appendfsync-on-rewrite no

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

# 当截断的aof文件被导入的时候,会自动发布一个log给客户端然后读取
# 关闭后需要手动修复aof文件
aof-load-truncated yes
# 在aof重写的时候,如果打开了aof-rewrite-incremental-fsync开关,
# 系统会每32MB执行一次fsync。这对于把文件写入磁盘是有帮助的,可以避免过大的延迟峰值。
aof-rewrite-incremental-fsync yes

AOF文件损坏修复方法

如果 aof 文件有错位,redis 会启动失败,可以使用 redis-check-aof --fix aoffile 进行修复

和RDB相比

优点:通常情况下AOF文件保存的数据比RDB文件保存的数据要完整

缺点

  1. 相对于数据文件来说,aof远远大于 rdb,修复的速度也比 rdb慢!
  2. Aof 运行效率也要比 rdb 慢,所以我们redis默认的配置就是rdb持久化!

小结

  1. RDB 持久化方式能够在指定的时间间隔内对你的数据进行快照存储

  2. AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以Redis 协议追加保存每次写的操作到文件末尾,Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大。

  3. 如果只做缓存,只希望数据在服务器运行的时候存在,可以不使用任何持久化

  4. 同时开启两种持久化方式

    在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。

    RDB 的数据不实时,同时使用两者时服务器重启也只会找AOF文件,那要不要只使用AOF呢?作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),快速重启,而且不会有AOF可能潜在的Bug,留着作为一个万一的手段。

  5. 性能建议

    • 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则
    • 如果Enable AOF ,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了,代价一是带来了持续的IO,二是AOF rewrite 的最后将 rewrite 过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上,默认超过原大小100%大小重写可以改到适当的数值。
    • 如果不Enable AOF ,仅靠 Master-Slave Repllcation 实现高可用性也可以,能省掉一大笔IO,也减少了rewrite时带来的系统波动。代价是如果Master/Slave 同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个 Master/Slave 中的 RDB文件,载入较新的那个,微博就是这种架构。

发布订阅

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。微信、

微博、关注系统!

Redis 客户端可以订阅任意数量的频道。

订阅/发布消息图:

第一个:消息发送者, 第二个:频道 第三个:消息订阅者!

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

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

命令

这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播、实时提醒等。

测试

订阅端:

1
2
3
4
5
6
7
8
127.0.0.1:6379> subscribe atomsk
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "atomsk"
3) (integer) 1
1) "message" # 消息
2) "atomsk" # 频道名字
3) "ground controll to major tom" #具体信息

发送端:

1
2
127.0.0.1:6379> publish atomsk "ground controll to major tom"
(integer) 0

原理

Redis是使用C实现的,通过分析 Redis 源码里的 pubsub.c 文件,了解发布和订阅机制的底层实现,籍此加深对 Redis 的理解。

Redis 通过 PUBLISH 、SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能。

微信公众号:

通过 SUBSCRIBE 命令订阅某频道后,redis-server 里维护了一个字典,字典的键就是一个个 频道!,而字典的值则是一个链表,链表中保存了所有订阅这个 channel 的客户端。SUBSCRIBE 命令的关键,就是将客户端添加到给定 channel 的订阅链表中。

通过 PUBLISH 命令向订阅者发送消息,redis-server 会使用给定的频道作为键,在它所维护的 channel字典中查找记录了订阅这个频道的所有客户端的链表,遍历这个链表,将消息发布给所有订阅者。

Pub/Sub 从字面上理解就是发布(Publish)与订阅(Subscribe),在Redis中,你可以设定对某一个key值进行消息发布及消息订阅,当一个key值上进行了消息发布后,所有订阅它的客户端都会收到相应的消息。这一功能最明显的用法就是用作实时消息系统,比如普通的即时聊天,群聊等功能。

使用场景

  1. 实时消息系统!
  2. 事实聊天!(频道当做聊天室,将信息回显给所有人即可!)
  3. 订阅,关注系统都是可以的!

稍微复杂的场景我们就会使用 消息中间件 MQ

主从复制

概念

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

数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave 以读为主。

*默认情况下,每台Redis服务器都是主节点 *

一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

主从复制的作用

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

一般来说,要将Redis运用于工程项目中,只使用一台Redis是万万不能的(宕机),原因如下:

  1. 从结构上,单个Redis服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力较大;
  2. 从容量上,单个Redis服务器内存容量有限,就算一台Redis服务器内存容量为256G,也不能将所有

内存用作Redis存储内存,一般来说,单台Redis最大使用内存不应该超过 20G

电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点也就是”多读少写“。

对于这种场景,我们可以使如下这种架构:

主从复制,读写分离! 80% 的情况下都是在进行读操作!减缓服务器的压力!架构中经常使用!

另外,slave服务器也可以有自己的slave服务器,这样的服务器称为sub-slave,而这些sub-slave通过主从复制最终数据也能与master保持一致,如下图所示:

只要在公司中,主从复制就是必须要使用的,因为在真实的项目中不可能单机使用Redis!

Replication配置

默认的replication配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 指导 slave 通过指定ip和端口号的 master 来获取DB数据
#replicaof <masterip> <masterport>
# 当主服务器开启了密码保护时使用
#masterauth <master-password>

# 如果从服务器失去了和主服务器之间的连接,或者当复制仍然处于处理状态的时候
# 1)如果设置为 yes(默认值),从机将会持续回复来自客户端的请求,可能会回复已经过期的数据,或者返回空的数据,当从服务器第一次异步请求数据时。
# 2)如果被设置为 no ,从服务器就会返回"SYNC with master in progress" 这个错误,来应答所有命令除了 INFO 和 SLAVEOF
replica-serve-stale-data yes

# 配置当前reids是从机时是否只读,默认为yes
replica-read-only yes

repl-diskless-sync no
repl-diskless-load disabled

repl-disable-tcp-nodelay no
# 主机宕机后,将高优先级的从机选举为主机
replica-priority 100

只需修改从机的以下配置:主库的ip和端口号,日志和rdb文件的名字

1
2
3
4
replicaof <masterip> <masterport> 
masterauth <master-password> # 如果master要求验证
logfile "redis.log"
dbfilename "dump.rdb"

修改完成后使用主机测试:

1
2
3
4
5
6
7
127.0.0.1:6379> info replication # 查看当前库的信息
# Replication
role:master
connected_slaves:2 # 从机数量和从机的具体信息
slave0:ip=129.204.240.134,port=6379,state=online,offset=3784,lag=0
slave1:ip=129.204.240.134,port=6379,state=online,offset=3784,lag=0
···

复制原理

复制方式

Redis主从复制分为以下三种方式:

  1. 增量复制:正常连接时,master会发生数据命令流给salve,将自身数据的改变复制到slave
  2. 部分复制:当连接断开后,slave在重连时会尝试重新获取断开后未同步的数据
  3. 全量复制:如果无法部分同步,则会请求进行全量复制,master将自己的rdb文件发送给slave,并记录同步期间的其他写入,再发送给slave,以达到完全同步的目的

工作原理

master会记录一个replicationId的伪随机字符串,用于标识当前的数据集版本,还会记录一个当数据集的偏移量offset,不管master是否有slave服务器,replication Id和offset会一直记录并成对存在,我们可以通过 info replication查看

1
2
3
4
···
master_replid:c524efe4dd8c213633ed47febb86cf088df05c4d
master_repl_offset:3784
···

当master与slave正常连接时,slave使用fsyn命令向master发送自己记录的旧master的replication id和offset,而master会计算与slave之间的数据偏移量,并将缓冲区中的偏移数量同步到slave,此时master和slave的数据一致。

而如果slave引用的replication太旧了,master与slave之间的数据差异太大,则master与slave之间会使用全量复制的进行数据同步。

其他细节

故障转移

如果主机宕机了,会根据replic-priority的值将优先级高的从机选举为主机——哨兵模式

或者,(不推荐)我们可以使用 SLAVEOF no one把从机设置成主机,再使用命令将其他节点手动连接到最新的这个主节点,如果后面默认的主机恢复了,那就重启服务,恢复到配置文件的主从配置。

主从复制中的key过期问题

slave服务器没有权限处理过期的key,这样的话,对于在master上过期的key,在slave服务器就可能被读取,所以master会累积过期的key,积累一定的量之后,发送del命令到slave,删除slave上的key。

哨兵模式

概述

主从切换技术的方法是:当主服务器宕机后,需要手动把一台从服务器切换为主服务器,这就需要人工干预,费事费力,还会造成一段时间内服务不可用。这不是一种推荐的方式,更多时候,我们优先考虑哨兵模式。Redis从2.8开始正式提供了Sentinel(哨兵) 架构来解决这个问题。

哨兵模式能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。

这里的哨兵有两个作用

  • 通过发送命令,让Redis服务器返回监控其运行状态,包括主服务器和从服务器。
  • 当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机。

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

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

测试示例

  1. 配置哨兵配置文件 sentinel.conf

    1
    2
    # sentinel monitor 被监控的名称 host port 进行投票时所需 认为主机已宕机的 哨兵数量
    sentinel monitor myredis 127.0.0.1 6379 1

    1表示只要有一个哨兵认为主机宕机了,就进行投票选举

  2. 可能用到的docker命令

1
2
3
4
5
6
docker run -d -p 6379:6379 --name redis -v /home/atomsk/redis/redis.conf:/etc/redis/redis.conf -v /home/atomsk/redis/data:/data redis redis-server /etc/redis/redis.conf

docker run -d -p 6380:6379 --name redis-s-01 -v /home/atomsk/redis/redis-s-01.conf:/etc/redis/redis.conf -v /home/atomsk/redis/data:/data redis redis-server /etc/redis/redis.conf
docker run -d -p 6381:6379 --name redis-s-02 -v /home/atomsk/redis/redis-s-02.conf:/etc/redis/redis.conf -v /home/atomsk/redis/data:/data redis redis-server /etc/redis/redis.conf

docker run -d -p 6382:6379 --name redis-s-03 -v /home/atomsk/redis/redis-s-03.conf:/etc/redis/redis.conf -v /home/atomsk/redis/data:/data -v /home/atomsk/redis/sentinel.conf:/etc/redis/sentinel.conf redis redis-server /etc/redis/redis.conf

优点:

  1. 哨兵集群,基于主从复制模式,所有的主从配置优点,它全有
  2. 主从可以切换,故障可以转移,系统的可用性就会更好
  3. 哨兵模式就是主从模式的升级,手动到自动,更加健壮!

缺点:

  1. Redis 不好啊在线扩容的,集群容量一旦到达上限,在线扩容就十分麻烦!
  2. 实现哨兵模式的配置其实是很麻烦的,里面有很多选择!

sentinel.conf详解

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
61
62
63
64
# Example sentinel.conf
# 哨兵sentinel实例运行的端口 默认26379
port 26379
# 哨兵sentinel的工作目录
dir /tmp
# 哨兵sentinel监控的redis主节点的 ip port
# master-name 可以自己命名的主节点名字 只能由字母A-z、数字0-9 、这三个字符".-_"组成。
# quorum 配置多少个sentinel哨兵统一认为master主节点失联 那么这时客观上认为主节点失联了
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379 2
# 当在Redis实例中开启了requirepass foobared 授权密码 这样所有连接Redis实例的客户端都要提供
密码
# 设置哨兵sentinel 连接主从的密码 注意必须为主从设置一样的验证密码
# sentinel auth-pass <master-name> <password>
sentinel auth-pass mymaster MySUPER--secret-0123passw0rd
# 指定多少毫秒之后 主节点没有应答哨兵sentinel 此时 哨兵主观上认为主节点下线 默认30秒
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 这个配置项指定了在发生failover主备切换时最多可以有多少个slave同时对新的master进行 同步,
这个数字越小,完成failover所需的时间就越长,
但是如果这个数字越大,就意味着越 多的slave因为replication而不可用。
可以通过将这个值设为 1 来保证每次只有一个slave 处于不能处理命令请求的状态。
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout 可以用在以下这些方面:
#1. 同一个sentinel对同一个master两次failover之间的间隔时间。
#2. 当一个slave从一个错误的master那里同步数据开始计算时间。直到slave被纠正为向正确的master那
里同步数据时。
#3.当想要取消一个正在进行的failover所需要的时间。
#4.当进行failover时,配置所有slaves指向新的master所需的最大时间。不过,即使过了这个超时,
slaves依然会被正确配置为指向master,但是就不按parallel-syncs所配置的规则来了
# 默认三分钟
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
#配置当某一事件发生时所需要执行的脚本,可以通过脚本来通知管理员,例如当系统运行不正常时发邮件通知
相关人员。
#对于脚本的运行结果有以下规则:
#若脚本执行后返回1,那么该脚本稍后将会被再次执行,重复次数目前默认为10
#若脚本执行后返回2,或者比2更高的一个返回值,脚本将不会重复执行。
#如果脚本在执行过程中由于收到系统中断信号被终止了,则同返回值为1时的行为相同。
#一个脚本的最大执行时间为60s,如果超过这个时间,脚本将会被一个SIGKILL信号终止,之后重新执行。
#通知型脚本:当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等),
将会去调用这个脚本,这时这个脚本应该通过邮件,SMS等方式去通知系统管理员关于系统不正常运行的信
息。调用该脚本时,将传给脚本两个参数,一个是事件的类型,一个是事件的描述。如果sentinel.conf配
置文件中配置了这个脚本路径,那么必须保证这个脚本存在于这个路径,并且是可执行的,否则sentinel无
法正常启动成功。
#通知脚本
# shell编程
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh
# 客户端重新配置主节点参数脚本
# 当一个master由于failover而发生改变时,这个脚本将会被调用,通知相关的客户端关于master地址已
经发生改变的信息。
# 以下参数将会在调用脚本时传给脚本:
# <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# 目前<state>总是“failover”,
# <role>是“leader”或者“observer”中的一个。
# 参数 from-ip, from-port, to-ip, to-port是用来和旧的master和新的master(即旧的slave)通
信的
# 这个脚本应该是通用的,能被多次调用,不是针对性的。
# sentinel client-reconfig-script <master-name> <script-path>
sentinel client-reconfig-script mymaster /var/redis/reconfig.sh # 一般都是由运维来配
置!

缓存穿透和雪崩

缓存穿透

(查不到)

概念

缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中(秒杀!),于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。

解决方案

布隆过滤器

布隆过滤器是一种数据结构,对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力;

缓存空对象

当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;

但是这种方法会存在两个问题:

  1. 如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
  2. 即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。

缓存击穿

(量太大,缓存过期!)

概述

这里需要注意和缓存击穿的区别,缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

当某个key在过期的瞬间,有大量的请求并发访问,这类数据一般是热点数据,由于缓存过期,会同时访问数据库来查询最新数据,并且回写缓存,会导使数据库瞬间压力过大。

解决方案

设置热点数据永不过期

从缓存层面来看,没有设置过期时间,所以不会出现热点 key 过期后产生的问题。

加互斥锁

分布式锁:使用分布式锁,保证对于每个key同时只有一个线程去查询后端服务,其他线程没有获得分布式锁的权限,因此只需要等待即可。这种方式将高并发的压力转移到了分布式锁,因此对分布式锁的考验很大

缓存雪崩

概念

缓存雪崩,是指在某一个时间段,缓存集中过期失效。Redis 宕机!

产生雪崩的原因之一,比如在写本文的时候,马上就要到双十二零点,很快就会迎来一波抢购,这波商品时间比较集中的放入了缓存,假设缓存一个小时。那么到了凌晨一点钟的时候,这批商品的缓存就都过期了。而对这批商品的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会挂掉的情况。

其实集中过期,倒不是非常致命,比较致命的缓存雪崩,是缓存服务器某个节点宕机或断网。因为自然形成的缓存雪崩,一定是在某个时间段集中创建缓存,这个时候,数据库也是可以顶住压力的。无非就是对数据库产生周期性的压力而已。而缓存服务节点的宕机,对数据库服务器造成的压力是不可预知的,很有可能瞬间就把数据库压垮。

解决方案

redis高可用

这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。(异地多活!)

限流降级(在SpringCloud讲解过!)

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

数据预热

数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。