1.场景引入
Web1.0的时代,数据访问量很有限,用一夫当关的高性能的单点服务器可以解决大部分问题。
随着Web2.0的时代的到来,用户访问量大幅度提升,同时产生了大量的用户数据所有的互联网平台都面临了巨大的性能挑战。
解决CPU和内存压力的方案:
解决IO压力:==减少IO操作==
2.NoSQL数据库
2.1NoSQL简介
NoSQL(NoSQL = Not Only SQL ),意即“不仅仅是SQL”,泛指==非关系型的数据库==。
NoSQL 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。
特点:
- 不遵循SQL标准
- 不支持ACID(事务的四大特性:原子性、一致性、隔离性、持久性)
- 远超于SQL性能
2.2NoSQL的适用场景
- ==对数据高并发的读写==
- ==海量数据的读写==
- ==对数据具有高扩展性==
2.3NoSQL不适用的场景
- 需要事务支持
- 基于SQL的结构化查询存储,处理复杂的关系需要即席查询
2.4NoSQL数据库的意义
==NoSQL数据库打破了传统关系型数据库以业务逻辑为依据的存储模式,而是针对不同数据结构的类型、以性能为优先的存储方式。==
3.SQL与NoSQL的区别
4.Redis简介
Redis是一个NoSQL数据库,其数据都在内存中,支持持久化,主要用作备份恢复。除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list、set、hash、zset等。==一般是作为缓存数据库辅助持久化的数据库==
redis诞生小故事:
说起我的诞生,跟关系数据库MySQL还挺有渊源的。
在我还没来到这个世界上的时候,MySQL过的很辛苦,互联网发展的越来越快,它容纳的数据也越来越多,用户请求也随之暴涨,而每一个用户请求都变成了对它的一个又一个读写操作,MySQL是苦不堪言。尤其是到“双11”、“618“这种全民购物狂欢的日子,都是MySQL受苦受难的日子。
据后来MySQL告诉我说,其实有一大半的用户请求都是读操作,而且经常都是重复查询一个东西,浪费它很多时间去进行磁盘I/O。
后来有人就琢磨,是不是可以学学CPU,给数据库也加一个缓存呢?于是我就诞生了!
出生不久,我就和MySQL成为了好朋友,我们俩常常携手出现在后端服务器中。
应用程序们从MySQL查询到的数据,在我这里登记一下,后面再需要用到的时候,就先找我要,我这里没有再找MySQL要。
5.Redis的应用场景
5.1配合关系型数据库做高速缓存
高频次,热门访问的数据,降低数据库IO
分布式架构,做session共享
5.2多样的数据结构存储持久化数据
6.Redis的安装、启动服务、关闭服务
6.1Redis的安装
6.1.1虚拟机环境搭建
Redis官网
下载可以发现是Linux版本
原因是因为Redis官方并不推荐在Windows系统下使用Redis。不用考虑在windows环境下对Redis的支持
一:创建Redis环境测试虚拟机
二:用Xshell连接虚拟机,首先在虚拟机中安装C语言的编译环境gcc
查看gcc版本信息
6.1.2解压安装包
解压到/opt目录下
注意:opt目录对于所有用户没有写权限,所以在此目录下传输文件时需要更改权限
==r w x 分别表示读权限、写权限、执行权限 用数字表示为 4 2 1==
解压压缩文件:
进入解压好的文件中进行编译安装:
编译
安装:
redis的默认安装目录为:/usr/local/bin/
==该目录下有redis服务端和redis客户端==
6.2后台启动Redis服务
切换到root用户下进入到redis文件目录下
复制redis.conf文件到etc目录下
==一:修改redis.conf(128行)文件将里面的daemonize no 改成 yes,让服务在后台启动==
采用vim编辑器中的搜索模式进行搜索(正斜线进行搜索模式)
==二:修改监听地址:默认是127.0.0.1,只有在本机才能访问,现在测试环境修改为0.0.0.0,即任何ip都能够访问,生产环境下不能修改==
==三:设置redis密码==
命令:config set requirepass 密码
启动redis服务
查看redis服务
6.3.redis客户端连接到redis服务
可以看到已经连接到了redis服务,redis服务在虚拟机的6379端口运行
6.4关闭Redis服务
一:单实例关闭
二:客户端连接后进行关闭
7.单线程+IO多路复用机制
「为了让单线的服务端应用同时处理多个客户端的事件,Redis 采用了 IO 多路复用机制。」
==多路【指的是多个网络连接客户端】==
==复用【指的是复用同一个线程】==
多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)
8.Redis的常用操作(基于key)
命令 |
说明 |
keys * |
查看当前库所有key |
exists key |
判断某个key是否存在 |
type key |
查看你的key是什么类型 |
del key |
删除指定的key数据 |
unlink key |
根据value选择非阻塞删除(提示已经删除,但其实并没有删除,在后续会删除) |
expire key 数字 |
数字的单位为正整数,表示秒,为给定的key设置过期时间 |
ttl key |
查看还有多少秒过期,-1表示永不过期,-2表示已过期 |
select [0-15] |
切换数据库,一共有16个 |
dbsize |
查看当前数据库的key的数量 |
flushdb |
清空当前库 |
flushall |
通杀全部库 |
可以采用help命令查看相关命令的使用方式:
==一:查看当前库中的所有key==
语法:
==二:判断某个key是否存在==
语法:
==三:查看key的类型==
语法:
==四:删除指定的key数据==
语法:
==五:设置key的过期时间,查看还有多少秒过期==
语法:
设置过期时间
查看还有多少秒过期
==六:切换不同的数据库==
语法:
==七:查看当前数据库的key的数量==
语法:
9.key的层级结构
Redis的key允许有多个单词形成层级结构,多个单词之间用’:’隔开,格式如下:
这个格式并非固定,也可以根据自己的需求来删除或添加词条。
如果Value是一个Java对象,例如一个User对象,则可以将对象序列化为JSON字符串后存储:
10.常用数据类型
10.1String字符串
10.1.1String说明
String是Redis最基本的类型,一个key对应一个value。
String类型是==二进制安全的==。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。
String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M
10.1.2String的常用命令
|
|
set key value |
添加键值对 |
get key |
根据key获取到value |
append key value |
将给定的value追加到key原值的末尾 |
strlen key |
获得值的长度 |
setnx key value |
只有在 key 不存在时 设置 key 的值 |
incr/decr key |
让数字类型加1/减1 |
incrby/decrby key step |
指定步长,让数字类型加步长/减步长 |
mset key1 value1 key2 value2 … |
同时设置一个或多个 key-value对 |
mget key1 key2 … |
同时获取一个或多个 value |
msetnx key1 value1 key2 value2 … |
同时设置一个或多个 key-value对(具有原子性,如果有一个已经存在则全部都会失败) |
getrange key start end |
获取从start开始,到end结束的字符串片段 |
setrange key start value |
用 value 参数覆写给定 key 所储存的字符串值,从偏移量 start开始。 |
setex key 过期时间 value |
设置值的同时设置过期时间 |
getset key value |
获取旧值的同时设置新值 |
==一:添加键值对==
语法:
==二:根据键来获取值==
语法:
==三:将给定的value追加到key原值的末尾==
语法:
==四:获得值的长度==
语法:
==五:只有在 key 不存在时 设置 key 的值==
语法:
==六:让数字类型加1/减1==
语法:
==七:让数字类型按照步长加/减==
语法:
其中数字类型的增加或减小是原子操作,即==原子性==
原子性就是不会被进程调度所影响的操作
==八:同时设置一个或多个 key-value对==
语法:
==九:同时获取一个或多个 value==
语法:
==十:获取到字符串片段==
语法:
==十一:替换掉指定的字符串片段==
语法:
==十二:设置值的同时设置过期时间==
语法:
==十三:获取旧值的同时设置新值==
语法:
10.1.3String的数据结构
String的数据结构为==简单动态字符串==(Sample Dynamic Sting)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.
如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。
10.2List列表
10.2.1List列表说明
List的数据存储形式为:==单键多值==
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。
它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。
10.2.2List列表的常用命令
命令 |
说明 |
lpush/rpush key value1 value2 value3 …. |
从左边/右边插入一个或多个值 |
lpop/rpop key |
从左边/右边移除一个值 |
rpoplpush key1 key2 |
从key1列表右边移出一个值,插到key2列表左边 |
lrange key start end |
按照start开始,end结束的索引下标获得元素(从左到右) |
lrange key 0 -1 |
0左边第一个,-1右边第一个,(0-1表示获取所有) |
lindex key index |
按照索引下标获得元素(从左到右) |
llen key |
获得列表长度 |
linsert key before/after value newvalue |
在value的前面/后面插入newvalue |
lrem key n value |
从左边删除n个value(从左到右) |
lset key index value |
将列表key下标为index的值替换成value |
==一:从左边/右边插入一个或多个值==
语法:
==二:从左边/右边移除一个值==
语法:
==三:从key1列表右边移出一个值,插到key2列表左边==
语法:
==四:按照start开始,end结束的索引下标获得元素(从左到右)==
语法:
==五:按照索引下标获得元素==
语法:
==六:获取到列表长度==
语法:
==七:在value的前面/后面插入newvalue==
语法:
==八:从左边删除n个值==
语法:
==九:将列表key下标为index的值替换成value==
语法:
10.2.3List列表的数据结构
List的数据结构为==快速链表quickList==。
首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。
当数据量比较多的时候才会改成quicklist。
因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。
Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。
10.3Set集合
10.3.1Set集合说明
Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set中的元素是可以自动排重,且无序,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的**复杂度都是O(1)**。
一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变
10.3.2Set集合的常用命令
命令 |
说明 |
sadd key value1 value2 |
将一个或多个 元素加入到集合 key 中,已经存在的元素将被忽略 |
smembers key |
取出该集合的所有值 |
sismember key value |
判断集合key是否为含有该value值,有1,没有0 |
scard key |
返回该集合的元素个数 |
srem key value1 value2 |
删除集合中的一个或多个元素 |
spop key |
随机移除集合中的某个元素 |
srandmember key n |
随机从该集合中取出n个值。不会从集合中删除 |
smove key1 key2 value |
把集合中一个值从一个集合移动到另一个集合 |
sinter key1 key2 |
返回两个集合的交集元素 |
sunion key1 key2 |
返回两个集合的并集元素 |
sdiff key1 key2 |
返回两个集合的差集元素(key1中的,不包含key2中的) |
==一:将一个元素或者多个元素添加到集合中==
语法:
==二:取出该集合的所有值==
语法:
==三:判断集合key是否为含有该value值,有1,没有0==
语法:
==四:返回该集合的元素个数==
语法:
==五:删除集合中的一个或多个元素==
语法:
==六:随机移除集合中的某个元素==
语法:
==七:随机从该集合中取出n个值。不会从集合中删除==
语法:
==八:把集合中一个值从一个集合移动到另一个集合==
语法:
==九:返回两个集合的交集元素==
语法:
==十:返回两个集合的并集元素==
语法:
==十一:返回两个集合的差集元素==
语法:
10.3.3Set集合的数据结构
Set数据结构是dict字典,字典是用哈希表实现的。
Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。
10.4Hash哈希
10.4.1Hash哈希说明
Redis hash 是一个键值对集合。
==Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。==
类似Java里面的Map<String,Object>
用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储
主要有以下2种存储方式:
方式1:
每次修改用户的某个属性需要先反序列化改好后再序列化回去。开销较大。
方式2:
用户ID数据冗余
采用Hash的方式进行实现:
通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题
10.4.2Hash哈希的常用命令
命令 |
说明 |
hset |
给集合中的 键赋值 |
hget |
从集合取出 value |
hmset … |
批量设置hash的值 |
hexists |
查看哈希表 key 中,给定域 field 是否存在 |
hkeys |
列出该hash集合的所有field |
hvals |
列出该hash集合的所有value |
hincrby |
为哈希表 key 中的域 field 的值加上增量 1 -1 |
hsetnx |
将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 |
==一:给集合中的 键赋值==
语法:
==二:从集合取出 value==
语法:
==三:批量设置hash的值==
语法:
==四:查看哈希表 key 中,给定域 field 是否存在==
语法:
==五: 列出该hash集合的所有field==
语法:
==六:列出该hash集合的所有value==
语法:
==七:为哈希表 key 中的域 field 的值加上增量increment==
语法:
==八:将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在==
语法:
10.4.3Hash哈希的数据结构
Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。
10.5有序集合Zset
10.5.1有序集合Zset说明
Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。
==不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。==
因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。
访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。
10.5.2有序集合Zset的常用命令
命令 |
说明 |
zadd … |
将一个或多个 member 元素及其 score 值加入到有序集 key 当中 |
zrange [WITHSCORES] |
返回有序集 key 中,下标在之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集 |
zrangebyscore key min max [withscores] [limit offset count] |
返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列 |
zrevrangebyscore key max min [withscores] [limit offset count] |
取得从大到小从max开始到min结束的数据 |
zincrby |
为元素的score加上增量 |
zrem |
删除该集合下,指定值的元素 |
zcount |
统计该集合,分数区间内的元素个数 |
zrank |
返回该值在集合中的排名,从0开始 |
==一:将一个或多个 member 元素及其 score 值加入到有序集 key 当中==
语法:
==二:返回有序集 key 中,下标在之间的元素,带WITHSCORES,可以让分数一起和值返回到结果集==
语法:
==三:返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列==
语法:
==四:取得从大到小从max开始到min结束的数据==
语法:
==五: 为元素的score加上增量==
语法:
==六:删除该集合下,指定值的元素==
语法:
==七:统计该集合,分数区间内的元素个数==
语法:
==八:返回该值在集合中的排名,从0开始==
语法:
10.5.3有序集合Zset的数据结构
SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。
zset底层使用了两个数据结构
(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。
(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。
什么是跳跃表
有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。
2、实例
对比有序链表和跳跃表,从链表中查询出51
(1) 有序链表
要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。
(2) 跳跃表
从第2层开始,1节点比51节点小,向后比较。21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层
在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下
在第0层,51节点为要查找的节点,节点被找到,共查找4次。
从此可以看出跳跃表比有序链表效率要高
11.Redis的java客户端Jedis
11.1Jedis简介
以 Redis 命令作为方法名称,学习成本低,简单实用。==但是 Jedis 实例是线程不安全的,多线程环境下需要基于连接池来使用==
11.2Jedis快速入门
Jedis的官网地址: https://github.com/redis/jedis
一:在虚拟机中关闭防火墙
二:创建maven项目,引入Jedis的相关依赖
三:创建测试类
控制台打印情况
查看redis可视化工具中的情况
11.3Jedis连接池
Jedis本身是线程不安全的,并且频繁的创建和销毁连接会有性能损耗,因此我们推荐大家使用Jedis连接池代替Jedis的直连方式
一:创建Jedis连接池配置类
二:测试类测试Jedis数据库连接池
控制台打印:
12.Redis的java客户端SpringDataRedis
12.1SpringDataRedis简介
==SpringData是Spring中数据操作的模块,包含对各种数据库的集成==,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis
12.2SpringDataRedis的特点
- 提供了对不同Redis客户端的整合(Jedis和Lettuce)
- 提供了RedisTemplate统一API来操作Redis
- 支持Redis的发布订阅模型
- 支持Redis哨兵和Redis集群
- 支持Lettuce的响应式编程
- 支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化
- 支持基于Redis的JDKCollection实现
==SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中==
12.3SpringBoot集成SpringDataRedis
12.3.1引入相应依赖
使用项目初始化工具创建SpringBoot项目
引入Spring对Redis的依赖:
12.3.2在配置文件中添加配置信息
12.3.3测试RedisTemplate
Spring对Redis的封装的各种API
每种API对应了redis所有操作数据的方法
12.3.4RedisTemplate的序列化方式
前面已经看到Spring对Redis的集成的各种Api的各种方法的参数类型为Object类型,==而Redis底层对Object对象的处理方式就是使用jdk的序列化工具ObjectOutputStream(对象操作流)进行序列化的。==
进入RedisTemplate
采用ObjectOutputStream序列化得到的结果为:
12.3.5更改RedisTemplate的序列化方式
一:导入相应依赖
导入json解析库的坐标(JSON解析库有:jackson(SpringMVC),fastjson(阿里),gson(Google))
二:创建配置类,更改序列化规则
三:再次进行测试,查看序列化情况
四:创建javaBean,测试对象类型
==可以看到当存入缓存的时候对象类型序列化为了JSON类型,读取的时候由JSON类型反序列化为了对象类型。==
12.3.6StringRedisTemplate
尽管JSON的序列化方式可以满足我们的需求,但依然存在一些问题,如图:
为了在反序列化时知道对象的类型,JSON序列化器会将类的class类型写入json结果中,存入Redis,会带来额外的内存开销。
==为了节省内存空间,我们并不会使用JSON序列化器来处理value,而是统一使用String序列化器,要求只能存储String类型的key和value。当需要存储Java对象时,手动完成对象的序列化和反序列化。==
==Spring默认提供了一个StringRedisTemplate类,它的key和value的序列化方式默认就是String方式。省去了我们自定义RedisTemplate的过程==
12.3.7StringRedisTemplate操作字符串类型
ObjectMapper类是Jackson的主要类,它可以帮助我们快速的进行各个类型和Json类型的相互转换。它使用JsonParser和JsonGenerator的实例实现JSON实际的读/写。
测试
JSON字符串
12.3.8StringRedisTemplate操作Hash类型
13.Redis缓存
13.1什么是缓存
缓存就是数据交换的缓冲区,是存贮数据的临时地方,==一般读写性能较高==。
13.2缓存的作用和成本
13.3缓存业务流程
==当不使用缓存的时候,当客户端向服务器端发送请求,服务器端每次都会调用DAO层查询数据库,数据库中的数据是写在磁盘当中的,读写效率很慢。==
==redis缓存的作用就是充当中间件,当客户端前服务器端请求数据的时候,首先会到缓存中查询数据,如果请求命中,redis缓存就返回数据,若请求未命中,则在关系型数据库进行查询,将查询到的数据写入缓存并返回给客户端。==
13.4缓存的更新策略
13.4.1缓存更新策略的三种方式
业务场景:
- ==低一致性需求:使用内存淘汰机制。例如不需要经常更新的数据—店铺类型的查询缓存==。
- ==高一致性需求:主动更新,并以超时剔除作为最后方案。例如店铺详情查询的缓存==。
13.4.2主动更新策略实现更新的三种方式
操作缓存和数据库的时候有三个问题需要考虑:
删除缓存还是更新缓存
更新缓存:每次更新数据库都更新缓存,无效写操作较多 ✘
==删除缓存:更新数据库时让缓存失效,查询时再更新缓存== ✔
如何保证缓存和数据库之间的操作同时成功同时失败
单体系统:将缓存与数据库操作放在一个事务里
分布式系统:利用TCC等分布式事务方案
先操作缓存还是先操作数据库
先删除缓存,再操作数据库
==先操作数据库,再删除缓存==
13.4.3缓存更新的最佳方案
13.5缓存穿透
13.5.1缓存穿透的定义
==缓存穿透==是客户端请求的数据既不在缓存当中,也不在数据库中。出于容错的考虑,如果从底层数据库查询不到数据,则不写入缓存。这就导致每次请求都会到底层数据库进行查询,缓存也失去了意义。当高并发或有人利用不存在的Key频繁攻击时,数据库的压力骤增,甚至崩溃,这就是缓存穿透问题。
13.5.2缓存穿透的解决方案
- ==缓存空值==
就是当缓存中和数据库中都不存在客户端请求的数据时,就设置一个空值作为缓存,并为缓存设置过期时间(一般很短),当客户端再次发送同样的请求时就会命中缓存,不会请求数据库,从而减小数据库压力。
- 布隆过滤
13.6缓存雪崩
13.6.1缓存雪崩的定义
==缓存雪崩==是指在一段时间内大量的缓存key同时失效或者Redis服务故障,使大量请求到达数据库,从而导致数据库崩溃。
13.6.2缓存雪崩的解决方案
13.7缓存击穿
13.7.1缓存击穿的定义
缓存击穿问题也叫热点Key问题,就是一个被==高并发访问==并且==缓存重建业务较复杂==的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
13.7.2缓存击穿的解决方案
一:互斥锁
==当同个业务不同线程访问redis未命中时,先获取一把互斥锁,然后进行数据库操作,此时另外一个线程未命中时,拿不到锁,等待一段时间后重新查询缓存,此时之前的线程已经重新把数据加载到redis之中了,线程二就直接缓存命中。这样就不会使得大量访问进入数据库==
互斥锁的实现方式:
使用setnx实现互斥锁
setnx要求只有当key不存在的时候才能设置key,所以可以采用setnx模拟互斥锁,当一个进程未命中缓存,要查询数据库的时候就添加setnx,并设置过期时间(为了防止忘记释放锁,出现死锁问题)。当其它线程请求时就会等待,等互斥锁时间过期就能获取到缓存中的数据。
二:逻辑过期
给缓存设置一个逻辑过期时间,什么意思呢?缓存本来在redis之中,正常情况下除了主动更新它是不会变的,为了防止缓存击穿,我们以一种预判或者说保守的方式,主动设置一个过期时间,当然这个时间过期了,缓存里面的数据是不会消失的,但是我们只需要根据这个假设的过期时间。来进行经常的动态的缓存数据的更新。可以对缓存击穿起一定的预防作用。
三:互斥锁和逻辑过期的优缺点比较
13.8缓存工具封装
13.8.1解决缓存穿透方法封装
步骤1:==将任意Java对象序列化为json并存储在string类型的key中,并且可以设置TTL过期时间(为了在解决缓存穿透时向缓存中添加有效缓存的数据的方法)==
步骤2:==根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题==
封装的方法:
调用封装的方法:font>
测试:查询缓存和数据库中都不存在的数据:成功向缓存中存入空值
13.8.2解决缓存击穿方法封装
步骤1:==将任意Java对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题==
步骤2:==根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题==
调用封装的方法:
测试,修改数据库数据,采用JMeter模拟高并发场景
控制台打印一条数据库查询信息
14.分布式锁
14.1分布式锁的概念
为了解决集群部署模式下多线程并发安全问题,引入分布式锁的概念。
分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁
多个服务器使用同一个锁监视器。
14.2分布式锁的实现方式的比较
分布式锁的核心是实现多进程之间互斥,而满足这一点的方式有很多,常见的有三种:
在15.7.7章节使用悲观锁(synchronized)来实现一人一单功能,但是在集群部署模式下,因为synchronized锁只能局限于当前服务器的线程,所以在多个服务器之间不能实现锁共享。
==因为由于Redis缓存中的数据在多个服务器之间是共享的,所以可以采用Redis的setnx来实现共享锁监视器==
14.3Redis分布式锁实现思路
Redis实现分布式锁时需要实现两个基本方法:
获取锁
- 互斥:确保只有一个线程获取锁
- 非阻塞:尝试一次,成功返回True,失败返回false
释放锁
手动释放
超时释放:获取锁时添加一个过期时间
setnx原子操作:设置过期时间并设置其原子性
14.4Redis实现分布式锁初级版本
需求:定义一个类,实现下面接口,利用Redis实现分布式锁功能。
测试单元见 17.7.9
14.5Redis分布式锁误删问题
场景描述:线程1首先获取到分布式锁,但是线程1执行过程中出现业务阻塞,导致分布式锁没有被主动释放,超时之后才被释放。释放后,线程2开始获取到分布式锁,并开始执行业务,在此期间,线程1的业务完成,并释放分布式锁(释放的锁是线程2的锁)。分布式锁被释放,其他线程就能获取到分布式锁。
解决分布式锁误删的方案:==在释放锁之前判断Redis缓存当中线程号是否和当前线程的线程号相同,相同就是放,不同就不释放。==
14.6解决分布式锁的误删问题
需求:修改之前的分布式锁实现,满足:
1.在获取锁时存入线程标示(可以用UUID表示)
2.在释放锁时先获取锁中的线程标示,判断是否与当前线程标示一致
之前采用的锁标识方案为采用线程id(线程id是自增的),但是在集群模式下,多个JVM有可能会产生相同的线程id,所以要加上UUID。
测试单元见17.7.10
14.7分布式锁的原子性问题
场景描述:==线程1获取到分布式锁,当线程完成业务查询分布式锁标识和自己的相符后,准备释放锁时,线程阻塞 ,随后超时释放。另一个线程开始获得分布式锁,执行自己的业务。但是线程1这事从阻塞状态转为就绪状态,因为已经判断过了分布式锁标识,随后就直接释放线程2的分布式锁。==
使用Lua脚本实现“判断分布式锁标识”和“释放锁”两个业务的原子性
Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。Lua是一种编程语言,它的基本语法大家可以参考网站:https://www.runoob.com/lua/lua-tutorial.html
14.8Lua脚本语言
Lua是一种轻量小巧的脚本语言,可以很方便的和其他程序进行集成和扩展(C#,Java…..),==其设计目的是为了嵌入应用程序中,为应用程序提供灵活的扩展和定制功能。==
在使用redis
的过程中,发现有些时候需要原子性
去操作redis命令,而redis的lua
脚本正好可以实现这一功能。比如: 扣减库存操作、限流操作等等。
Redis提供的Lua脚本调用函数语法如下:
写好脚本以后,需要用Redis命令来调用脚本,调用脚本的常见命令如下:
例如,我们要执行==redis.call(‘set’, ‘name’, ‘jack’)==这个脚本,语法如下:
如果脚本中的key、value不想写死,可以作为参数传递。key类型参数会放入KEYS数组,其它参数会放入ARGV数组,在脚本中可以从KEYS和ARGV数组获取这些参数:
在idea中安装Lua脚本的相关插件
使用Lua脚本实现分布式锁的原执行操作,测试单元见15.7.11
15.Redisson框架
15.2Redisson引入
基于setnx实现的分布式锁会出现以下问题:
15.2Redisson框架简介
之前用的Redis,都是用的原生的RedisTempale或者是StringRedisTemplate,各种API非常的难易记忆
,每次用的时候还得去网上查询API文档。
Redisson是一个在Redis的基础上实现的Java驻内存数据网格
。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中就包含了各种分布式锁的实现。**Redisson是Java的Redis客户端之一,提供了一些API方便操作Redis。**
15.3Redisson配置
一:引入Redisson依赖
二:创建配置对象
三:在使用的类中注入RedissonClient,获取锁,释放锁
测试单元在15.7.12
15.5读写锁
==读写锁能够保证每次读取到的数据都是新数据。==
修改数据期间:写锁是一个互斥锁,读锁是一个共享锁
模式 |
说明 |
读+读 |
相当于无锁,并发读 |
写+读 |
等待写锁释放,读锁再执行 |
读+写 |
等待读锁释放,写锁再执行 |
写+写 |
阻塞方式 |
写锁没有释放,读锁就必须等待
15.6信号量
每当释放信号量的时候,信号量字段就会加1,获取到信号量的时候信号量字段就会减1。
同时信号量也可以做分布式限流,使用tryAcquire()
来进行判断信号量是否获取成功。
15.7闭锁
15.8Redisson可重入锁原理
==一个线程连续两次获取锁就是锁的重入。==
以下方式是采用setnx自定义锁的方式,当一个线程获取到锁后,调用另一个方法再次获取到锁,但是由于是因为基于setnx实现的,再次获取所就会失败
。
而Redisson实现重入锁的原理就是判断获取分布式锁的线程是否是当前线程,并且记录线程获取锁的次数。当当前线程再次获取分布式锁的时候获取锁的次数就会增加,释放锁后再释放。
Redisson的分布式锁的创建过程:
查看RedissionClient接口的实现类,实现类是Redisson
getLock方法中调用了RedissonLock类中的构造器
找到创建锁的lua脚本
测试:
执行方法1,获取锁成功,在缓存中存入锁的标识和获取重入次数,当前次数为1
执行到方法2,获取锁成功,重入次数变为2
方法2执行后释放锁,重入次数变为1,再执行方法1,重入次数变为0
控制台业务流程:
15.9Redisson锁重试和WatchDog机制
Redisson分布式锁原理:
•可重入:==利用hash结构记录线程id和重入次数==
•可重试:==利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制==
•超时续约:==利用watchDog,每隔一段时间(时间间隔为lockWatchdogTimeout
/ 3),重置超时时间。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout
来另行指定。==
15.10Redisson分布式锁主从一致性问题
主从一致性就是一个主节点连有两个从节点,主节点和从节点之间存在主从同步
,当一个线程获取锁的时候,主节点存入分布式锁标识。但是在未完成主从同步的时候主节点发生宕机。发生宕机后哨兵会在剩下的从节点中选出一个作为主节点,但是此主节点中并没有分布式锁标识。这就是导致主从一致性的问题。
解决主从一致性问题的方法:
只要有任意节点存活,其他线程就获取不到锁,不会出现锁失效问题。
15.11锁的粒度
如果是粗粒度锁会出现以下问题:
- 每次锁住所有的资源,导致事务碰撞率提高,影响效率
- 一旦发生异常影响锁的释放,会产生死锁
==我们可以为集合中的每个资源提供一个锁,这样可以避免每次的操作都会锁住所有的资源,其次我们为每一个锁设置一个超时时间,避免死锁情况的出现。
16.消息队列
16.2消息队列的概念
消息队列(Message Queue)一般简称为MQ。==是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成,是在消息的传输过程中保存消息的容器。消息队列本质上是一个队列,而队列中存放的是一个个消息。==
最简单的消息队列模型包括3个角色:
消息队列让生产者和消费者之间解耦合
可以采用市面上提供的消息队列,如Kafka、RabbitMQ、RocketMQ等等,但是Redis也提供了三种不同的方式来实现消息队列:
list结构:基于List结构模拟消息队列
PubSub:基本的点对点消息模型
Stream:比较完善的消息队列模型
16.3消息队列-list结构
队列底层的实现是双向链表,是入口和出口不在一边,因此我们可以利用:LPUSH 结合 RPOP、或者 RPUSH 结合 LPOP来实现。
==不过要注意的是,当队列中没有消息时RPOP或LPOP操作会返回null,并不像JVM的阻塞队列那样会阻塞并等待消息。因此这里应该使用BRPOP或者BLPOP来实现阻塞效果。==
Redis brpop命令移出并获取列表最右侧的元素
。==如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。==
blpop是相反方向。
基于List的消息队列有哪些优缺点?
优点:
利用Redis存储,不受限于JVM内存上限
基于Redis的持久化机制,数据安全性有保证
可以满足消息有序性
缺点:
16.4消息队列-PubSub
PubSub(发布订阅)是Redis2.0版本引入的消息传递模型。顾名思义,消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息。
SUBSCRIBE channel [channel] :==订阅一个或多个频道==
PUBLISH channel msg :==向一个频道发布消息==
PSUBSCRIBE pattern[pattern] :==订阅与pattern格式匹配的所有频道==
基于PubSub的消息队列有哪些优缺点?
优点:
缺点:
不支持数据持久化
无法避免消息丢失
消息堆积有上限,超出时数据丢失
16.5消息队列-Stream
Stream 是 Redis 5.0 引入的一种新数据类型,可以实现一个功能非常完善的消息队列。
发送消息的命令:
例如:
读取消息的方式1:XREAD
XREAD阻塞方式,阻塞读取最新的消息:
在业务开发中,我们可以循环的调用XREAD阻塞方式来查询最新消息,从而实现持续监听队列的效果,伪代码如下
阻塞读取消息队列中的最新的一条消息,最多等待2s
STREAM类型消息队列的XREAD的优缺点:
优点:
消息可回溯
一个消息可以被多个消费者读取
可以阻塞读取
缺点:
17.Redis企业实战项目
17.1项目主要业务功能
17.2项目架构
==该项目是一个前后端分离项目,前端部署在Nginx动态代理服务器上。后端部署在Tomcat上面。==
客户端向Nginx发送请求获取到静态资源,页面通过Nginx向服务端发送请求查询数据,数据可能来自于MySQL集群,也有可能来自Redis集群。
17.3项目初始化
一:创建数据库,导入SQL文件
其中的表有:
二:导入后端项目
在Gitee中获取到远程仓库的地址
idea克隆项目
初始为master分支,切换分支为init分支
三:pom文件内容
Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅;
提供了Java基础工具类,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类,同时提供以下组件
如随机工具:
四:SpringBoot配置文件
五:启动项目,进行测试
六:导入前端项目
将已经准备好的Nginx文件夹放到目录下:
该文件夹内已经准备好了前端项目
七:启动前端项目
在nginx所在目录下打开一个CMD窗口,输入命令:
打开浏览器,打开设备工具栏,输入前端运行端口8080
17.4基于Session短信登录
17.4.1发送短信验证码
找到对应的控制器方法,调用Service层接口,传递参数phone和session
在Service层实现类中编写相应的业务流程
业务流程实现过程:==用户输入手机号点击发送验证码后,首先检验手机号是否合法,如果合法就随机生成验证码,并将验证码存入session==
运行项目,再次测试。
返回成功信息
控制台打印日志信息
17.4.2验证码登录和注册
业务流程实现过程:==当用户点击登录按钮后,获取到用户在前端提交的表格信息中的手机号和验证码,首先验证手机号是否合法,再验证输入的验证码是否和存在session中的验证码是否一致。验证完成后根据手机号查询用户状态,如果用户存在则登录,若不存在就首先创建用户。最后将用户信息存入session。==
17.4.3拦截器实现登陆验证
业务流程实现过程:==当用户执行登录的时候,已经在sessino中存入了用户的相关信息。用户登录状态的校验在很多地方都需要执行,这样就会比较麻烦,所以配置拦截器来做用户登陆验证。所以在前端向Controller层发送请求的时候,都会先由拦截器判断用户的登录状态,如果用户信息已经在session中就证明用户已经登录,否则返回401状态码。==
配置拦截器规则
测试登录成功,显示用户信息。
17.4.4隐藏用户敏感信息
从响应信息中可以看出存在用户的敏感信息,如手机号,密码等等。这是因为向session中存入对象时是将用户的所有属性都存了进去。
下面进行隐藏用户的敏感信息,即更改存储到session中的对象属性
UserDTO只有以下三个属性,刚好满足我们的需求。
==采用hutool的对象拷贝方法实现根据数据源对象获取到新对象,即存入session的对象类型为UserDTO==
17.4.5集群的session的共享问题
session共享问题:多台Tomcat并不共享session存储空间,当请求切换到不同tomcat服务时导致数据丢失的问题。
session的替换方案应该满足以下特点:
17.5基于Redis实现短信登录
17.5.1为什么要使用redis代替session
==前面已经提到,当项目部署在一个服务器当中的时候,session可以实现共享。但是负载均衡操作时将一个项目部署在多台服务器上,那么服务器之间的session共享问题就很显而易见了,如果多台服务器之间相互拷贝必将造成数据冗余和存储压力。所以采用Redis做持久化缓存就很有必要,它可以实现存储数据可以在多台服务器之间进行共享。==
17.5.1Redis代替session的业务流程
redis替代短信验证码的业务流程
redis替代校验登录状态的业务流程
17.5.2 Redis实现短信登录
==在拦截器中设置redis存储中登录对象的过期时间,因为拦截器可以检测用户的登录状态,只要前端向后端发送请求拦截器就会判断用户的登录状态,如果用户处在登录状态,就重设redis缓存的过期时间==
但是如果只有一个拦截器,当用户访问公共资源的时候并不会触发拦截器,这就导致当用户一直停留在公共资源的时候redis缓存中的数据更新时间并不会发生变化,直到缓存数据失效,用户登录状态变为未登录。
==为解决以上问题,采用两个拦截器优化,第一个拦截器拦截所有路径,负责将用户信息存入redis缓存,存入线程并刷新token有效期。第二个只判断线程中是否有用户信息,没有就拦截。==
存储用户信息,更新token的拦截器
登录拦截器,用于专门验证用户登录状态
拦截器的配置,设置拦截器等级,让刷新token、存储用户信息的拦截器首先执行,登录验证拦截器后执行。
查看存在redis中的验证码
请求头中的token设置方式:
当调用login接口后,service层会返回toekn字符串,再经由控制层返回给前端,前端将token信息保存到sessionStorage会话存储中。
在前端设置拦截器,每次发送请求时都会从sessionStorage获取到token,并在请求头中添加token信息
==有了请求头中的token, 就能够在后端拦截器中获取到token,进而通过token去获取到存储在redis中的用户信息,刷新用户信息存储时间==
重新发送请求后token过期时间刷新
17.6商户查询缓存
17.6.1添加商户缓存
添加商户缓存业务流程图
查看缓存
17.6.2采用缓存更新策略优化商户缓存
修改ShopController中的业务逻辑,满足下面的需求:
==①根据id查询店铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间==
==②根据id修改店铺时,先修改数据库,再删除缓存==
在添加缓存的时候设置过期时间
找到shopController,将之前在控制层直接更改数据库的操作放到service层
在service层首先更改数据库数据,然后删除缓存
采用postman进行接口测试:
数据库内容修改成功
再次请求,缓存重建
17.6.3解决查询商户不存在时出现的缓存穿透问题
测试:
==一:redis中没有真实商铺的缓存数据,也没有空值==
postman进行测试
控制台打印查询数据库信息:
将数据存入缓存
在缓存未过期的时间内再次发送请求会获取到缓存中的数据,而不会获取数据库中的数据,控制台没有sql信息
==二:测试数据库中和缓存中都不存在的情况==
打印信息为商铺不存在
17.6.4基于互斥锁的方式解决缓存击穿问题
需求:==修改根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题==
一:采用性能测试工具JMeter进行高并发请求测试,以用来检验基于互斥锁实现的解决缓存击穿的问题是否能够得到解决。
二:设置线程组规则,测试1000条线程在5秒内请求完成
三:设置HTTP请求
四:开始测试
所有请求均已得到响应
可以看到控制台只打印一条数据库查询信息
redis中已经存入缓存
17.6.5基于逻辑过期的方式解决缓存击穿问题
需求:修改根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题
一:设置工具类,添加过期时间属性和对象属性,这个对象属性就是返回给前端的数据
==二:设置热点数据,并采用单元测试的方式进行热点数据写入缓存==
二:由于之前向缓存中提前加入热点数据的时候设置的逻辑过期时间为10s,所以逻辑时间已经过期。
测试:
一:在高并发情况下,会不会出现多个线程重建缓存的情况(并发的安全问题)
二:数据一致性的问题(在缓存重建之前查询的是旧数据)
缓存中的热点数据
修改数据库
==在JMeter模拟高并发场景,查看逻辑过期后重新构建缓存查询数据库时,数据前后是否一致。==
可以看到数据前后不一致。
热点数据已经更改
17.7优惠券秒杀
17.7.1全局ID生成器
当用户购买商品时,就会生成订单并保存到tb_voucher_order这张表中,而订单表如果使用数据库自增ID就存在一些问题
这样的情况下可以考虑使用全局ID生成器
全局唯一ID生成策略:
UUID
==Redis自增==
snowflake算法
数据库自增
Redis自增ID策略:
==每天一个key,方便统计订单量==
==ID构造是 时间戳 + 计数器==
==全局ID生成器,是一种在分布式系统下用来生成全局唯一ID的工具,一般要满足下列特性:==
为了增加ID的安全性,我们可以不直接使用Redis自增的数值,而是拼接一些其它信息:
ID的组成部分:
- 符号位:1bit,永远为0,表示为正数
- 时间戳:31bit,以秒为单位,可以使用69年
- 序列号:32bit,秒内的计数器,每秒支持2*32次方个不同的ID
17.7.2添加优惠券
每个店铺都可以发布优惠券,分为平价券和特价券。平价券可以任意购买,而特价券需要秒杀抢购:
在VoucherController中提供了一个接口,可以添加秒杀优惠券:
Controller层:
Service层:
进行接口测试:
JSON数据
优惠券添加成功
17.7.3实现秒杀下单
用户可以在店铺页面中抢购这些优惠券
下单时需要判断两点:
17.7.4库存超卖问题(多线程并发问题)分析
就是在高并发的场景下,可能会有多个线程同时进行查询,当商品数量仅剩1个时,多个线程同时查询,都判断为1,都会进行下单。
使用ApiFox测试接口是否可用:
模拟高并发场景下,库存的超卖问题
测试:
出现超卖问题,最多只能卖100件,在高并发的场景下却卖出了200件
17.7.5悲观锁和乐观锁
超卖问题是典型的多线程安全问题,针对这一问题的常见解决方案就是加锁:
1.悲观锁:添加同步锁,让线程串行执行
2.乐观锁:不加锁,在更新时判断是否有其它线程再修改
乐观锁
乐观锁的关键是判断之前查询得到的数据是否有被修改过,常见的方式有两种:
- ==CAS法(Campare And Swap)==
17.7.6使用乐观锁解决库存超卖(多线程并发安全)
采用CAS法解决多线程并发安全问题:
17.7.7使用悲观锁实现一人一单功能
需求:修改秒杀业务,要求同一个优惠券,一个用户只能下一单
测试:
发现只有第一个请求成功了
查看数据库:
订单表中只有一条订单信息
17.7.8集群下线程并发安全问题
将当前项目放到两台Tomcat服务器下进行运行:
修改nginx的conf目录下的nginx.conf文件,配置反向代理和负载均衡
重启nginx服务(不行就关机重启)
访问两次测试端口:
两个端口下都有日志信息,表名该项目已经在两台服务器上部署。
测试订单接口
放行之后数据库中有两条数据
出现以上问题的原因是因为多个JVM都是属于自己的锁监视器,每个JVM中的线程运行时,都会根据自己的锁监视器进行多线程之间的调用。而不会和其他JVM中的锁监视器有关系。所以集群部署的方式下,使用synchronized锁并不能解决多线程并发安全问题。
为了解决集群模式下多线程并发的安全问题,可以采用分布式锁的办法解决。
15.7.9使用分布式锁优化一人一单问题
使用悲观锁解决一人一单问题时时采用synchronize(同步锁)的方式来实现,但是在集群部署的模式下并不能解决多线程并发的安全性问题。所以可以采用Redis中的setnx在集群当中充当锁监视器,实现在多个服务器当中只有一个锁。
创建锁监视器
调用分布式锁,实现一人一单功能优化,在集群部署下不会出现多线程并发的安全性问题。
采用ApiFox进行测试:
17.7.10分布式锁误删优化
==为了防止因为线程阻塞而导致的分布式锁误删问题,在线程获取分布式锁的时候,向缓存中添加分布式锁的标识。当线程要释放锁的时候,查询缓存中的分布式锁的标识是否和自己的相同,相同的话就释放锁,不同的话就不做操作。==
让第1台服务器获取到分布式锁
删除刚刚生成的分布式锁,模拟超时过期,让服务器2获取到分布式锁
服务器2成功获取到分布式锁
服务器2成功获取到分布式锁并且最后释放锁。
数据库新增一条数据
17.7.11使用Lua脚本实现分布式锁的原子性
一:首先编写Lua脚本
17.7.12使用Redisson实现分布式锁
Redisson是一个在Redis的基础上实现的Java驻内存数据网格
。它不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务。其中就包含了各种分布式锁的实现。
使用ApiFox测试接口:
使用JMeter进行压力测试:
数据库只有一条数据
17.7.13秒杀优化(异步秒杀)
问题描述:==在之前的秒杀业务中,客户端向Nginx代理服务器发送请求,Nginx做负载代理到Tomcat服务器,整个业务流程中,查询优惠券、查询订单、减库存、创建订单都是操作数据库来完成的。对数据库做太多的读写操作的话整个业务耗时就会很长,并发能力就会很差。==
该如何解决以上问题呢?可以采用异步操作来完成。
==将校验用户购买资格的业务流程放到Redis缓存当中,当客户端发送请求时就会在缓存当中判断用户的购买资格,如果没有购买资格就直接返回错误。==
==如果有购买资格就保存优惠券、用户、订单id到阻塞队列,然后后台数据库异步读取队列中的信息,完成下单。==
为了保证判断用户是否有购买资格的业务的原子性,需要使用Lua脚本执行业务。
如果用户没有购买资格,就直接返回异常。如果有购买资格,完成将优惠券、用户、订单id写入阻塞队列,等待数据库完成异步下单操作。
需求:
==新增秒杀优惠券的同时,将优惠券信息保存到Redis中==
基于Lua脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
==如果抢购成功,将优惠券id和用户id封装后存入阻塞队列==
开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
开始创建需求
1.在创建秒杀券的同时将秒杀券的库存存入缓存当中。
查看数据库
查看缓存中有秒杀券库存数量:
2.基于Lua脚本完成用户下单资格验证
lua脚本文件内容
缓存中订单数加1,库存数减1
同一用户再次下单显示不能再次下单
==如果抢购成功,将优惠券id和用户id封装后存入阻塞队列==
开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能
使用ApiFox进行测试:
第一次下单成功
数据库完成异步更新
同一用户再次发送请求失败
使用JMeter进行高并发测试:
数据库只填加一条信息
秒杀业务的优化思路是什么?
==先利用Redis完成库存余量、一人一单判断,完成抢单业务==
==再将下单业务放入阻塞队列,利用独立线程异步下单==
基于阻塞队列的异步秒杀存在哪些问题?
一:内存限制问题
因为实现异步秒杀功能所使用的阻塞队列是JDK的阻塞队列,JDK的阻塞队列会使用JVM的内存,在高并发的场景下,会有无数的订单对象被创建并被放到阻塞队列里,可能会导致内存溢出。
二:数据安全问题
基于缓存保存的订单信息,如果服务崩溃,则所有的订单信息都会失效。
17.8达人探店
17.8.1发布探店博客
探店笔记类似点评网站的评价,往往是图文结合。对应的表有两个:
上传图片的接口:
设置图片的保存地址:
发布的接口:
17.8.2查看探店博客
需求:点击首页的探店笔记,会进入详情页面,实现该页面的查询接口:
在Blog类中有三个不属于Blog类的字段,分别是用户id,用户头像和用户姓名,用于在博客页面展示用户信息
控制层
service层
实现成功
17.8.3点赞博客(限制点赞次数)
在首页的探店笔记排行榜和探店图文详情页面都有点赞的功能:
需求:
实现步骤:
①给Blog类中添加一个isLike字段,标示是否被当前用户点赞
②修改点赞功能,==利用Redis的set集合判断是否点赞过,未点赞过则点赞数+1,已点赞过则点赞数-1==
③修改根据id查询Blog的业务,判断当前登录用户是否点赞过,赋值给isLike字段
④修改分页查询Blog业务,判断当前登录用户是否点赞过,赋值给isLike字段
实现步骤:
为Blog添加isLike属性,表示点赞的状态
17.8.4点赞排行榜
在探店笔记的详情页面,应该把给该笔记点赞的人显示出来,比如最早点赞的TOP5,形成点赞排行榜:
可以采用Redis中的有序列表进行排序,以博客id为key,用户id为value,用户点赞时间戳为score,以score作为排序的条件。
解决查询数据库in字段顺序不一致问题
添加ORDER BY FIELD字段指定参数顺序
修改前
修改后
17.8.5关注和取关
在探店图文的详情页面中,可以关注发布博客的作者:
需求:基于该表数据结构,实现两个接口:
①关注和取关接口
②判断是否关注的接口
关注是User之间的关系,是博主与粉丝的关系,数据库中有一张tb_follow表来标示:
控制层
Service层
关注成功,数据库插入一条数据
17.8.6共同关注
==共同关注就是当前登录的用户和查看的博主共同关注的人==
点击博主头像,可以进入博主首页:
首先实现点击用户头像进入博主页面,博主页面有博主信息和发布的博客信息。
查询用户信息的url http://localhost:8080/api/user/{用户id}
获取到用户博客信息的url http://localhost:8080/api/of/user
Service层
实现完成,接下实现共同关注
实现共同关注
需求:利用Redis中恰当的数据结构,实现共同关注功能。在博主个人页面展示出当前用户与博主的共同好友。
查询博主和当前用户的共同关注接口 http://localhost:8080/api/follow/common/{博主id}
在之间的关注功能业务里添加:当关注一个博主时,将以当前登录用户id做为key,关注的博主id做为value。查询两个用户的共同关注就求缓存中两个用户关注列表的交集。
Service层的FollowServiceImpl
添加用户关注博主时的缓存信息
测试:
登录三个用户,分别是 阳光、可爱多、可可今天不吃肉,让前两者关注后者,在阳光账户下查看可爱多的共同关注列表。
缓存信息,==id为1(阳光)和id为5(可爱多)的用户共同关注了id为2(可可今天不吃肉)的用户==
17.8.7关注推送(Feed流)
关注推送也叫做Feed流,直译为投喂。为用户持续的提供“沉浸式”的体验,通过无限下拉刷新获取新的信息。
Feed流的模式:
Feed流产品有两种常见模式:
Timeline:不做内容筛选,简单的按照内容发布时间排序,常用于好友或关注。例如朋友圈
智能排序:利用智能算法屏蔽掉违规的、用户不感兴趣的内容。推送用户感兴趣信息来吸引用户
优点:投喂用户感兴趣信息,用户粘度很高,容易沉迷
缺点:如果算法不精准,可能起到反作用
本例中的个人页面,是基于关注的好友来做Feed流,因此采用Timeline的模式。该模式的实现方案有三种:
==拉模式==
==推模式==
==推拉结合==
==一:拉模式==
用户可以根据关注的博主对其发件箱中的内容进行拉取,然后将拉取的内容按照时间进行排序。
==二:推模式==
博主会将内容推送给所有的粉丝的收件箱中,并且会进行排序好。粉丝每次可以在收件箱中直接进行读取。
==三:推拉结合==
活跃粉丝采用推模式,普通粉丝采用拉模式。
17.8.9Feed的分页问题
Feed流中的数据会不断更新,所以数据的角标也在变化,会读取到重复的数据。因此不能采用传统的分页模式。
Feed流的滚动分页
记录上次最后的一条记录,下次分页在此记录之后进行分页
17.8.8基于Timeline模式的推方式实现关注推送
需求:
①修改新增探店笔记的业务,在保存blog到数据库的同时,遍历当前用户的粉丝,并将blog推送到粉丝的收件箱。
②收件箱满足可以根据时间戳排序,必须用Redis的数据结构实现。收件箱是以粉丝的id作为key,发布的新博客id作为value,时间戳为score。
③查询收件箱数据时,可以实现分页查询
17.8.9实现关注页面的分页查询
需求:在个人主页的“关注”卡片中,查询并展示推送的Blog信息:
17.9附近商户
17.9.1GEO数据结构概念
GEO就是Geolocation(地理坐标)的简写形式
。Redis在3.2版本中加入了对GEO的支持,允许存储地理坐标信息,帮助我们根据经纬度来检索数据。常见的命令有:
GEOADD:添加一个地理空间信息,包含:经度(longitude)、纬度(latitude)、值(member)
GEODIST:计算指定的两个点之间的距离并返回
GEOHASH:将指定member的坐标转为hash字符串形式并返回
GEOPOS:返回指定member的坐标
GEOSEARCH:在指定范围内搜索member,并按照与指定点之间的距离排序后返回。范围可以是圆形或矩形。6.2.新功能
GEOSEARCHSTORE:与GEOSEARCH功能一致,不过可以把结果存储到一个指定的key。 6.2.新功能
17.9.2GEO数据结构练习
1.添加下面几条数据:
北京南站( 116.378248 39.865275 )
北京站( 116.42803 39.903738 )
北京西站( 116.322287 39.893729 )
查看缓存
2.计算北京西站到北京站的距离
3.搜索天安门( 116.397904 39.909005 )附近10km内的所有火车站,并按照距离升序排序
17.9.3将商户按照类型分组并存入缓存
在首页中点击某个频道,即可看到频道下的商户:
按照商户类型做分组,类型相同的商户作为同一组,以typeId为key存入同一个GEO集合中即可
17.9.4搜索附近商户
因为GEO是在redis3.2版本加入的,所以对redis的依赖和lettuce连接池版本要求高。
进行排除,添加高版本的相关依赖
17.10用户签到
17.10.1BitMap的用法
假如我们用一张表来存储用户签到信息,其结构应该如下:
假如有1000万用户,平均每人每年签到次数为10次,则这张表一年的数据量为 1亿条
每签到一次需要使用(8 + 8 + 1 + 1 + 3 + 1)共22 字节的内存,一个月则最多需要600多字节
我们按月来统计用户签到信息,签到记录为1,未签到则记录为0.
Redis中是利用string类型数据结构实现BitMap,因此最大上限是512M,转换为bit则是 2^32个bit位。
BitMap的操作命令有:
SETBIT:向指定位置(offset)存入一个0或1
GETBIT :获取指定位置(offset)的bit值
BITCOUNT :统计BitMap中值为1的bit位的数量
BITFIELD :==操作(查询、修改、自增)BitMap中bit数组中的指定位置(offset)的值==
BITFIELD_RO :获取BitMap中bit数组,并以十进制形式返回
BITOP :将多个BitMap的结果做位运算(与 、或、异或)
BITPOS :查找bit数组中指定范围内第一个0或1出现的位置
查询某一天的签到情况
使用BITFIELD命令查询指定位置的数值,返回的是十进制。
u表示是无符号(即正值),0表示从头开始查询
查询结果为7,即二进制111的十进制结果
17.10.2实现签到功能
需求:实现签到接口,将当前用户当天签到信息保存到Redis中
提示:因为BitMap底层是基于String数据结构,因此其操作也都封装在字符串相关操作中了。
测试签到接口,签到成功。
17.10.3统计连续签到
问题1:什么叫做连续签到天数?
从最后一次签到开始向前统计,直到遇到第一次未签到为止,计算总的签到次数,就是连续签到天数。
问题2:如何得到本月到今天为止的所有签到数据?
BITFIELD key GET u[dayOfMonth] 0
问题3:如何从后向前遍历每个bit位?
与 1 做与运算,就能得到最后一个bit位。
随后右移1位,下一个bit位就成为了最后一个bit位。
需求:实现下面接口,统计当前用户截止当前时间在本月的连续签到天数
17.11UV统计
17.11.1HyperLogLog用法
UV:全称U**nique **Visitor,也叫独立访客量,是指通过互联网访问、浏览这个网页的自然人。1天内同一个用户多次访问该网站,只记录1次。
PV:全称P**age **View,也叫页面访问量或点击量,用户每访问网站的一个页面,记录1次PV,用户多次打开页面,则记录多次PV。往往用来衡量网站的流量。
UV统计在服务端做会比较麻烦,因为要判断该用户是否已经统计过了,需要将统计过的用户信息保存。但是如果每个访问的用户都保存到Redis中,数据量会非常恐怖。
Hyperloglog(HLL)是从Loglog算法派生的概率算法,用于确定非常大的集合的基数,而不需要存储其所有值。Redis中的HLL是基于string结构实现的,==单个HLL的内存永远小于16kb==,内存占用低的令人发指!作为代价,其测量结果是概率性的,有小于0.81%的误差。不过对于UV统计来说,这完全可以忽略。
17.11.2实现UV统计
测试插入100万条数据
插入之前的内存大小
插入之后内存大小
2415512-2401128=14384
14384/1024约等于14kb,小于16kb
18.分布式缓存
18.1采用分布式缓存的原因
在前面的章节中都是使用==redis的单节点部署==,单点redis会出现很多问题。
数据丢失问题
Redis是内存存储,服务重启可能会丢失数据
并发能力问题
单节点Redis并发能力虽然不错,但也无法满足如618这样的高并发场景
故障恢复问题
如果Redis宕机,则服务不可用,需要一种自动的故障恢复手段
存储能力问题
Redis基于内存,单节点能存储的数据量难以满足海量数据需求
单点Redis出现的问题的解决方案如下:
18.2Redis持久化
18.2.1RDB持久化
RDB全称Redis Database Backup file(==Redis数据备份文件==),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据。
快照文件称为RDB文件,默认是保存在当前运行目录。
Redis停机时会执行一次RDB。
Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:
RDB的其它配置也可以在redis.conf文件中设置:
采用vim编辑器进入redis.conf文件
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
fork采用的是copy-on-write技术:
读时共享,写时复制
RDB方式bgsave的基本流程?
fork主进程得到一个子进程,共享内存空间
子进程读取内存数据并写入新的RDB文件
用新RDB文件替换旧的RDB文件。
RDB会在什么时候执行?save 60 1000代表什么含义?
默认是服务停止时。
代表60秒内至少执行1000次修改则触发RDB
RDB的缺点?
18.2.2AOF持久化
AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是==命令日志文件。==
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF:
AOF的命令记录的频率也可以通过redis.conf文件来配:
配置项 |
刷盘时机 |
优点 |
缺点 |
Always |
同步刷盘 |
可靠性高,几乎不丢数据 |
性能影响大 |
everysec |
每秒刷盘 |
性能适中 |
最多丢失1秒数据 |
no |
操作系统控制 |
性能最好 |
可靠性较差,可能丢失大量数据 |
为了验证AOF能否实现持久化的效果,首先禁用RDB
开启AOF
设置刷盘方式为每秒钟1次
测试:
存储一条数据
在redis目录下生成了aof文件
在存储一条数据
查看aof文件
测试关闭redis服务
再次启动redis服务
数据依然存在
AOF文件存在的问题
因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。
查看当前aof文件
使用bgrewriteaof
命令进行aof文件重写操作
bgrewriteaof
重启服务数据依旧存在
Redis也会在触发阈值时自动去重写AOF文件
。阈值也可以在redis.conf中配置:
18.2.3RDB持久化和AOF持久化比较
RDB和AOF各有自己的优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用
。
18.3Redis主从
18.3.1主从复制简介
主从复制,简单来说就是主机数据更新后根据配置和策略, 自动同步到从机的master/slaver机制,Master以写为主,Slave以读为主。
主节点承担写操作,并将数据同步到多个从节点上,实现数据同步。
多个从节点承担读操作,提高读操作的并发能力。
18.3.2主从集群搭建
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。
==要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。==
将redis.conf文件内容恢复到初始状态,并创建三个文件,将redis的配置文件redis.conf复制到这三个文件夹当中。
修改各个配置文件当中redis服务的端口号和修改数据保存目录(原始是dir。表示当前目录)
修改每个实例的声明IP
虚拟机本身有多个IP,为了避免将来混乱,我们需要在redis.conf文件中指定每一个实例的绑定ip信息,格式如下:
redis实例的声明 IP
replica-announce-ip 192.168.150.101
每个目录都要改,我们一键完成修改(在/opt目录执行下列命令):
启动
为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:
在新建的三个实例中同时开启redis服务
18.3.3开启主从关系
现在三个实例还没有任何关系,要配置主从可以使用replicaof 或者slaveof(5.0以前)命令。
有临时和永久两种模式:
==设置将7001作为主节点,7002和7003做为从节点。==
关闭主节点保护模式,不关闭从节点连接不上主节点
从节点7002、7003日志信息改变
在主节点下查看主从架构状态信息
可以发现7001为主节点,旗下有两个从节点,端口号分别是7002、7003
18.3.4测试主从读写
在主节点上进行读写测试
在从节点进行读写操作
18.3.5主从数据同步原理
18.3.5.1全量同步
主从第一次同步是全量同步:
master如何判断slave是不是第一次来同步数据?这里会用到两个很重要的概念:
•Replication Id:简称replid,==是数据集的标记==,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
•offset:==偏移量==,随着记录在repl_baklog中的数据增多而逐渐增大。slave完成同步时也会记录当前同步的offset。如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。
==因此slave做数据同步,必须向master声明自己的replication id 和offset,master才可以判断到底需要同步哪些数据==
全量同步的流程
slave节点请求增量同步
master节点判断replid,发现不一致,拒绝增量同步
master将完整内存数据生成RDB,发送RDB到slave
slave清空本地数据,加载master的RDB
master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave
slave执行接收到的命令,保持与master之间的同步
18.3.5.2增量同步
主从第一次同步是全量同步,但如果slave重启后同步,则执行增量同步
18.3.5.3从主优化
在master中配置repl-diskless-sync yes启用无磁盘复制,避免全量同步时的磁盘IO。
Redis单节点上的内存占用不要太大,减少RDB导致的过多磁盘IO
适当提高repl_baklog的大小,发现slave宕机时尽快实现故障恢复,尽可能避免全量同步
限制一个master上的slave节点数量,如果实在是太多slave,则可以采用主-从-从链式结构,减少master压力
18.3.5.4全量同步和增量同步的区别
简述全量同步和增量同步区别?
什么时候执行全量同步?
什么时候执行增量同步?
- slave节点断开又恢复,并且在repl_baklog中能找到offset时
18.4Redis哨兵
slave节点宕机恢复后可以找master节点同步数据,那master节点宕机怎么办?Redis中的哨兵机制可以解决这个问题
18.4.1哨兵的作用
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复
。哨兵的结构和作用如下:
作用:
监控:Sentinel 会不断检查您的master和slave是否按预期工作
自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主
通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
18.4.2哨兵的工作原理
Sentinel基于心跳机制监测服务状态
,每隔1秒向集群的每个实例发送ping命令:
主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)
的sentinel都认为该实例主观下线,则该实例客观下线。
quorum值最好超过Sentinel实例数量的一半
。
18.4.3主节点的选取方式
判定主节点下线后哨兵会从从节点中选取一个新的节点作为主节点
首先会判断slave节点与master节点断开时间长短,如果超过指定值(down-after-milliseconds * 10)则会排除该slave节点
然后判断slave节点的slave-priority值,越小优先级越高,如果是0则永不参与选举(默认都一样)
如果slave-prority一样,则判断slave节点的offset值,越大说明数据越新,优先级越高
最后是判断slave节点的运行id大小,越小优先级越高。
18.4.4故障转移
当选中了其中一个slave为新的master后(例如slave1),故障的转移的步骤如下
sentinel给备选的slave1节点发送slaveof no one
命令,让该节点成为master
sentinel给所有其它slave发送slaveof 192.168.150.101 7002 命令,让这些slave成为新master的从节点,开始从新的master上同步数据。
最后,sentinel将故障节点标记为slave,当故障节点恢复后会自动成为新的master的slave节点
总结:
Sentinel的三个作用是什么?
Sentinel如何判断一个redis实例是否健康?
故障转移步骤有哪些?
18.4.5搭建哨兵集群
这里我们搭建一个三节点形成的Sentinel集群,来监管之前的Redis主从集群。如图:
三个sentinel实例信息如下:
节点 |
IP |
PORT |
s1 |
192.168.26.133 |
27001 |
s2 |
192.168.26.133 |
27002 |
s3 |
192.168.26.133 |
27003 |
要在同一台虚拟机开启3个实例,必须准备三份不同的配置文件和目录,配置文件所在目录也就是工作目录。
我们创建三个文件夹,名字分别叫s1、s2、s3:
然后我们在s1目录创建一个sentinel.conf文件,添加下面的内容:
解读:
然后将s1/sentinel.conf文件拷贝到s2、s3两个目录中(在/opt目录执行下列命令):
修改s2、s3两个文件夹内的配置文件,将端口分别修改为27002、27003:
启动哨兵集群
为了方便查看日志,我们打开3个ssh窗口,分别启动3个redis实例,启动命令:
开始监控主从集群
测试:
尝试让master节点7001宕机,查看sentinel日志:
主节点停机之后,从节点连接失败
==主节点宕机后,哨兵集群会选出最先发现主节点宕机的哨兵作为leader在slave中选出新的主节点==
7003的主模式启用
恢复7001从节点
7001从节点读取RDB文件进行全量同步
7003主节点开始同步7001从节点
在7003查看主从架构信息
7003为主节点,7001和7002是他的从节点
18.4.6RedisTemplate连接哨兵集群
首先关闭虚拟机防火墙,并重启redis和sentinel
在Sentinel集群监管下的Redis主从集群,其节点会因为自动故障转移而发生变化,==Redis的客户端必须感知这种变化,及时更新连接信息。==Spring的RedisTemplate底层利用lettuce实现了节点的感知和自动切换。
一:在pom文件当中添加SpringBoot对Redis的开发场景
二:在SpringBoot配置文件当中指定sentinel(哨兵)的信息
三:配置Redis主从读写分离(可以在配置文件当中或主类当中配置)
这里的ReadFrom是配置Redis的读取策略,是一个枚举,包括下面选择:
MASTER:从主节点读取
MASTER_PREFERRED:优先从master节点读取,master不可用才读取replica
==REPLICA:从slave(replica)节点读取==
==REPLICA _PREFERRED:优先从slave(replica)节点读取,所有的slave都不可用才读取master==
项目结构:
控制器:
启动项目,测试接口。
读操作:
写操作是在主节点7002完成的
写操作:
写操作是在主节点7001完成的
18.4.7测试lettuce的节点感知和自动切换
使主节点宕机,让哨兵集群选出新的主节点
主节点变为7003
执行写操作,查看控制台
18.5Redis分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
使用分片集群可以解决上述问题,分片集群特征:
18.5.1搭建分片集群
分片集群需要的节点数量较多,这里我们搭建一个最小的分片集群,包含3个master节点,每个master包含一个slave节点,结构如下:
这里我们会在同一台虚拟机中开启6个redis实例,模拟分片集群,信息如下:
IP |
PORT |
角色 |
192.168.26.133 |
7001 |
master |
192.168.26.133 |
7002 |
master |
192.168.26.133 |
7003 |
master |
192.168.26.133 |
8001 |
slave |
192.168.26.133 |
8002 |
slave |
192.168.26.133 |
8003 |
slave |
删除搭建主从集群的文件夹,重新创建文件
在7001下准备一个新的redis.conf文件,内容如下:
将这个文件拷贝到其他几个文件下:
修改每个redis.conf中的端口信息
启动所有redis服务
虽然服务启动了,但是目前每个服务之间都是独立的,没有任何关联。
我们需要执行命令来创建集群,在Redis5.0之前创建集群比较麻烦,5.0之后集群管理命令都集成到了redis-cli中。
我们使用的是Redis5.0以上的版本,集群管理以及集成到了redis-cli中,格式如下:
命令说明:
redis-cli --cluster
或者./redis-trib.rb
:代表集群操作命令
create
:代表是创建集群
--replicas 1
或者--cluster-replicas 1
:指定集群中每个master的副本个数为1,此时节点总数 ÷ (replicas + 1)
得到的就是master的数量。因此节点列表中的前n个就是master,其它节点都是slave节点,随机分配到不同master
查看集群状态
18.5.2散列插槽
在创建分片集群的时候,每一个主节点后都有slots,这是散列插槽
Redis会把每一个master节点映射到0~16383共16384个插槽(hash slot)上,查看集群信息时就能看到:
由上图可以看出:
主节点7001分配的插槽范围是[0-5460]
主节点7002分配的插槽范围是[5461-10922]
主节点7003分配的插槽范围是[10923-16383]
数据key不是与节点绑定,而是与插槽绑定。redis会根据key的有效部分计算插槽值
,分两种情况:
例如:key是num,那么就根据num计算,如果是{itcast}num,则根据itcast计算。计算方式是利用CRC16算法得到一个hash值,然后对16384取余,得到的结果就是slot值。
测试插槽:
==可以查看存入值后插槽位置是14315,属于主节点7003,则将客户端切换到7003==
总结:
Redis如何判断某个key应该在哪个实例?
将16384个插槽分配到不同的实例
根据key的有效部分计算哈希值,对16384取余
余数作为插槽,寻找插槽所在实例即可
如何将同一类数据固定的保存在同一个Redis实例?
- 这一类数据使用相同的有效部分,例如key都以{typeId}为前缀
18.5.3集群伸缩
redis-cli –cluster提供了很多操作集群的命令,可以通过下面方式查看
向集群中添加一个新的master节点,并向其中存储 num = 10
需求:
修改复制后的配置文件端口,再启动7004文件中的redis服务
后面指定已经存在的集群节点
查看集群状态
但是新创建的节点没有查询
向新的节点分配插槽
根据命名查看帮助文档
删除节点
首先将节点的插槽转移
删除节点
删除成功
18.5.4故障转移
采用watch的方式监控分片集群
测试将7002宕机
重启7002的redis服务
18.5.5数据迁移
利用cluster failover命令可以手动让集群中的某个master宕机,切换到执行cluster failover命令的这个slave节点,实现无感知的数据迁移
。其流程如下:
测试:将刚刚成为slave节点的7002变为master
利用redis-cli连接7002这个节点
执行cluster failover命令
查看集群状态信息
18.5.6RedisTemplate连接分片集群
首先关闭虚拟机防火墙,并重启redis和sentinel
RedisTemplate底层同样基于lettuce实现了分片集群的支持,而使用的步骤与哨兵模式基本一致:
引入redis的starter依赖
配置分片集群地址
配置读写分离
与哨兵模式相比,其中只有分片集群的配置方式略有差异,如下:
测试接口:
查看控制台,读操作操作7002的从节点8003
写操作操作7002主节点
19.多级缓存
传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,存在下面的问题:
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能:
浏览器访问静态资源时,优先读取浏览器本地缓存
在多级缓存架构中,Nginx内部需要编写==本地缓存查询、Redis查询、Tomcat查询的业务逻辑==,因此这样的nginx服务不再是一个反向代理服务器,而是一个编写业务的Web服务器了。
因此这样的业务Nginx服务也需要搭建集群来提高并发,再有专门的nginx服务来做反向代理,如图:
可见,多级缓存的关键有两个:
一个是在nginx中编写业务,实现nginx本地缓存、Redis、Tomcat的查询
另一个就是在Tomcat中实现JVM进程缓存
20.缓存同步策略
缓存数据同步的策略常见的方式有3种:
- 失效模式:在修改数据库时,直接使对应的缓存数据失效,下次读取该数据时,从数据库中重新加载最新数据并更新缓存。
- 优点:简单、方便
- 缺点:缓存在失效后可能被多次同时重建,导致短时间内的数据库负载增加。
- 场景:较为宽松一致性要求的场景
- 同步双写:在修改数据库的同时,直接修改缓存
- 优点:时效性强,缓存和数据库强一致性
- 缺点:有代码侵入性,耦合度高
- 场景:对一致性、时效性要求比较高的缓存数据
- 异步通知:修改数据库时发送时间通知,相关服务监听到通知以后修改缓存数据
- 优点:低耦合,可以同时通知多个缓存服务
- 缺点:时效性一般,可能存在中间不一致状态
- 场景:时效性要求一般,有多个服务需要同步
- 基于消息队列的异步通知
- 基于Canal的异步通知
Canal就是把自己伪装成MySQL的一-个slave节点,从而监听master的binary log
变化。再把得到的变化信息通知给
Canal
的客户端,进而完成对其它数据库的同步。