谷粒商城高级(二)组件的使用
组件的使用
- 一、Elasticsearch
- 1.知识梳理
- 1.1 基本概念
- 1.1.1 Index(索引)
- 1.1.2 Type(类型)
- 1.1.3 Document(文档)
- 1.1.4 倒排索引机制
- 1.2 相关命令
- 1.2.1 _cat
- 1.2.2 索引一个文档(保存一条记录)
- 1.2.3 查询文档
- 1.2.4 更新文档
- 1.2.5 删除文档和索引
- 1.2.6 bulk批量API
- 1.3 进阶检索
- 1.3.1 ES中的两种检索方法
- 1.3.2 Query DSL
- 1.3.2.1 基本语法结构
- 1.3.2.2 针对某个字段结构
- 1.3.2.3 返回部分字段
- 1.3.2.4 match查询
- 1.3.2.5 match_phrase短语匹配
- 1.3.2.6 multi_match多字段匹配
- 1.3.2.7 bool复合查询
- 1.3.2.8 term
- 1.4 aggregations聚合功能
- 1.4.1 基本概念
- 1.4.2 基本语法
- 1.4.3 举例
- 1.5 Mapping映射
- 1.5.1 基本概念
- 1.5.2 相关操作
- 1.5.2.1 创建映射规则
- 1.5.2.2 添加新字段映射
- 1.5.2.3 映射的更新
- 1.5.2.4 数据迁移
- 2. 组件使用
- 2.1 全文检索-数据库数据的检索
- 2.2 ELK技术栈-日志分析检索
- 二、nginx
- 1. nginx搭建域名访问环境
- 1.1 正向代理与反向代理
- 1.1.1 正向代理
- 1.1.2 反向代理
- 1.2 nginx配置文件简介
- 1.3 nginx的域名访问环境搭建
- 1.3.1 nginx在虚拟机中
- 1.3.1.1 修改host文件
- 1.3.1.2 修改nginx配置文件
- 1.3.2 nginx在在腾讯云服务器中
- 1.3.2.1 修改host文件
- 1.3.2.2 花生壳进行内网穿透
- 1.3.2.3 修改nginx配置文件
- 1.3.2.3 注意
- 1.3.3 nginx在本地windows系统中(使用)
- 1.3.3.1 修改host文件
- 1.3.3.2 下载一个windows版的nginx
- 1.3.3.3 修改nginx配置文件
- 2. nginx实现动静分离
- 2.1 动静分离
- 2.2 实现步骤
- 2.2.1 移动静态资源
- 2.2.2 设置nginx访问规则
- 2.2.3 修改项目中首页
- 三、Redis做缓存
- 1. Redis的整合
- 2. 分布式锁框架Redisson
- 2.1 Redisson的引入
- 2.2 Redisson的使用
- 2.2.1 可重入锁(Reentrant Lock)
- 2.2.2 读写锁(ReadWriteLock)
- 2.2.3 信号量(Semaphore)
- 2.2.4 闭锁(CountDownLatch)
- 3. 缓存数据一致性问题
- 3.1 双写模式
- 3.2 失效模式
- 3.3 总结
- 3.4 高效解决:Canal
- 4. SpringCache简化缓存开发
- 4.1 SpringCache的引入
- 4.2 SpringCache的使用
- 4.2.1 前提
- 4.2.2 @Cacheable注解
- 4.2.3 @CachEvict注解
- 4.2.4 @CachePut注解
- 4.2.5 @Caching注解
- 4.3 SpringCache总结
- 4.3.1 读模式
- 4.3.2 写模式
- 4.3.3 总结
- 5. 缓存小总结
- 四、注册登录
- 1. 验证码
- 2. 社交登录
- 2.1 OAuth基本概念
- 2.2 OAuth2.0的社交登录流程
- 2.3 微博社交登录
- 2.3.1 微博授权机制说明
- 2.3.2 微博登录流程解析
- 3. 分布式Session
- 3.1 Session的工作原理
- 3.2 Session缺陷及解决
- 3.2.1 同一微服务Session共享问题
- 3.2.1.1 问题描述
- 3.2.1.2 解决方案
- 3.2.2 不同同微服务Session共享
- 3.2.2.1 问题描述
- 3.2.2.2 解决方案(采用)
- 3.3 整合SpringSession解决问题
- 3.3.1 同一微服务Session共享问题
- 3.3.2 不同同微服务Session共享
- 3.3.3 附加:采用JSON序列化机制
- 3.3 SpringSession的核心原理(装饰者模式)
- 4. 单点登录
- 4.1 概述
- 4.2 实现
- 4.2.1 实现前提
- 4.2.2 实现步骤
- 4.2.3 要点分析
- 4.2.4 现成框架
- 五、Seata分布式事务
- 1. 本地事务
- 1.1 本地事务相关基础知识
- 1.1.1 本地事务基本概念
- 1.1.2 @Transactional注解
- 1.1.2.1 说明
- 1.1.2.2 传播行为
- 1.1.3 SpringBoot中的本地事务
- 1.2 本地事务在分布式下的问题
- 2. 分布式事务
- 2.1 分布式事务定理
- 2.1.1 CAP定理
- 2.2.2 BASE理论
- 2.2.3 三种一致性
- 2.3 分布式事务的解决方案
- 2.3.1 刚性事务(ACID)- 2PC模式
- 2.3.2 柔性事务(BASE)- TCC事务补偿型方案
- 2.3.3 柔性事务(BASE)- 最大努力通知型方案
- 2.3.4 柔性事务(BASE)- 可靠消息 + 最终一致性方案
- 3. Seata
- 3.1 Seata的基本介绍
- 3.2 Seata的环境准备
- 3.2.1 建立UNDO_LOG表
- 3.2.2 下载安装Seata
- 3.3 Seata的使用(默认为AT模式)
- 3.3.1 导入Seata的依赖
- 3.3.2 修改配置文件
- 3.2.1 registry.conf
- 3.2.2 file.conf
- 3.3.3 注入DataSourceProxy
- 3.3.4 复制配置文件
- 3.3.5 修改配置文件
- 3.3.6 @GlobalTransactional
- 六、RabbitMQ
- 1. MQ基础知识
- 1.1 使用场景
- 1.1.1 异步处理
- 1.1.2 应用解耦
- 1.1.3 流量控制
- 1.2 基础知识
- 1.2.1 基本概念
- 1.2.1.1 消息代理(message broker)和目的地(destination)
- 1.2.1.2 消息队列主要有两种形式的目的地
- 1.2.1.3 两种规范
- 2. RabbitMQ基本概念
- 2.1 RabbitMQ简介
- 2.2 核心概念
- 2.2.1 Message
- 2.2.2 Publisher
- 2.2.3 Exchange
- 2.2.4 Queue
- 2.2.5 Binding
- 2.2.6 Connection
- 2.2.7 Channel
- 2.2.8 Consumer
- 2.2.9 Virtual Host
- 2.2.10 Broker
- 2.3 Docker下的安装
- 2.3.1 安装流程
- 2.3.2 端口介绍
- 2.4 RabbitMQ的交换机Exchange
- 2.4.1 direct
- 2.4.2 fanout
- 2.4.3 topic
- 2.5 RabbitMQ的整合
- 2.6 RabbitMQ的具体使用
- 2.6.1 AmqpAdmin创建交换机、队列、绑定
- 2.6.2 RabbitTemplate收发消息
- 2.6.3 @RabbitListener监听队列
- 2.6.3.1 前提:@RabbitListener必须开启@EnableRabbit
- 2.6.3.2 标注在方法上
- 2.6.3.3 标注在类上(常用)
- 2.6.4 @RabbitHandler监听队列
- 2.6.4.1 前提:@RabbitHandler必须开启@EnableRabbit
- 2.6.3.2 标注在方法上
- 2.7 RabbitMQ的消息确认机制
- 2.7.1 概述
- 2.7.2 发送端确认
- 2.7.2.1 confirmaCallback
- 2.7.2.2 returnCallback
- 2.7.3 消费端确认
- 2.8 RabbitMQ的延时队列
- 2.8.1 死信
- 2.8.1.1 TTL
- 2.8.1.2 Dead Letter Exchanges(DLX)
- 2.8.2 延时
一、Elasticsearch
1.知识梳理
全文搜索属于最常见的需求,开源的Elasticsearch是目前全文搜索引擎的首选。它可以快速地储存、搜索和分析海量数据。Elastic的底层是开源库Lucene。但是,Lucene无法直接使用,必须自己写代码去调用它的接口。而Elastic是Lucene的封装,提供了RESTAPI的操作接口,开箱即用。
1.1 基本概念

1.1.1 Index(索引)
- 动词,相当于MySQL中的insert。ES中的索引一条数据等同于MySQL中插入一条数据。
- 名词,相当于MySQL中的Database。ES中的简历一个索引等同于MySQL中建立一个数据库。
1.1.2 Type(类型)
- 在Index(索引)中,可以定义一个或多个类型。
- 类型就类似于MySQL中的Table每一种类型的数据放在一起。
1.1.3 Document(文档)
- 保存在某个索引(Index)下,某种类型(Type)的一个数据(Document)。
- 文档是JSON格式的,Document就像是MySQL中的某个Table里面的内容,一条文档就类似于MySQL中的一条数据。
1.1.4 倒排索引机制

1.2 相关命令
1.2.1 _cat
| 命令 | 功能 |
|---|---|
| GET请求_cat/nodes | 查看所有节点 |
| GET请求_cat/health | 查看 es 健康状况 |
| GET请求_cat/master | 查看主节点 |
| GET请求_cat/indices | 查看所有索引 |
1.2.2 索引一个文档(保存一条记录)
需要具体指明保存在哪个索引的哪个类型下,可能要指明用哪个唯一标识。
//比如在customer索引下的external类型下保存1号数据:
//1.以PUT方式或POST方式发送如下请求
customer/external/1
//2.请求体内容如下
{ "name": "John Doe"
}
1.PUT和POST都可以完成文档的索引(即数据的插入)。
2.POST,如果不指定id,会自动生成id并新增数据;指定id如果id不存在就会新增这条数据,存在则会修改这个数据,并新增版本号。
3.PUT,PUT必须指定id,如果id不存在则为新增,存在则为修改。一般用PUT做修改操作。
1.2.3 查询文档
需要指明查询哪个索引的哪个类型下的哪个文档
//比如查询customer索引下的external类型下的1号数据:
//1.以GET方式发送如下请求:
customer/external/1
//2.查询结果如下:
{"_index": "customer", //在哪个索引"_type": "external", //在哪个类型"_id": "1", //记录 id"_version": 2, //版本号"_seq_no": 1, //并发控制字段,每次更新就会+1,用来做乐观锁"_primary_term": 1, //同上,主分片重新分配,如重启,就会变化"found": true, "_source": { //真正的内容"name": "John Doe"}
}
//3.如何实现乐观锁?
更新时,发送的请求后面携带"?if_seq_no=最新信息&if_primary_term=1"即可
1.2.4 更新文档
1.方式一:POST方式带_update
//比如更新customer索引下的external类型下的1号数据
//POST方式发送请求customer/external/1/_update
{"doc":{"name": "John Doew"}
}
//该方式会对比源文档数据,如果相同不会进行任何操作,版本号等信息都不变。
2.方式二
//比如更新customer索引下的external类型下的1号数据
//POST方式发送请求customer/external/1
{"name": "John Doe2"
}
//该方式就是不论数据是否不变,都会直接更新
3.方式三
//比如更新customer索引下的external类型下的1号数据
//PUT方式发送请求customer/external/1
{"name": "John Doe2"
}
//该方式就是不论数据是否不变,都会直接更新
4.注解:以上方式都允许更新的同时增加属性,比如
//POST方式发送请求customer/external/1/_update
{"doc": {"name": "Jane Doe", "age": 20 }
}
1.2.5 删除文档和索引
1.删除文档
//比如删除customer索引下的external类型下的1号数据
//DELETE方式发送请求customer/external/1
2.删除索引
//比如删除customer索引
//DELETE方式发送请求customer
1.2.6 bulk批量API
//1.语法格式:以POST方式发送_bulk请求,携带以下请求体
{ action: { metadata }}
{ request body }
{ action: { metadata }}
{ request body }
1.说明:bulkAPI以此按顺序执行所有的action(动作)。如果一个单个的动作因任何原因而失败,它将继续处理它后面剩余的动作。当bulkAPI 返回时,它将提供每个动作的状态(与发送的顺序相同),所以可以检查是否一个指定的动作是不是失败了。
2.注意:可以在请求中携带对哪个索引的哪个类型进行操作,如果请求中没有携带,则需要在metadata中指明,否则会报错找不到索引。
1.3 进阶检索
1.3.1 ES中的两种检索方法
1.检索的所有条件放在请求中
GET bank/_search?q=*&sort=account_number:asc
2.利用uri和请求体进行检索
GET bank/_search
{"query": {"match_all": {}},"sort": [{"account_number": { "order": "desc"}}]
}
3.结果分析
took:Elasticsearch 执行搜索的时间(毫秒)。
time_out:告诉我们搜索是否超时。
_shards:告诉我们多少个分片被搜索了,以及统计了成功/失败的搜索分片。
hits:搜索结果。
hits.total:搜索结果。
hits.hits:实际的搜索结果数组(默认为前10的文档)。
sort:结果的排序 key(键)(没有则按score排序)。
score和max_score:相关性得分和最高得分(全文检索用)。
1.3.2 Query DSL
利用uri和请求体检索时,请求体其实就是我们所说的Query DSL,他要遵循指定的规则。
1.3.2.1 基本语法结构
1.基本语法:
{QUERY_NAME: {ARGUMENT: VALUE, ARGUMENT: VALUE,... }
}
2.举例:
GET bank/_search
{ "query": {"match_all": {}}
}
1.3.2.2 针对某个字段结构
1.基本语法:
{QUERY_NAME: {FIELD_NAME: {ARGUMENT: VALUE, ARGUMENT: VALUE,... }}
}
2.举例:
{"query": {"match_all": {}},"from": 0, "size": 5,"sort": [{ "account_number": {"order": "desc"}}]
}
解释:
query定义如何查询,match_all查询类型(代表查询所有的所有),es中可以在query中组合非常多的查询类型完成复杂查询。
除了query参数之外,我们也可以传递其它的参数以改变查询结果,如 sort,size等。
from+size限定,完成分页功能。
sort 排序,多字段排序,会在前序字段相等时后续字段内部排序,否则以前序为准。
1.3.2.3 返回部分字段
1.使用方法:在_source中以数组方式指明需要返回的字段即可。
2.举例:
{"query": {"match_all": {}},"from": 0, "size": 5, "_source": ["age","balance"]
}
1.3.2.4 match查询
1.基本语法:
{ "query":{"match": {"FIELD": "TEST"}}
}
2.举例:
//例1
{ "query": {"match": {"account_number": "20"}}
}
//例2
{ "query": {"match": {"address": "mill"}}
}
//例3
{ "query": {"match": {"address": "mill road"}}
}
3.注意:查询的属性值为基本类型时,为精确匹配;查询属性值为字符串时,为全文检索;查询属性值为字符串多个单次时,还会进行分词。最后查询得到的结果会按照相关性得分由高到低的顺序返回。
1.3.2.5 match_phrase短语匹配
1.说明:当match检索查询的属性值为字符串多个单词时,会开启分词检索。而采用match_phrase则将要匹配的值当成一个整体单词(不分词)进行检索
2.举例:
{ "query": {"match_phrase": {"address": "mill road"}}
}
1.3.2.6 multi_match多字段匹配
1.说明:只要指定的属性中包含要查询的值,则该条数据就会被返回。
2.举例:
//state或者address包含mill
{"query": {"multi_match": { "query": "mill","fields": ["state","address"]}}
}
1.3.2.7 bool复合查询
1.基本概念:bool用来做复合查询,复合语句可以合并任何其它查询语句,包括复合语句,了解这一点是很重要的。这就意味着,复合语句之间可以互相嵌套,可以表达非常复杂的逻辑。
2.说明:① must:必须达到must列举的所有条件,must会贡献相关性得分。② should:应该达到should列举的条件,如果达到会增加相关文档的评分,并不会改变查询的结果。但如果query中只有should且只有一种匹配规则,那么should的条件就会被作为默认匹配条件而去改变查询结果。③ must_not必须不是指定的情况。④ filter:和must功能一样,唯一的区别是,它不会贡献相关性得分。
3.举例:
{ "query": {"bool": {"must": [{ "match": { "address": "mill" } },{ "match": { "gender": "M" } }],"should": [{"match": { "address": "lane" }}],"must_not": [{"match": { "email": "baluba.com" }}],"filter": { "range": { "balance": { "gte": 10000,"lte": 20000}}}}}
}
1.3.2.8 term
1.说明:term和match一样,用于匹配某个属性的值。一般来说,全文检索字段用match,其他非text字段匹配(即精确检索)用term。
2.举例:
{ "query": {"bool": {"must": [{"term": { "age": { "value": "28"}}},{"match": { "address": "990 Mill Road"}}]}}
}
1.4 aggregations聚合功能
1.4.1 基本概念
1.4.2 基本语法
"aggs": { "aggs_name 这次聚合的名字,方便展示在结果集中": { "AGG_TYPE 聚合的类型(avg,term,terms)": {聚合的具体字段}
}
1.4.3 举例
1.搜索address中包含mill的所有人的年龄分布以及平均年龄,但不显示这些人的详情。
{"query": {"match": {"address": "mill"}},"aggs": {"group_by_state": {"terms": {"field": "age"}},"avg_age": {"avg": {"field": "age"}}},"size": 0
}
2.按照年龄聚合,并且请求这些年龄段的这些人的平均薪资。
{"query": {"match_all": {}},"aggs": {"age_avg": {"terms": {"field": "age","size": 1000},"aggs": {"banlances_avg": {"avg": {"field": "balance"}}}}},"size": 1000
}
3.查出所有年龄分布,并且这些年龄段中M的平均薪资和F的平均薪资以及这个年龄段的总体平均薪资。
{"query": {"match_all": {}},"aggs": {"age_agg": {"terms": {"field": "age","size": 100},"aggs": {"gender_agg": { "terms": { "field": "gender.keyword","size": 100},"aggs": {"balance_avg": {"avg": {"field": "balance"}}}},"balance_avg":{"avg": {"field": "balance"}}}}},"size": 1000
}
1.5 Mapping映射
1.5.1 基本概念
映射指的就是文档中的每个属性的数据类型,在第一次存放文档时,ES就会为每个文档中的每一个属性猜测一个数据类型。
1.5.2 相关操作
1.5.2.1 创建映射规则
//创建索引并指定映射
PUT /my-index
{"mappings": {"properties": {"age": { "type": "integer" },"email": { "type": "keyword" },"name": { "type": "text" }}}
}
1.5.2.2 添加新字段映射
//在已有索引my-index中添加新属性及其映射
PUT /my-index/_mapping
{"properties": {"employee-id": { "type": "keyword", }}
}
1.5.2.3 映射的更新
对于已经存在的映射字段,我们不能更新,更新必须创建新的索引进行数据迁移。
1.5.2.4 数据迁移
首先明确,在ES6以后,开始逐步移除type(注意,此时的type代表SQL中的表,而不是属性的数据类型)
1.ES6之前的写法
POST _reindex
{"source": {"index": "原索引","type": "类型(代表SQL中的表)"},"dest": {"index": "目的索引"}
}
2.ES6之后的写法
POST _reindex
{"source": {"index": "原索引"},"dest": {"index": "目的索引"}
}
2. 组件使用
2.1 全文检索-数据库数据的检索
2.2 ELK技术栈-日志分析检索
二、nginx
1. nginx搭建域名访问环境
1.1 正向代理与反向代理

1.1.1 正向代理
1.概念:正向代理功能是用于帮助当前主机进行访问外界,当我们的主机无法访问目标服务器时,我们可以选择购买一台正向代理服务器,我们的主机只需要访问代理服务器即可,由代理服务器来代替我们访问目标服务器,并将访问得到的结果返回给我们的主机。
2.功能:由于当前主机对于目标服务器的访问都是通过代理服务器进行的,目标服务器与代理服务器进行交互,获取查询到代理服务器的信息,所以,正向代理可以很好的隐藏客户端信息。
1.1.2 反向代理
1.概念:反向代理功能是用于帮助外界主机访问当前服务器,当外界主机需要访问当前服务器时,可以让主机访问当前服务器所指定的代理服务器,由代理服务器来访问当前服务器,当前服务器会将返回的信息传递给代理服务器并由代理服务器返回给主机。
2.功能:反向代理可以很好的隐藏服务器信息,对外界屏蔽内网服务器信息。此外,反向代理还可以实现负载均衡访问的功能。
1.2 nginx配置文件简介




1.3 nginx的域名访问环境搭建

1.3.1 nginx在虚拟机中
1.3.1.1 修改host文件
在host文件中,将gulimall.com与指定的ip地址进行绑定。
1.3.1.2 修改nginx配置文件
让nginx进行反向代理,将所有来自gulimall.com的请求都转到网关。




1.3.2 nginx在在腾讯云服务器中
1.3.2.1 修改host文件
在host文件中,将gulimall.com与指定的ip地址进行绑定。

1.3.2.2 花生壳进行内网穿透
此时,需要进行内网穿透,让本机的web服务,可以被腾讯云所访问,需要下载一个花生壳,花6块钱进行http认证。得到一个属于本机的,外网可访问的域名。建立一个映射,实现 “外网域名:外网端口”<===>“本机的ip:网关端口” 的绑定关系。

1.3.2.3 修改nginx配置文件
与虚拟机中配置步骤一样,只需要将nginx配置文件中的"虚拟机ip:端口"改为"本机外网域名:外网端口"即可。
1.3.2.3 注意
有坑,一开始配置可以正常使用,但是一旦配置了proxy_set_header,并采用上游服务器作为proxy_pass,再利用gulimall.com进行访问,nginx就会报错404,找不到页面。
1.3.3 nginx在本地windows系统中(使用)
1.3.3.1 修改host文件
在host文件中,将gulimall.com与指定的ip地址进行绑定。

1.3.3.2 下载一个windows版的nginx
具体步骤详见:https://blog.csdn.net/GyaoG/article/details/124081770
1.3.3.3 修改nginx配置文件
与虚拟机中配置步骤一样,此时,由于部署在本地windows系统中,只需要让nginx监听访问域名为gulimall.com的、访问端口为80端口的请求,并将其转到本机的88端口。

2. nginx实现动静分离
2.1 动静分离

由于一开始,所有的静态资源也放在项目的各个模块中,所以,当我们发送首页请求时,无论是查数据库的动态请求,还是申请静态资源的请求都需要交给Tomcat服务器进行处理,这就导致静态请求占用了很大一部分Tomcat的资源,从而导致吞吐量的急剧下降。为了避免这种情况的发生,我们利用nginx进行动静分离,将静态资源从项目的各个模块中分离出去,放在nginx中,并指定哪些访问属于静态资源的请求,此后,当nginx收到动态请求时,才转交给网关进行后续处理,如果收到了静态请求,由nginx直接进行返回,不再需要转交给后台服务。
2.2 实现步骤
2.2.1 移动静态资源
将项目中类路径下的static中的文件全部转移到nginx的html文件夹中自创的static文件夹中。
2.2.2 设置nginx访问规则
在nginx的nginx.conf文件中,添加如下配置,使对nginx的所有以/static/开头的请求,都直接访问nginx文件夹的html/static/中的文件。
2.2.3 修改项目中首页
将首页中所有的超链接前都加上/static/路径。
三、Redis做缓存
1. Redis的整合
1.在gulimall-product模块的pom文件中引入Redis的场景启动器。
org.springframework.boot spring-boot-starter-data-redis
2.在application.yml文件中对Redis进行相关配置。
spring: redis:host: 101.43.199.148port: 6379
3.可以使用springboot自动配置好的StringRedisTemplate来操作Redis。
2. 分布式锁框架Redisson
2.1 Redisson的引入
1.导入依赖
org.redisson redisson 3.12.0
2.配置Redisson
@Configuration
public class MyRedissonConfig {/*** 所有对Redisson的使用都是通过RedissonClient对象* @return*/@Beanpublic RedissonClient redisson(){//1.创建配置Config config = new Config();config.useSingleServer().setAddress("redis://101.43.199.148:6379");//2.根据配置创建RedissonClient实例RedissonClient redissonClient = Redisson.create(config);return redissonClient;}
}
2.2 Redisson的使用
2.2.1 可重入锁(Reentrant Lock)
//第一种加锁方式
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {//1、获取一把锁,只要锁的名字一样,就是同一把锁RLock myLock = redisson.getLock("my-lock");//2、加锁//阻塞式等待。默认加的锁都设置了过期时间是30s//① 锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉。//② 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题。myLock.lock(); try {System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());try {TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) {e.printStackTrace(); }} catch (Exception ex) {ex.printStackTrace();} finally {//3、解锁 假设解锁代码没有运行,Redisson会不会出现死锁System.out.println("释放锁..." + Thread.currentThread().getId());myLock.unlock();}return "hello";
}
//第二种加锁方式:实际上,我们就是用这种方式
@ResponseBody
@GetMapping(value = "/hello")
public String hello() {//1、获取一把锁,只要锁的名字一样,就是同一把锁RLock myLock = redisson.getLock("my-lock");//2、加锁//10秒钟自动解锁,在锁时间到了以后,不会自动续期,因此要求自动解锁时间一定要大于业务执行时间。//原理:① 如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们制定的时间。//原理:② 如果我们未指定锁的超时时间,就使用看门狗的默认时间(lockWatchdogTimeout = 30 * 1000)作为锁的超//时时间,并且只要占锁成功,就会启动一个定时任务,每隔一段时间(默认为看门狗默认时间的三分之一)用来重新设//置锁的过期时间为看门狗的默认时间。最终实现的效果就是,每隔10s,看门狗就重新将过期时间续为30s。myLock.lock(10,TimeUnit.SECONDS); try {System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());try {TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) {e.printStackTrace(); }} catch (Exception ex) {ex.printStackTrace();} finally {//3、解锁 System.out.println("释放锁..." + Thread.currentThread().getId());myLock.unlock();}return "hello";
}
2.2.2 读写锁(ReadWriteLock)
/*** 保证一定能读到最新数据,修改期间,写锁是一个排它锁(互斥锁、独享锁),读锁是一个共享锁* 写锁没释放读锁必须等待* 读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功* 写 + 读 :必须等待写锁释放* 写 + 写 :阻塞方式* 读 + 写 :有读锁。写也需要等待* 只要有写的存在都必须等待* @return*/
@GetMapping(value = "/write")
@ResponseBody
public String writeValue() {String s = "";RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");RLock rLock = readWriteLock.writeLock();try {//改数据加写锁rLock.lock();s = UUID.randomUUID().toString();ValueOperations ops = stringRedisTemplate.opsForValue();ops.set("writeValue",s);TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();} finally {rLock.unlock();}return s;
}@GetMapping(value = "/read")
@ResponseBody
public String readValue() {String s = "";RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");//读数据加读锁RLock rLock = readWriteLock.readLock();try {rLock.lock();ValueOperations ops = stringRedisTemplate.opsForValue();s = ops.get("writeValue");try { TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) { e.printStackTrace(); }} catch (Exception e) {e.printStackTrace();} finally {rLock.unlock();}return s;
}
2.2.3 信号量(Semaphore)
/*** Redis中存储value的值为3*/
@GetMapping(value = "/acquire")
@ResponseBody
public String acquireTest() throws InterruptedException {RSemaphore semaphore= redisson.getSemaphore("value");semaphore.acquire();//阻塞式的获取一个信号,Redis中的value值减1/*** 非阻塞式的获取信号,获取成功返回true,Redis中的value值减1;获取失败返回false,Redis中的value值不变* 据此方式,可以利用信号量来进行限流* boolean flag = semaphore.tryAcquire();* if (flag) {* //执行业务* } else {* return "error";* }*/return "ok==>park";
}@GetMapping(value = "/release")
@ResponseBody
public String releaseTest() {RSemaphore semaphore= redisson.getSemaphore("value");semaphore.release(); //阻塞式的释放一个信号,Redis中的value值加1return "ok==>release";
}
2.2.4 闭锁(CountDownLatch)
/*** 放假、锁门* 1班没人了* 5个班,全部走完,我们才可以锁大门* 分布式闭锁*/@GetMapping(value = "/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {RCountDownLatch door = redisson.getCountDownLatch("door");door.trySetCount(5);//设置闭锁的值是5door.await(); //等待闭锁完成,即计数值减为0return "放假了...";
}@GetMapping(value = "/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id) {RCountDownLatch door = redisson.getCountDownLatch("door");door.countDown(); //闭锁的值计数减1return id + "班的人都走了...";
}
3. 缓存数据一致性问题
由于缓存都是存在Redis中,而数据都是存在MySQL数据库中,所以不可避免的会出现缓存一致性问题,也就是说,在数据库数据更新之后,缓存不会得到及时的更新更新,导致两者的数据不一致,此时如果用户从缓存中读取数据,读取到的实际上是脏数据。
3.1 双写模式
双写模式就是在更新数据库时写缓存,将缓存中的数据也进行更新。双写模式依然存在脏数据的问题,但由于设置了过期时间,总能保证数据的最终一致性。

3.2 失效模式
失效模式就是在更新数据库时删缓存,等待下次用户查询数据时,再将新数据更新到缓存中。失效模式依然存在脏数据的问题,但由于设置了过期时间,总能保证数据的最终一致性。

3.3 总结
无论是双写模式还是失效模式,都会导致缓存的不一致问题,即多个实例同时更新会出现问题,为此,可以有以下几个解决方案:
1.如果是用户维度数据(订单数据、用户数据),这种并发几率非常小,不用考虑这个问题,正常缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。
2.如果是菜单、商品介绍等基础数据,一般允许长时间的数据不一致问题,只需要正常缓存数据加上过期时间,每隔一段时间触发读的主动更新即可(但是,也可以去使用canal订阅binlog的方彻底解决此问题)。
3.缓存数据+过期时间足够解决大部分业务其他对于缓存的要求。
4.某些一致性要求高的场景,可以通过对整个操作(更新数据库+更新缓存或更新数据库+删除缓存)加读写锁来保证并发读写,写写的时候按顺序排好队,读读无需处理。
3.4 高效解决:Canal
Canal是阿里的开源中间件,它的工作原理是伪装成了数据库的从库,来对缓存进行同步更新。比如当数据库为MySQL时,它就伪装成MySQL的从库,随后通过读取数据库的binlog日志,来判定数据是否改变,如果改变则同步更新缓存,保证数据的一致性。

4. SpringCache简化缓存开发
4.1 SpringCache的引入
1.引入SpringCache的场景启动器。
org.springframework.boot spring-boot-starter-cache
2.进行配置。
在application.properties中配置缓存类型。
spring.cache.type=redis
其余的springboot会自动帮我们进行配置,比如CacheAutoConfiguration会根据我们配置文件中设置的缓存类型自动导入RedisCacaheConfiguration,自动配置好缓存管理器RedisCacheManager等。
3.注意:如果Redis没有提前配置,则还需配置Redis。
4.2 SpringCache的使用
4.2.1 前提
在主类上加注解@EnableCaching开启缓存功能。
4.2.2 @Cacheable注解
1.基本使用
@Cacheable注解标注在方法上,表明当前方法的结果需要缓存,如果缓存中有,则方法不调用,如果缓存中没有,则调用方法。可以通过设置其value属性,属性值为数组,来指明缓存的数据要放到那个名字的缓存中。
2. 默认行为
① 如果缓存有,方法不被调用。
② key自动生成,默认为缓存名字::SimpleKey[](自动生成的key值)。
③ 缓存的value值默认使用jdk序列化机制将序列化后的数据保存在Redis中。
④ 默认ttl为-1,即永久有效。
3. 自定义简单配置
① 指定生成缓存使用的key:通过该注解的key属性指定,该属性接收一个SPEL表达式。此时,key会被调整为缓存名字::自定义部分。
② 指定缓存数据的存活时间:在配置文件中指定ttl==>spring.cache.redis.time-to-live=360000,单位为ms。
4. 自定义复杂配置
4.1 原理分析
① CacheAutoConfiguraion自动配置了RedisCacheConfiguration。
② RedisCacheConfiguration自动配置了RedisCacheManager。
③ RedisCacheManager会调用determineConfiguration方法,该方法中会决定每个缓存用什么样的配置。在该方法中,会判断容器中是否有用户自定义的RedisCacheConfiguration,如果有就用用户自定义的,否则,就用默认的配置类(默认的配置类中指定了一部分规则)并在determineConfiguration方法中将默认配置类绑定配置文件中的设置。
④ 因此,想修改缓存的配置,只需要自定义RedisCacheConfiguration并注册到容器中即可,注意,此时也要将自定义的配置类中的配置与配置文件中的配置认为绑定,否则配置文件中的配置会失效。
4.2 开始配置
//由于CacheAutoConfiguration已经添加了注解@EnableConfigurationProperties(CacheProperties.class)
//所以在此无需添加该注解,容器中就有CacheProperties配置类
@EnableCaching
@Configuration
public class MyCacheConfig {// @Autowired
// private CacheProperties cacheProperties;@Beanpublic RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();//认为指定将数据转换为json格式存在缓存中config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));//必须在此添加如下配置,才能将配置文件中的所有配置都生效,否则所有配置文件中的配置都会失效CacheProperties.Redis redisProperties = cacheProperties.getRedis();if (redisProperties.getTimeToLive() != null) {config = config.entryTtl(redisProperties.getTimeToLive());}if (redisProperties.getKeyPrefix() != null) {config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());}if (!redisProperties.isCacheNullValues()) {config = config.disableCachingNullValues();}if (!redisProperties.isUseKeyPrefix()) {config = config.disableKeyPrefix();}return config;}
}
5. 其他配置
# 指定Redis中key的前缀,如果没有就默认用缓存名字::作为前缀
spring.cache.redis.key-prefix=CACHE_
# 指定Redis中key的命名是否使用前缀,默认为true(使用)
spring.cache.redis.use-key-prefix=true
# 指定Redis中是否可以缓存控制,默认为true(可以),可以防止缓存穿透
spring.cache.redis.cache-null-values=true
4.2.3 @CachEvict注解
1. 基本使用
失效模式,标注在更新方法上,value属性指定要删除的缓存的片区,key属性指定要删除的缓存的key,在当进行更新时,会将缓存中的指定value片区的键为key的缓存清除。还可以设置allEntries为true表明更新时,删除指定的value片区的所有缓存内容。
4.2.4 @CachePut注解
1. 基本使用
双写模式,如果修改方法返回的数据正好是修改后最新的数据对象,则可以标注此注解,表明此次修改的结果更新到缓存中。注意,该注解标注的方法必须有返回值。
4.2.5 @Caching注解
1. 基本使用
通过对该注解的属性的设置,可以组合多个SpringCache的注解。比如
@Caching(evict = {@CacheEvict(value = {"category"}, key = "'getLevel1Categorys'"),@CacheEvict(value = {"category"}, key = "'getCatalogJson'")})
4.3 SpringCache总结
4.3.1 读模式
使用缓存的读模式中,一共存在三个问题:缓存穿透、缓存击穿、缓存雪崩。
① 缓存穿透:查询一个数据库不存在的数据,SpringCache采用cache-null-value=true解决。
② 缓存击穿:大量并发进来同时查询一个正好过期的数据,SpringCache可以通过设置@Cachable注解的属性sync=true解决(注意,加此注解时,读取数据的方法会加上本地锁,虽然不如分布式锁完美,但是也可以解决这个问题)。
③ 缓存雪崩:大量key同时过期,SpringCache为每一个key加上过期时间就可以解决了(由于每个key存入缓存的时间不同,所以加上过期时间就可以保证数据失效的时间离散形式,也没必要加随机时间,容易弄巧成拙)。
4.3.2 写模式
写模式中,SpringCache可以通过@CachEvict和@CachePut注解来片面地解决缓存一致性问题,之所以说片面,是因为仍然只能保证数据的最终一致性,如果需要更高标准的一致性,则还需要采用其他手段,比如加入读写锁、引入Canal、或者在读多写多的情况下直接去数据库查询。
4.3.3 总结
常规数据一般读多写少,即时性和一致性要求不高,因此完全可以使用SpringCache。
特殊数据要进行特殊设计。
5. 缓存小总结
1.自己使用StringRedisTemplate引入了缓存。
2.自己使用StringRedisTemplate实现了本地锁和分布式锁解决读模式的缓存击穿问题。
3.自己使用Redisson实现了分布式锁解决读模式的缓存击穿问题。
4.利用SpringCache的注解来使用缓存,并解决读模式下的缓存击穿问题。
5.利用SpringCache的注解来初步解决写模式下的缓存一致性问题(最终一致性)。
6.从头到尾,没有真正的利用读写锁完全解决缓存一致性问题。
四、注册登录
1. 验证码
2. 社交登录
2.1 OAuth基本概念
- OAuth:OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容。
- OAuth2.0:对于用户相关的OpenAPI(例如获取用户信息,动态同步,照片,日志,分享等),为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。
2.2 OAuth2.0的社交登录流程

1.当重新获取了新的Code以后,原来的Code会失效。
2.同一个用户的AccessToken一段时间内是不会变化的,过期时间内多次获取则会获取相同的值。
2.3 微博社交登录
2.3.1 微博授权机制说明
https://open.weibo.com/wiki/%E6%8E%88%E6%9D%83%E6%9C%BA%E5%88%B6
2.3.2 微博登录流程解析

3. 分布式Session
3.1 Session的工作原理

3.2 Session缺陷及解决

3.2.1 同一微服务Session共享问题
3.2.1.1 问题描述
当果我们对于同一个服务进行集群搭建后,多个服务器会共同组成一个服务模块,由于Session是存在于每一个服务器的内存中,这会导致用户保存在单个服务器的信息无法共享到同样从属于该服务模块的其他服务器上。
3.2.1.2 解决方案
-
方案一:session复制

-
方案二:客户端存储

-
方案三:Hash一致性

-
方案四:统一存储(采用)

3.2.2 不同同微服务Session共享
3.2.2.1 问题描述
在分布式情况下,一个完整的服务系统是由多个不同的微服务模块组成的,不同的微服务模块父域名相同但子域名是不同的,由于Session是依赖于Cookie实现的,而Cookie是针对域名进行存储(即,只有域名相同才可以共享同一个Cookie),这就导致不同的微服务之间不能共享用户保存的信息。
3.2.2.2 解决方案(采用)
人为设置Cookie信息,扩大Session所依赖的Cookie的作用范围。
3.3 整合SpringSession解决问题
3.3.1 同一微服务Session共享问题
1.在需要Session存储的微服务的pom文件中引入依赖
org.springframework.session spring-session-data-redis
2.在配置文件中配置Session所用数据库类型和数据库设置以及一些其他配置(本次使用redis)
# 配置redis
spring.redis.host=自己的服务器
spring.redis.port=6379
# 配置session
spring.session.store-type=redis
# session的其他相关配置
spring.session.timeout=30m
3.在主类上标注注解@EnableRedisHttpSession(以整合redis作为Session存储为例)
4.注意:如果不配置使用默认的JDK序列化机制,则需要实现序列化接口Serializable
3.3.2 不同同微服务Session共享
通过自定义CookieSerializer来人为设置Cookie的作用域
@Configuration
public class GilimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}
}
3.3.3 附加:采用JSON序列化机制
通过设置RedisSerializer来人为指定序列化机制
@Configuration
public class GilimallSessionConfig {@Beanpublic CookieSerializer cookieSerializer(){DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();cookieSerializer.setDomainName("gulimall.com");cookieSerializer.setCookieName("GULISESSION");return cookieSerializer;}@Beanpublic RedisSerializer
3.3 SpringSession的核心原理(装饰者模式)
1.@EnableRedisHttpSession注解中利用@Import(RedisHttpSessionConfiguration.class)导入了RedisHttpSessionConfiguration类。
2.RedisHttpSessionConfiguration类给容器中注入了RedisIndexedSessionRepository组件,该组件就是利用redis操作session,其内部封装了redis对session的增删改查操作。
3.RedisHttpSessionConfiguration类中继承了SpringHttpSessionConfiguration类,该类首先在构造之前指定使用的CookieSerializer ,如果未曾认为设定则使用默认的CookieSerializer ;其次,该类为容器中注册了SessionRepositoryFilter组件。
4.SessionRepositoryFilter组件是一个Filter,每个请求过来都需要经过Filter。该组件首先在创建时就自动在容器中获取RedisHttpSessionConfiguration;其次,又重写了doFilterInternal方法,在doFilterInternal方法中,对原生的request和response进行了包装,因此,以后通过request获取session实际上都是通过包装后的request获取的session,包装后的request获取的seesion是通过RedisIndexedSessionRepository获取的,RedisIndexedSessionRepository组件是我们自行注入的。

4. 单点登录
4.1 概述

单点登录可以简单的概括成一句话:一处登录,处处登录。即,当我们在某个大型公司的某个项目注册并登录之后,再次访问同属该公司的其他项目时,无需重新注册登录即可实现自动登录。单点登录的功能可以通过认证中心来完成。
4.2 实现
4.2.1 实现前提
浏览器、网站1client1、网站2client2、登录中心ssoserver。
4.2.2 实现步骤
1.浏览器访问client1的指定网址http://client1.com:8081/employees。
2.client1判断是否已经登陆,判定规则为:① 如果参数有token,则再次访问登陆中心,根据token查询用户并放在session中,判定为登录; ② session中有,判定为登录。如果已登录可以直接访问,否则命令浏览器重定向到新的位置:http://ssoserver.com:8080/login.html?redirect_url=http://client1.com:8081/employees。
注意,此时由于登陆成功还需要从登陆中心重定向回来,所以需要将自身的地址作为参数一起传递给登录中心。
3.浏览器访问登录中心的登录页http://ssoserver.com:8080/login.html?redirect_url=http://client1.com:8081/employees,此时登陆中心通过Cookie中的是否含有sso_token判断是否登陆过,若登陆过则将sso_token拼接到重定向的连接中,随后直接命令浏览器重定向到访问页面(http://client1.com:8081/employees?token=sso_token),否则展示登陆页面(此时,redirect_url存放在Model中,可以由前端页面取出进行下次传递)。
4.浏览器展示登录中心的登录页。
5.用户在浏览器输入账号密码并提交给登录中心(http://ssoserver.com:8080/doLogin)进行登录。此时,需要携带的参数包括用户名、密码、和上面的redirect_url=http://client1.com:8081/employees。
6.登录中心处理登录请求,登陆成功,则将登录成功的用户存在redis中并将生成用户相关的token,将其存放在redisect_url的重定向路径中(http://client1.com:8081/employees?token=uuid),命令浏览器重定向到client1(http://client1.com:8081/employees?token=uuid)。同时,登陆中心ssoserver的域名下保存一个Cookie(sso_token)来存放该token信息来关联该用户来表明该用户已经登陆过此网页(此后,该浏览器在访问此网址时就会带上Cookie)。
7.浏览器访问client1(http://client1.com:8081/employees?uuid),走2的流程。
8.浏览器访问client2的指定网址(http://client2.com:8082/boss),client2和client1的逻辑相同,走2的流程,即由于未登录,则命令浏览器重定向到http://ssoserver.com:8080/login.html?redirect_url=http://client2.com:8082/boss,随后浏览器访问该网址。
9.走3的流程,即由于client1已经登陆过了,所以此时浏览器中存在指定的Cookie(sso_token),将sso_token拼接到重定向的连接中,随后直接命令浏览器重定向到访问页面(http://client2.com:8082/boss?token=sso_token),接着,浏览器重新访问(http://client2.com:8082/boss?token=sso_token)。
10.走2的流程,由于已经登陆过了,所以访问登陆中心,根据token查询用户并放在session中,判定为登录,直接展示被保护页面http://client2.com:8082/boss。
4.2.3 要点分析
1.给登陆中心留下登录痕迹(也就是Cookie)证明已经登陆过该登陆中心了。
2.登录中心要将用户关联的token信息拼接在重定向连接中,保证token的传递。
3.其它系统在接收到token信息之后要进行处理,将token对应的用户信息保存在session中。
4.2.4 现成框架
通用的单点登录框架可参考gittee链接:https://gitee.com/xuxueli0323/xxl-sso?_from=gitee_search。
五、Seata分布式事务
1. 本地事务
1.1 本地事务相关基础知识
1.1.1 本地事务基本概念
- 多个业务使用同一条连接操作同一个数据库的整个过程,可以理解为是一个本地事务。
1.1.2 @Transactional注解
1.1.2.1 说明
@Transactional注解实际上就是开启了一个本地事务,该注解只能保证本地事务的回滚,分布式情况下会出现问题。
1.1.2.2 传播行为
可以通过设置@Transactional的属性propagation来设置事事务传播行为。
| 传播行为 | 说明 |
|---|---|
| PROPAGATION_REQUIRED | 如果当前没有事务,就创建一个新的事务,如果当前存在事务,就加入该事务。 |
| PROPAGATION_REQUIRED_NEW | 无论当前存不存在事务,都创建新的事务。 |
假设方法A中调用了方法B和方法C,B的传播行为设置为PROPAGATION_REQUIRED,C的传播行为设置为PROPAGATION_REQUIRED_NEW,则A和B在同一个事务中,C在一个独立的事务中,换句话说,如果A或B中发生异常,则A和B同时回滚,C不受影响。
注意,此时由于A和B共用一个事务,所以B的其他事务设置(比如超时时间等)都一定会与A相同,人为设置会失效,C的设置不受影响。
1.1.3 SpringBoot中的本地事务
SpringBoot中同一个对象事务方法互调默认失效。假设在方法A、B、C都在同一个Service中,此时,方法A中调用了方法B和方法C,此时方法B和C无论设置什么传播行为,B和C的所有设置全部失效,B和C都会和A共用同一个事务。这是由于Spring中的事务是用代理对象来控制的,同一个Service共用同一个代理对象。
1.2 本地事务在分布式下的问题
- 远程服务假失败:远程服务其实成功了,但由于出现网络故障等原因没有返回,最终导致订单回滚,库存却扣减。
- 远程服务执行完成,但后续操作出现问题,最终导致订单回滚,库存却扣减。
2. 分布式事务
2.1 分布式事务定理
2.1.1 CAP定理
CAP定理可以概括为以下三点:
- 一致性(Consistency):在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本)。
- 可用性(Availability):在集群中一部分节点故障后,集群整体是否还能响应客户端的读写请求。(对数据更新具备高可用性)。
- 分区容错性(Partition tolerance):大多数分布式系统都分布在多个子网络。每个子网络就叫做一个区(partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。
CAP原则指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。一般来说,分区容错无法避免,因此可以认为CAP的P总是成立。CAP 定理告诉我们,剩下的C和A无法同时做到。
2.2.2 BASE理论
对于多数大型互联网应用的场景,主机众多、部署分散,而且现在的集群规模越来越大,所以节点故障、网络故障是常态,在这种情况下,我们要保证服务可用性达到99.99999%(N 个 9),即保证P和A,舍弃C,但一致性也是需要进行一定的保障的,因此提出了BASE理论。BASE理论是对CAP理论的延伸,思想是尽管无法做到强一致性(CAP的一致性就是强一致性),但可以采用适当的采取弱一致性,即最终一致性。BASE理论可以概括为以下三点:
- 基本可用(Basically Available):基本可用是指分布式系统在出现故障的时候,允许损失部分可用性(例如响应时间、功能上的可用性),允许损失部分可用性。响应时间上的损失指的是正常情况下搜索引擎需要在0.5秒之内返回给用户相应的查询结果,但由于出现故障(比如系统部分机房发生断电或断网故障),查询结果的响应时间增加到了 1~2 秒。功能上的损失指的是购物网站在购物高峰(如双十一)时,为了保护系统的稳定性,部分消费者可能会被引导到一个降级页面。
- 软状态( Soft State):软状态是指允许系统存在中间状态,而该中间状态不会影响系统整体可用性。分布式存储中一般一份数据会有多个副本,允许不同副本同步的延时就是软状态的体现。
- 最终一致性( Eventual Consistency):最终一致性是指系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。弱一致性和强一致性相反,最终一致性是弱一致性的一种特殊情况。
2.2.3 三种一致性
从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。
- 强一致性:要求更新过的数据能被后续的访问看到。
- 弱一致性:更新过的数据可以容忍后续的访问看不到。
- 最终一致性:要求经过一段时间后能访问到更新后的数据,最终一致性是弱一致性的一种特殊情况。
2.3 分布式事务的解决方案
2.3.1 刚性事务(ACID)- 2PC模式
2PC(2 phase commit 二阶提交),又叫做XA Transactions。其中,XA 是一个两阶段提交协议,该协议分为以下两个阶段:

第一阶段:事务协调器要求每个涉及到事务的数据库预提交(precommit)此操作,并反映是否可以提交。
第二阶段:事务协调器要求每个数据库提交数据。其中,如果有任何一个数据库否决此次提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。
2.3.2 柔性事务(BASE)- TCC事务补偿型方案
所谓TCC模式,就是指支持把自定义的分支事务纳入到全局事务的管理中。


一阶段prepare行为:调用自定义的prepare逻辑。
二阶段commit行为:调用自定义的commit逻辑。
二阶段rollback行为:调用自定义的rollback逻辑。
2.3.3 柔性事务(BASE)- 最大努力通知型方案
按规律进行通知,不保证数据一定能通知成功,但会提供可查询操作接口进行核对。这种
方案主要用在与第三方系统通讯时,比如:调用微信或支付宝支付后的支付结果通知。这种方案也是结合MQ进行实现,例如:通过MQ发送http请求,设置最大通知次数。达到通
知次数后即不再通知。比如:银行通知、商户通知等(各大交易业务平台间的商户通知:多次通知、查询校对、对账文件),支付宝的支付成功异步回调。
2.3.4 柔性事务(BASE)- 可靠消息 + 最终一致性方案
业务处理服务在业务事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不是真正的发送。业务处理服务在业务事务提交之后,向实时消息服务确认发送。只有在得到确认发送指令后,实时消息服务才会真正发送。
3. Seata
3.1 Seata的基本介绍
Seata是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata将为用户提供了AT、TCC、SAGA和XA事务模式,为用户打造一站式的分布式解决方案。

TC (Transaction Coordinator) - 事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚。
TM (Transaction Manager) - 事务管理器:定义全局事务的范围,开始全局事务、提交或回滚全局事务。
RM (Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
3.2 Seata的环境准备
3.2.1 建立UNDO_LOG表
对于每一个微服务,都必须创建一个undo_log表
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`branch_id` bigint(20) NOT NULL,`xid` varchar(100) NOT NULL,`context` varchar(128) NOT NULL,`rollback_info` longblob NOT NULL,`log_status` int(11) NOT NULL,`log_created` datetime NOT NULL,`log_modified` datetime NOT NULL,`ext` varchar(100) DEFAULT NULL,PRIMARY KEY (`id`),UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
3.2.2 下载安装Seata
https://github.com/seata/seata/releases,找到0.7.0版本的Seata下载安装。
3.3 Seata的使用(默认为AT模式)
3.3.1 导入Seata的依赖
com.alibaba.cloud spring-cloud-starter-alibaba-seata
3.3.2 修改配置文件
3.2.1 registry.conf
- 修改registry的type为nacos,并指定nacos的setverAddr为localhost:8848,来人为指定Seata需要注册到哪个注册中心。
- config内容暂不修改,就让配置都采用file.conf的配置即可。
3.2.2 file.conf
- store中制定了事务日志存储的位置,不修改,默认存放在sessionStore中即可。
3.3.3 注入DataSourceProxy
package com.atguigu.gulimall.order.config;import com.zaxxer.hikari.HikariDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;import javax.sql.DataSource;/*
@author zqn
@create 2022-07-08 16:46
*/
@Configuration
public class MySeataConfig {@Autowiredprivate DataSourceProperties dataSourceProperties;@Beanpublic DataSource dataSource(DataSourceProperties dataSourceProperties){HikariDataSource dataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();if(StringUtils.hasText(dataSourceProperties.getName())){dataSource.setPoolName(dataSourceProperties.getName());}return new DataSourceProxy(dataSource);}
}
3.3.4 复制配置文件
将registry.conf和file.conf复制到每个微服务的类路径下(resources)。
3.3.5 修改配置文件
- 将每个微服务下的file.conf文件中的vgroup_mapping.my_test_tx_group更改为vgroup_mapping.{application.name}-fescar-service-group。
- 也可以选择在配置文件application.properties中设置spring.cloud.alibaba.seata.tx-service-group属性。
3.3.6 @GlobalTransactional
- 给分布式大事务的入口标注注解@GlobalTransactional开启全局事务,每一个远程的小事务只需要标注注解@Transactional即可。
六、RabbitMQ
1. MQ基础知识
1.1 使用场景
1.1.1 异步处理

1.1.2 应用解耦

1.1.3 流量控制

1.2 基础知识
1.2.1 基本概念
1.2.1.1 消息代理(message broker)和目的地(destination)
当消息发送者发送消息以后,将由消息代理接管,消息代理保证消息传递到指定目的地。
1.2.1.2 消息队列主要有两种形式的目的地
- 队列(queue):点对点消息通信(point-to-point),消息发送者发送消息,消息代理将其放入一个队列中,消息接收者从队列中获取消息内容,消息读取后被移出队列。消息只有唯一的发送者和接受者,但并不是说只能有一个接收者(可以很多个接收者监听队列,但最终只有一个接受者接受消息)。
- 主题(topic):发布(publish)/订阅(subscribe)消息通信,发送者(发布者)发送消息到主题,多个接收者(订阅者)监听(订阅)这个主题,那么就会在消息到达时同时收到消息。
1.2.1.3 两种规范
- JMS(Java Message Service)JAVA消息服务:基于JVM消息代理的规范。ActiveMQ、HornetMQ是JMS实现。
- AMQP(Advanced Message Queuing Protocol):高级消息队列协议,也是一个消息代理的规范,兼容JMS,RabbitMQ是AMQP的实现。
- 对比:如下图

2. RabbitMQ基本概念
2.1 RabbitMQ简介
RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。
2.2 核心概念


2.2.1 Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优权)、delivery-mode(指出该消息可能需要持久性存储)等。
2.2.2 Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
2.2.3 Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。Exchange有4种类型:direct(默认)、fanout、topic和headers,不同类型的Exchange转发消息的策略有所区别。
2.2.4 Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将其取走。
2.2.5 Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。Exchange 和Queue的绑定可以是多对多的关系。
2.2.6 Connection
网络连接,比如一个TCP连接。
2.2.7 Channel
信道,多个信道共用一个连接。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。因为对于操作系统来说建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接。
2.2.8 Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
2.2.9 Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换器、绑定和权限机制。vhost是AMQP概念的基础,必须在连接时指定,RabbitMQ默认的 vhost是 / 。
2.2.10 Broker
表示消息队列服务器实体.
2.3 Docker下的安装
2.3.1 安装流程
在docker容器中执行如下指令即可
docker run -d --name rabbitmq -p 5671:5671 -p 5672:5672 -p 4369:4369 -p
25672:25672 -p 15671:15671 -p 15672:15672 rabbitmq:management
2.3.2 端口介绍
- 4369、25672(Erlang发现&集群端口)
- 5672、 5671(AMQP端口)
- 15672(web管理后台端口):访问此端口即可访问图形化界面,默认初始账号密码都尉guest
- 61613、61614(STOMP协议端口)
- 1883、8883(MQTT协议端口)
2.4 RabbitMQ的交换机Exchange

生产者把消息发布到Exchang上,消息最终到达队列并被消费者接收,消息头中的route-key和Binding共同决定了交换器的消息应该发送到那个队列。Exchange分发消息时根据类型的不同分发策略有区别,目前共四种类型:direct、fanout、topic、headers。direct和headers都是点对点的实现,fanout和topic是发布订阅的实现。headers由于功能和direct相同但性能差很多,所以几乎用不到了,故在此不说。
2.4.1 direct

消息中的路由键(routing key)如果和Binding中的binding key一致,交换器就将消息发到对应的队列中。路由键与队列名完全匹配,如果一个队列绑定到交换机要求路由键为“dog”,则只转发routingkey 标记为“dog”的消息,不会转发“dog.puppy”,也不会转发“dog.guard” 等等。它是完全匹配、单播的模式。(如果多个routingkey为"dog",注意是多个接收者,一个接受者)
2.4.2 fanout

每个发到fanout类型交换器的消息都会分到所有绑定的队列上去。fanout交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。fanout类型转发消息是最快的。
2.4.3 topic

topic交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。它同样也会识别两个通配符:符号“#”和符号“*”。#匹配0个或多个单词,*匹配一个单词。
2.5 RabbitMQ的整合
- 引入spring-boot-starter-amqp场景启动器,RabbitAutoConfiguration就会自动生效,此时会自动为容器中配置RabbitTemplate、AmqpAdmin、CachingConnectionFactory、RabbitMessagingTemplate。
- 在主类或配置类上标注注解@EnableRabbit,开启功能。
- 在配置文件applicaton.properties中进行配置,以spring.rabbitmq为前缀
spring.rabbitmq.host=主机地址 spring.rabbitmq.port=5672 spring.rabbitmq.virtual-host=/
2.6 RabbitMQ的具体使用
2.6.1 AmqpAdmin创建交换机、队列、绑定
@Test
public void createExchange() {Exchange directExchange = new DirectExchange("hello-java-exchange",true,false);amqpAdmin.declareExchange(directExchange);log.info("Exchange[{}]创建成功:","hello-java-exchange");
}@Test
public void testCreateQueue() {Queue queue = new Queue("hello-java-queue",true,false,false);amqpAdmin.declareQueue(queue);log.info("Queue[{}]创建成功:","hello-java-queue");
}@Test
public void createBinding() {Binding binding = new Binding("hello-java-queue",Binding.DestinationType.QUEUE,"hello-java-exchange","hello.java",null);amqpAdmin.declareBinding(binding);log.info("Binding[{}]创建成功:","hello-java-binding");
}
2.6.2 RabbitTemplate收发消息
@Test
public void sendMessageTest() {//1.使用convertAndSend发送消息String msg = "hello world";rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",msg);//2.如果发送的消息是个对象,会使用序列化机制,将对象写出去,因此对象必须实现Serializable接口OrderReturnReasonEntity reasonEntity = new OrderReturnReasonEntity();reasonEntity.setId(1L);reasonEntity.setCreateTime(new Date());reasonEntity.setName("reason");reasonEntity.setStatus(1);reasonEntity.setSort(2);rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",reasonEntity);//3.发送的对象类型的消息,也可以是一个json,需要自定义一个消息转化器rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",reasonEntity);
}@Configuration
public class MyRabbitConfig {@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}
}
2.6.3 @RabbitListener监听队列
2.6.3.1 前提:@RabbitListener必须开启@EnableRabbit
2.6.3.2 标注在方法上
- @RabbitListener标注在方法上,在该注解的queues属性指明需要监听的队列,该属性是一个数组,表明可以监听多个队列。
- 在方法形参中设置Object类型的参数,即可以接收队列中的消息,该消息的类型实际是
org.springframework.amqp.core.Message。 - 在方法形参中设置Message类型的参数,即可接收队列中的消息,Message中包含原生消息的详细信息(头+体)。
- 在方法形参中设置发送的消息的类型的参数,即可接收队列中的消息。
- 在方法形参中设置Channel类型的参数可以获取当前传输数据的通道。
@RabbitListener(queues = {"hello-java-queue"}) public void revieveMessage(Message message,OrderReturnReasonEntity content,Channel channel) {//拿到主体内容byte[] body = message.getBody();//拿到的消息头属性信息MessageProperties messageProperties = message.getMessageProperties();System.out.println("接受到的消息...内容" + message + "===>内容:" + content); }
2.6.3.3 标注在类上(常用)
和@RabbitHandler搭配使用,此时@RabbitListener表明此类可以接收哪个队列的消息(监听哪些队列)。这样做的好处是,可以便捷的利用不同的方法接收队列中不同类型的消息并进行处理。
2.6.4 @RabbitHandler监听队列
2.6.4.1 前提:@RabbitHandler必须开启@EnableRabbit
2.6.3.2 标注在方法上
和@RabbitListener搭配使用,此时@RabbitHandler表明类中的哪个方法可以接收队列的消息(重载区分不同类型的消息)。这样做的好处是,可以便捷的利用不同的方法接收队列中不同类型的消息并进行处理。
2.7 RabbitMQ的消息确认机制
2.7.1 概述

为了在实际生产环境中保证消息不丢失,可靠抵达,我们可以选择使用事务消息,但这种方式会导致性能下降250倍,为此我们选择引入确认机制来保证可靠性。消息确认机制代替可以分为两个部分,分别是发送端确认和消费端确认。发送端确认分为publisher confirmCallback确认模式和publisher returnCallback未投递到队列退回模式两种。消费端确认则采用ack机制。
2.7.2 发送端确认
2.7.2.1 confirmaCallback
1.说明
① 消息只要被broker接收到就会执行confirmCallback,如果是cluster模式,需要所有broker接收到才会调用confirmCallback。
② 被broker接收到只能表示message已经到达服务器,并不能保证消息一定会被投递到目标queue里。所以需要用到接下来的returnCallback。
2.流程
① 在applicaton.properties中开启配置。
# 开启发送端确认
spring.rabbitmq.publisher-confirms=true
# 也可以选择在创建connectionFactory的时候设置PublisherConfirms(true) 选项,开启confirmcallback 。
② 在配置类中定制RabbitTemplate,设置确认回调,服务器收到消息就会回调。
@Configuration
public class MyRabbitConfig {@AutowiredRabbitTemplate rabbitTemplate;@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}/*** 定制RabbitTemplate*/@PostConstruct//在对象创建完成后执行该方法,设置该方法public void initRabbitTemplate(){rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {/*** 设置确认回调,服务器收到消息就会毁回调* @param correlationData 当前消息的唯一关联数据(消息的唯一id,需要在发送消息时设置,convertAndSend的最后一个参数可以设置为这个)* @param ack 消息是否成功收到* @param cause 失败的原因*/@Overridepublic void confirm(CorrelationData correlationData, boolean ack, String cause) {}});}
}
2.7.2.2 returnCallback
1.说明
如果未能投递到目标queue里将调用returnCallback,可以记录下详细投递数据,定期的巡检或者自动纠错都需要这些数据。
2.流程
① 在applicaton.properties中开启配置。
# 开启发送确认(抵达队列)
spring.rabbitmq.publisher-returns=true
# 以异步方式优先回调returns(可以不设置)
spring.rabbitmq.template.mandatory=true
② 在配置类中定制RabbitTemplate,设置抵达队列确认回调,消息未能成功抵达队列就会回调。
@Configuration
public class MyRabbitConfig {@AutowiredRabbitTemplate rabbitTemplate;@Beanpublic MessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}/*** 定制RabbitTemplate*/@PostConstruct//在对象创建完成后执行该方法,设置该方法public void initRabbitTemplate(){rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {/*** 设置确认回调,服务器收到消息就会回调* @param correlationData 当前消息的唯一关联数据(消息的唯一id,需要在发送消息时设置,convertAndSend的最后一个参数可以设置为这个)* @param ack 消息是否成功收到* @param cause 失败的原因*/@Overridepublic void confirm(CorrelationData correlationData, boolean ack, String cause) {}});rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {/*** 只要消息没有投递给指定的队列,就触发这个失败回调* @param message 投递失败的消息详细信息* @param replyCode 回复的状态码* @param replyText 回复的文本内容* @param exchange 消息发送给哪个交换机* @param routingKey 消息的路由键*/@Overridepublic void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {}});}
}
2.7.3 消费端确认
1.说明
RabbitMQ默认会在消费端自动ack,即消息被消费者收到,就会进行确认签收,让服务端将消息从broker的queue中移除。需要人为设置为手动ack方式,并在消息被消费者消费后,再进行确认签收,最终从队列中删除消息(当然,也可以拒绝,下面详细说明)。
2.流程
① 在配置文件application.properties中进行配置
# 消费端确认,手动确认消息
spring.rabbitmq.listener.direct.acknowledge-mode=manual
② 在消费端接收方法中,手动调用Channel的方法进行确认签收,如果确认签收,则broker就会删除该消息,而只要未确认签收,消息就一直是uncaked状态,此时消费端停止服务,则消息变为ready状态,等到下次有新的消费端重新接入,就会再次发送这些消息。
//delivreyTag可以理解为消息的标识,在通道内自增;
//multiple表示是是否为批量确认签收,true则是,false则不是;
channel,basicAck(long delivreyTag, boolean multiple);
③ 在消费端接收方法中,手动调用Channel的方法进行确认拒绝签收。
//delivreyTag可以理解为消息的标识,在通道内自增;
//multiple表示是是否为批量确认拒绝签收,true则是,false则不是;
//requeue表示是被拒绝签收的消息是否重新入队,true则重新入队,broker中依旧存在此消息,false则不入队直接丢弃,broker中删除此消息;
channel.basicNack(long delivreyTag, boolean multiple, boolean requeue);
channel.basicReject(long delivreyTag, boolean requeue);
④ 在消费端接收方法中,没有调用ack/nack方法,则broker认为此消息正在被处理,不会投递给别人,此时客户端断开,消息不会被broker移除,而会投递给别人(其实和确认拒绝签收时requeue参数设置为true一个效果)。
2.8 RabbitMQ的延时队列
2.8.1 死信
2.8.1.1 TTL
- 消息的TTL就是消息的存活时间。
- RabbitMQ可以对队列和消息分别设置TTL,超过了这个时间,我们认为这个消息就死了,称之为死信。
① 对队列设置就是队列没有消费者连着的保留时间。
② 也可以对每一个单独的消息做单独的设置。 - 如果队列设置了,消息也设置了,那么会取小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。
2.8.1.2 Dead Letter Exchanges(DLX)
- 一个消息在满足如下条件下,会进死信路由,记住这里是路由而不是队列,一个路由可以对应很多队列。
① 一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。(basic.reject/ basic.nack)requeue=false。
② 上面的消息的TTL到了,消息过期了。
③ 队列的长度限制满了,排在前面的消息会被丢弃或者扔到死信路由上。 - Dead Letter Exchange其实就是一种普通的exchange,和创建其他exchange没有两样。只是在某一个设置Dead Letter Exchange的队列中有消息过期了,会自动触发消息的转发,发送到Dead Letter Exchange中去。
- 我们既可以控制消息在一段时间后变成死信,又可以控制变成死信的消息被路由到某一个指定的交换机,结合二者,其实就可以实现一个延时队列。
2.8.2 延时




本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
