当前位置:   article > 正文

Java经验速记_idea docker插件

idea docker插件

一、Git

1、Git的作用

记录内容的变化,以便将来查阅特定版本修订情况,版本回溯

2、Git和SVN的区别

Git是分布式版本控制系统,保存的是每个版本的索引(可直接回退到任意版本),客户端不只是提取最新版本的文件快照,而是把代码仓库完整的镜像下来(这里面运用了压缩算法,相较SVN所占内存只多一点点)

CVS/SVN是集中化版本控制系统,有一个单一的集中管理的服务器,服务器中存放着所有的历史版本(保存的是相邻两个版本的差异,回退的效率非常低)和当前版本,而客户端只拉取当前最新版本,缺点:单点故障,服务器宕机,开发人员无法提交更新,也就无法协同工作

3、Git的结构

本地Git结构

工作区->git add->暂存区->git commit->本地库

推送远程GitHub流程

本地库->git merge->本地库master分支->git pull->git push->远程仓库master分支或其他分支

托管中心的类型

局域网环境下,搭建gitlab服务器作为代码托管中心

外网环境下,可以由GitHub(国外)或Gitee(国内)作为代码托管中心,不用自己搭建

4、Git命令

信息相关命令

brew install git
# 安装Git
 
git --version
# git version 2.30.1 (Apple Git-130)
 
which git
# /usr/local/bin/git
# 查看git的安装目录

# 查看用户名以及邮箱
git config user.name
git config user.email

# 设置/更改用户名和邮箱
git config --global user.name "your name"
git config --global user.email "your email"

# 查看Git add 和 commit的文件状态
git status

# 查看提交时备注的信息
git log --stat

# 查看提交的从最近到最远的日志
# 有以下3种方式
git log
git log --oneline
git reflog

# 前进或后退历史版本(会同时重置本地库、暂存区、工作区) ID索引由git reflog获取
git reset --hard ID索引

# (了解)前进或后退历史版本(会重置本地库、暂存区,保留工作区) ID索引由git reflog获取
git reset --mixed ID索引
# 前进或后退历史版本(会重置本地库,保留暂存区、工作区) ID索引由git reflog获取
git reset --soft ID索引

# 将工作区和暂存区的指定文件进行比较
git diff 文件名
# 将工作区和暂存区的所有文件进行比较
git diff

# 比较工作区和本地仓库中差别
git diff ID索引 文件名
  • 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

代码相关命令

# 首先需要初始化一个本地仓库,在任意位置新建一个文件夹
cd 到该文件夹的绝对路径

git init
# 初始化本地仓库,会生成一个.git的文件夹

git add -A
# 将所有的文件提交到暂存区
git add "文件名"
# 将指定的文件提交到暂存区
 
git commit -m "commit备注信息"
# 提交到本地仓库

git checkout master
git merge a
# 在主分支下合并a分支(首先需要先切换到主分支,表示以主分支为基准,合并其他分支的修改)

# 如果有冲突,可以选择处理冲突文件,修改冲突的内容(此时主分支上的冲突文件自动加载了分支上不同的内容),再次add commit(此时就不再是合并状态了,合并成功)
git merge --abort
# 如果有冲突,也可以先放弃merge,请求协助

# 回到本地仓库分支,我们会发现内容还是原本提交的有冲突的内容,因此在此分支上继续开发前需要将本地仓库主分支的代码拉取到分支

git pull
# 拉取远程仓库的代码

git push origin master
# 推送到远程仓库
# fatal: unable to access 'https://github.com/xxx/myRepository.git/': HTTP/2 stream 1 was not closed cleanly before end of the underlying stream
# 对于上述报错的解决方案(将默认通信协议修改为 http/1.1)
git config --global http.version HTTP/1.1
  • 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

5、Git分支

在版本控制的过程中,使用多条分支同时推进多个任务,新的独立的功能开辟一个新的分支

注意:切换不同分支后,在本地仓库的内容是相互独立的,内容随着分支的切换而改变

优点

并行开发,互不影响,开发效率高,防止代码污染

分支相关命令

# 查看分支
# 以下2种
git branch -v
git branch

# 创建分支
git branch 分支名

# 切换分支
git checkout 分支名
# 切换到主分支
git checkout main/master

# 创建并切换分支,在此分支进行代码修改并提交不影响主分支及其他分支
git checkout -b 分支名

# 删除a分支
git branch -D a
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

什么情况需要使用分支

  1. 在本地使用分支

    本来在master分支上开发的,如果我每实现一个小的功能,就进行一次commit的话?那么分支上不就有很多的commit的吗?推送上去,您会看见服务器上有很多不必要的提交,这样子就不简洁了,版本历史也不清楚.但是使用分支,完成一个完整的功能,然后主分支使用 git merge --squash branchName 合并分支,做一个整的提交推送,那么服务器上的历史只有这一个commit的了,这不就简洁了吗?

  2. 在服务端,我有多个小组,每个小组是一个分支,因为master是保持最稳定代码的版本,所以我要审查过每个分支上的代码再合并,而不是立刻将他们分支上的合并到master上面,一来保证了代码的质量,二来在小组方面可以更快发现bug,然后通知修改

工作中遇到的问题

Q&A1:
在本地test分支上进行新功能开发并且commit后,切换回本地主分支,想要在本地主分支上merge test分支的修改,执行git merge test,报错
在这里插入图片描述
查看为一些log文件,解决办法是删除这些log文件,重新执行,可正常合并

Q&A2:
在分支上想要pull远程代码时报错
test has no tracked branch
解决办法:
点击报错信息处的链接,选择origin/dev(远程分支)

Q&A3:
新建了一个分支,push到远程时,自动在远程创建了一个名称相同的分支,并成功push
解决方法:切换到和目标远程分支连接的分支,点击git-log,选择新建的分支,选中错误push的记录,点击樱桃
提交会自动移动到主分支,点击push到目标远程分支
在这里插入图片描述
对樱桃的理解:
我们代码库中的一个个 commit 可以看做一个个 cherry。同一个代码库中的 commit-id 往往是唯一的,当你在分支 B 上也需要在分支 A 上的提交内容时,就可以将它们 cherry-pick 过来!
和merge的区别:可以筛选需要的内容

以命令的形式使用

# 切换到 feature-b 分支
git checkout feature-b
# 挑选 commit 6b95b5和b09a488为commit-id
git cherry-pick 6b95b5 b09a488
# 不进行自动提交的方式 加上-n
git cherry-pick 6b95b5 b09a488 -n
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

樱桃摘的时候有冲突怎么办

  • 解决有冲突的文件
  • git add -u 标记有冲突的文件已经解决好冲突了
  • git cherry-pick --continue

除此以外,如果你过程中不想 cherry-pick了,只需执行:git cherry-pick --abort

6、GitHub远程仓库

# 取别名
git remote add 别名 仓库HTTP地址

# 查询远程仓库别名
git remote -v

# 推送(add commit 且 merge到本地库master分支后进行,为防止修改期间远程仓库的代码发生改变,先pull下代码进行对比,在工作区解决冲突后再次add commit 再push)
# git push origin master
git push 远程仓库别名 远程仓库分支名

# copy克隆github上code下的URL下载项目到当前文件夹
# 此时这个文件夹默认是git管控的,无需初始化本地库git init
# 默认远程仓库别名为origin,无需设置
git clone 仓库HTTP地址

# 抓取远程仓库的文件fetch+merge
git fetch origin master
# 抓取后查看远程仓库的内容是否正确(此时处于远程分支上,合并远程分支前需先切换回本地仓库主分支)
git checkout origin/master
ls
ls -a
git checkout master
git merge origin/master

# 以上拉取可以直接执行pull
git pull origin master

  • 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

7、跨团队合作GitHub的使用

# 得到团队1远程库的地址
# 团队2登录GitHub账号 在主页网址栏输入团队1远程库的地址 点击fork(一般在star旁边)得到fork远程库
# clone到本地 进行修改
# push到fork远程库
# 发送pull request请求(在code导航栏旁边)
# 团队1同意merge pull request
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

8、SSH免密登录

# 进入用户主目录
cd ~
# 生成一个.ssh的文件
ssh-keygen -t rsa -C “github邮箱”
#一路回车
# 输出
# Your public key has been saved in 绝对路径
cat 绝对路径
# 输出 ssh-rsa .....="邮箱"
复制 ssh-rsa .....="邮箱"

配置:

GitHub右上角-个人头像-settings-SSH Keys

效果:

本机可免密拉取代码

# 可以给SSH地址取别名,提交代码改用git push origin_ssh master, 不使用HTTP地址(origin)实现免密登录
git remote add origin_ssh SSH地址
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

9、IDEA集成Git

本地创建项目

# 配置Git
settings->git->path to git executable:输出Git的路径(/usr/local/bin/git)
# 本地库初始化操作
VCS->import into version control->create git repository->选中需要初始化的文件

# IDEA中 修改项目提交到本地库
在本地仓库中对文件进行修改,如添加一个类后,会出现如下提示,这个add就是git add 文件 的等同操作,即将该类文件从工作区添加到暂存区,选择add后,文件会变成绿色

右键文件->git->commit file->输入commit的信息->commit

对文件进一步修改后->如果之前没有add File to git->右键文件->git->Add

->右键文件->git->commit file->输入commit的信息->commit

# 告诉Git允许不相关历史合并 2个仓库(本地库和远程仓库)有不同的开始点
git pull origin master --allow-unrelated-histories
git push -u origin master -f
# 此时本地库和远程库就可以进行交互了(此处特指本地库为自己在本地创建,没有从远程库pull过代码的情况)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
image-20230113222231216

IDEA拉取项目

# IDEA中 拉取项目到本地库
file->new->project from version control

# IDEA中 修改项目提交到本地库
在本地仓库中对文件进行修改,如添加一个类后,会出现如下提示,这个add就是git add 文件 的等同操作,即将该类文件从工作区添加到暂存区,选择add后,文件会变成绿色

右键文件->git->commit file->输入commit的信息->commit

对文件进一步修改后->如果之前没有add File to git->右键文件->git->Add

->右键文件->git->commit file->输入commit的信息->commit

# IDEA中 修改项目提交到本地库后 推送到远程库
右键文件->git->repository->pull
有冲突先解决->add->commit
右键文件->git->repository->push
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

如何避免代码冲突

团队开发时避免再一个文件中改代码
修改一个文件前,在push前,先pull操作
  • 1
  • 2

10、ignore

安装插件

要使.gitignore文件生效,需要先安装.ignore插件

settings->plugins->安装.ignore插件(若搜索不到,可进入下面的地址获取)->重启IDEA

https://plugins.jetbrains.com/plugin/7495–ignore

创建文件

如果当前项目没有.gitignore文件:

在项目上右键->New ->.ignore file ->.gitignore file(Git)

配置文件

下面是一些.gitignore文件忽略的匹配规则:

*.a       # 忽略所有 .a 结尾的文件
!lib.a    # 但 lib.a 除外
/TODO     # 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
build/    # 忽略 build/ 目录下的所有文件
doc/*.txt # 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

特殊情况

.gitignore只能忽略那些原来没有被track的文件,如果某些文件已经被纳入了版本管理中,则修改.gitignore是无效的。那么解决方法就是先把本地缓存删除(改变成未track状态),然后再提交:

输入:
git rm -r –cached filePath
git commit -m "remove xx"
或者:
git rm -r --cached .
git add .
git commit -m "update .gitignore"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

来解释下几个参数

-r 是删除文件夹及其子目录

–cached 是删除暂存区里的文件而不删除工作区里的文件,第一种是删除某个文件,第二种方法就把所有暂存区里的文件删了,再加一遍,相当于更新了一遍。

如何判断哪些文件需要ignore

image-20230114134723007

需要忽略的文件
(1) mvnw
(2) mvnw.cmd
(3) .mvn
(4)target 不需提交 只需要编译后自己提取使用即可
(5)子模块的.gitignore文件
(6).idea 文件

**/mvnw
**/mvnw.cmd
**/.mvn
**/target/
.idea
**/.gitignore
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

二、缓存

详见: 05、缓存&分布式锁.pdf

1、Redis

1、改pom

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

2、配置Redis主机和端口

3、注入客户端

@Autowired
private StringRedisTemplate redisTemplate;
  • 1
  • 2

4、调用方法

@RestController
@RequestMapping("/sms")
public class SmsSendController {
    @Autowired
    private SmsComponent smsComponent;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @PostMapping("/sendcode")
    public R sendCode(HttpServletRequest request){
        String phone = request.getParameter("phone_num").trim();
        // 实现接口防刷
        String redisCode = redisTemplate.opsForValue().get("sms_code_" + phone);
        if (!StringUtils.isEmpty(redisCode)) {
            Long redisCodeTime = Long.parseLong(redisCode.split("_")[1]);
            if (System.currentTimeMillis() - redisCodeTime < 60000) {
                // 60秒内不能再次发送
                return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
            }
        }
      	String code = String.valueOf((int) (Math.random() * 900000 + 100000));
        // 验证码的再次校验,直接存在Redis
        redisTemplate.opsForValue().set("sms_code_"+phone,code+"_"+System.currentTimeMillis(),10, TimeUnit.MINUTES);


        System.out.println(code);
        smsComponent.sendSmsCode(phone,code);
        return R.ok();
    }
}

  • 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

存储数据类型

  • string
    应用场景:常规数据,如session、token、地址、序列化后的对象;计数,如请求量、访问量
  • list
    应用场景:信息流的展示,如最新文章或动态
  • hash:一个 String 类型的 field-value(键值对) 的映射表
    应用场景:对象数据,如用户信息、商品信息
  • set
    应用场景:数据不能重复,如点赞数;多个数据源交集、并集、差集,如共同好友
  • zset:sorted set,设置时需要指定score
    应用场景:随机获取元素并根据某个权重排序,如排行榜

解决缓存失效问题

1、缓存穿透:查询一定不存在的数据

解决:缓存空结果null,并且设置过期时间

2、缓存雪崩:大量缓存同时失效

解决:原有失效时间上增加随机值

3、缓存击穿:超高并发访问一个正好失效的Key

解决:加锁(用户出现大并发访问的时候,在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接集中缓存)
不设置热点key的失效时间

解决缓存数据一致性的问题

1、双写模式-写数据库,写缓存
问题:有暂时性的脏数据问题,由于卡顿等原因
2、失效模式-写数据库,删缓存
问题:有暂时性的脏数据
3、解决方案

  • 对于实时性、一致性要求不高的:加过期时间;加读写锁
  • 对于实时性、一致性要求高的:直接查数据库

4、为什么不更新缓存而是删除缓存
更新操作麻烦,浪费性能
5、为什么不先删除缓存,再更新数据库
并发下,如果线程2在线程1更新数据库前读取了数据库并写入缓存,那么相当于我们删除缓存的操作无效
6、延迟双删
先执行缓存清除操作,再执行数据库更新操作,延迟 N 秒之后再执行一次缓存清除操作
N秒要大于一次写操作的时间,否则会导致请求A已经删除了缓存,请求B还没写入缓存
删除缓存的原子性,可以结合rabbitmq的消息消费失败重试机制

2、Redisson

Redisson为Redis的客户端,整合了分布式锁功能
通过netty支持非阻塞io
实现像操作本地锁一样操作Redis的分布式锁

在这里插入图片描述

3、SpringCache

每次调用缓存功能的方法时,先去Redis中查,如果没有才会查数据库,同时更新缓存

使用策略:

  • 确定方法需要被缓存以及缓存策略
  • 优先从缓存中读取

3.1、改pom

				<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3.2、配置apllication.properties

# 设置Redis连接信息
spring.redis.host=localhost
spring.redis.port=6379
# 指定SpringCache的类型为Redis
spring.cache.type=redis
# 设置Redis过期时间-30分钟-以毫秒为单位
spring.cache.redis.time-to-live=1800000
# 如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
# "CACHE_allSinger"
# spring.cache.redis.key-prefix=CACHE_
# "singer::allSinger"默认就是true,使用前缀
# spring.cache.redis.use-key-prefix=true
# "allSinger"
# spring.cache.redis.use-key-prefix=false

# 是否缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

3.3、主启动类配置注解

@EnableCaching
  • 1

3.4、service的实现类的方法上配置注解

双写模式

@Cacheable(value = {"singer"},key = "# root.method.name",sync = true)
  • 1

失效模式

		@CacheEvict(value = "singer",allEntries = true)
    @Override
    public boolean update(Singer singer) {
        return singerMapper.update(singer)>0;
    }
/**
     * 级联更新所有关联的数据
     *
     * @CacheEvict:失效模式
     * @CachePut:双写模式,需要有返回值
     * 1、同时进行多种缓存操作:@Caching
     * 2、指定删除某个分区下的所有数据 @CacheEvict(value = "category",allEntries = true)
     * 3、存储同一类型的数据,都可以指定为同一分区
     * @param category
     */
    // @Caching(evict = {
    //         @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
    //         @CacheEvict(value = "category",key = "'getCatalogJson'")
    // })
    // @CacheEvict(value = "category",allEntries = true)       //删除某个分区下的所有数据
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

3.5、自定义配置类

MyCacheConfig.java

将主启动类的配置注解移到该配置类上

package com.flora.music.config;

import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @Author qinxiang
 * @Date 2023/1/12-下午9:22
 * 1、修改存入Redis时key 和 value的序列化规则,使其存入JSON格式
 * 2、将配置文件中所有的application.properties配置都生效,否则配置的过期时间将不生效 为-1
 */
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        // config = config.entryTtl();
        //修改存入Redis时key 和 value的序列化规则
        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.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }

        return config;
    }

}

  • 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

三、线程

详见:07、异步&线程池.pdf

1、初始化线程的4种方式

1.1 继承 Thread

在这里插入图片描述
在这里插入图片描述

1.2 实现 Runnable 接口

在这里插入图片描述
在这里插入图片描述

多线程与静态代理模式的关系

在这里插入图片描述
在这里插入图片描述

1.3 实现 Callable 接口 + FutureTask (可以通过get方法拿到返回结果,可以处理异常)

1.4 线程池

Java线程池的实现原理其实就是一个线程集合workerSet和一个阻塞队列workQueue
当用户向线程池提交一个任务(也就是线程)时,线程池会先将任务放入workQueue中
workerSet中的线程会不断从workQueue中获取线程然后执行。

具体实现描述:

1、继承Thread,重写run方法,new对象,调用对象的start方法
2、实现 Runnable 接口 ,重写run方法,new对象,new Thread(传入对象),调用Thread的start方法
3、实现 Callable 接口 ,重写call方法,new对象,new FutureTask<>(传入对象),new Thread(传入FutureTask对象),调用Thread的start方法
4、线程池 Executors.newFixedThreadPool(线程数),作为常量放在前面,调用execute(传入对象),该对象可以是实现了Runnable接口的类的对象
5、线程池 new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit, workQueue, threadFactory, handler);

区别

1/2不能获得返回值,3可以获得返回值,1/2/3不能控制资源,资源耗尽风险,4可以控制资源,性能稳定

2、线程池的七大参数

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    //...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  1. corePoolSize:核心线程数,线程池正常情况下保持的线程数,大户人家“长工”的数量。
  2. maximumPoolSize:最大线程数,当线程池繁忙时最多可以拥有的线程数,大户人家“长工”+“短工”的总数量。
  3. keepAliveTime:空闲线程存活时间,没有活之后“短工”可以生存的最大时间。
  4. TimeUnit:时间单位,配合参数 3 一起使用,用于描述参数 3 的时间单位。
  5. BlockingQueue:线程池的任务队列,用于保存线程池待执行任务的容器。
  6. ThreadFactory:线程工厂,用于创建线程池中线程的工厂方法,通过它可以设置线程的命名规则、优先级和线程类型。
  7. RejectedExecutionHandler:拒绝策略,当任务量超过线程池可以保存的最大任务数时,执行的策略。

线程池实现线程复用的原理:
在这里插入图片描述

拓展:有哪些拒绝策略?

AbortPolicy(默认):中止策略,线程池会抛出异常并中止执行此任务;
CallerRunsPolicy:把任务交给添加此任务的(main)线程来执行;
DiscardPolicy:忽略此任务,忽略最新的一个任务;
DiscardOldestPolicy:忽略最早的任务,最先加入队列的任务。
自定义拒绝策略:通过 new RejectedExecutionHandler,并重写 rejectedExecution 方法来实现,可以通过记录日志、加入消息队列等方法进行处理,实例如下:

public static void main(String[] args) {
    // 任务的具体方法
    Runnable runnable = new Runnable() {
        @Override
        public void run() {
            System.out.println("当前任务被执行,执行时间:" + new Date() +
                               " 执行线程:" + Thread.currentThread().getName());
            try {
                // 等待 1s
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    };
    // 创建线程,线程的任务队列的长度为 1
    ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1,
                                                           100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1),
                                                           new RejectedExecutionHandler() {
                                                               @Override
                                                               public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                                                                   // 执行自定义拒绝策略的相关操作
                                                                   System.out.println("我是自定义拒绝策略~");
                                                               }
                                                           });
    // 添加并执行 4 个任务
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
    threadPool.execute(runnable);
}
  • 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

在这里插入图片描述

拓展:如何设置合适的线程数量?

首先理解CPU密集型任务和IO密集型任务的区别
CPU密集型任务:又称计算密集型,占用服务器CPU较多,如加密解密、压缩、计算等,
设置线程数=CPU核心数2-1
IO密集型任务:占用服务器的硬盘、内存硬件较多,系统运行多是CPU在等I/O (硬盘/内存) 的读写操作,此类情景下CPU负载并不高。如数据库读写、文件读写、网络通讯等。特点是比CPU密集型任务慢很多,如果等待时间长可以多设置一些线程,
设置线程数=CPU核心数
(1+平均等待时间/平均工作时间)

  • 设置过大:上下文切换损耗大
  • 设置过小:不能充分利用CPU资源

3、为什么使用线程池

  • 降低资源消耗,通过重复利用已经创建好的线程降低线程的创建和销毁带来的损耗

  • 响应速度快,因为线程池中的线程数没有超过线程池的最大上限时,有的线程处于等待分配任务的状态,当任务来时无需创建新的线程就能执行

  • 可管理性高,线程池会根据当前系统特点对池内的线程进行优化处理,减少创建和销毁线程带来的系统开销。无限的创建和销毁线程不仅消耗系统资源,还降低系统的稳定性,使用线程池进行统一分配

4、CompletableFuture类

解决:Future对于任务结果的获取不方便,只能通过阻塞(get())或者轮询(消耗CPU资源)的方式得到
详细API见上方PDF

5、线程不安全

理解:多个线程操作同一个资源

解决方案:

在Java中,可以采用以下方法来解决线程不安全的问题:

  1. 同步代码块或方法:使用synchronized关键字来实现同步化操作,从而确保多个线程不会同时访问共享资源,进而避免线程安全问题。

  2. 使用锁:可以使用Java中的Lock和ReentrantLock等锁机制来控制多个线程对共享资源的访问。使用锁可以避免出现多个线程同时访问共享资源的问题。

在这里插入图片描述
区别:
在这里插入图片描述
在这里插入图片描述

  1. 使用原子类:可以使用Java中的原子类,如AtomicInteger和AtomicLong等,来确保对共享资源的读写操作是原子性的,从而避免出现线程安全问题。

  2. 使用线程安全的集合类:Java中提供了很多线程安全的集合类,如ConcurrentHashMap和CopyOnWriteArrayList等,可以直接使用这些类来避免线程安全问题。

  3. 避免共享资源:尽可能避免多个线程访问同一个共享资源,可以将共享资源进行拆分或使用ThreadLocal等技术来实现线程私有化。

需要注意的是,在解决线程安全问题时,应该尽量避免使用过多的同步化操作,因为这可能会降低程序的性能。同时,在编写多线程程序时,应该充分考虑线程安全问题,从而避免出现潜在的线程安全隐患。

补充知识点:lambda表达式


高亮部分删除(接口+方法),只留参数(只有一个参数也可以删除,留()即可),中间用->连接
在这里插入图片描述

四、Nginx

1、正向代理与反向代理

正向代理服务器:这类代理位于用户之前,在用户与其访问的网页服务器之间充当中介。这就是说用户的请求要通过正向代理后才能抵达网页。从互联网检索数据后,这些数据就会被发送到代理服务器并将其重定向后返回请求者。从互联网服务器的角度来看,这个请求是有代理服务器、而不是用户发送的。

作用:

  1. 访问受限地理位置
  2. 匿名性
  3. 网络抓取

反向代理服务器:即以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个服务器。

作用:

  1. 负载均衡 如轮询、加权轮询、iphash
  2. 缓存 如动静分离,静态资源存在Nginx
  3. 匿名性 安全性

区别:

  • 正向代理由客户端使用,例如专用网络内的用户;反向代理由互联网服务器使用

  • 正向代理确保网站绝不与用户直接通信;反向代理确保用户不会与后端服务器直接通信

Nginx主要完成的工作就是反向代理,比如我们向一台服务器的80端口发送了请求,该请求首先由Nginx监听到,其接受到请求内容后再转发给其他服务器,其他服务器处理完后再将结果传送给Nginx,最后由Nginx来统一返回给初始请求端。

2、解决问题

要访问后台页面,还得自己加上端口:http://api.gmall.com:8888。这就不够优雅了。我们希望的是直接域名访问:http://api.gmall.com。这种情况下端口默认是80,而80端口只有一个,将来我们可能有多个工程需要通过域名访问,如何让多个工程都直接通过域名访问呢?

查看Nginx配置

docker ps
docker exec -it ID /bin/bash
ls
cd etc/nginx
ls
  • 1
  • 2
  • 3
  • 4
  • 5

Nginx配置文件

文件结构:

image-20230115192947724

Nginx.conf文件

域名映射

说明:

Nginx.conf的http部分引入了conf.d下的所有文件

可直接在此目录下新增server配置

# 代理音乐网站客户端
    server {
    		# 监听端口号
        listen       80;
        # 域访问名 可配置多个
        server_name  floramusic.com;
				# 代理到的服务器位置 /代表根目录 可增加其他后缀配置多个域名映射location
        location / {
            proxy_pass   http://127.0.0.1:8080;
        }
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
负载均衡

http内还可以配置负载均衡

# 两台服务器轮询
upstream kuangstudy {
		server 127.0.0.1:8080 weight=1;
		server 127.0.0.1:8081 weight=1;
}
server {
		# 监听端口号
    listen       80;
    # 域访问名 可配置多个
    server_name  localhost;
    # 代理到的服务器位置 /代表根目录 可增加其他后缀配置多个域名映射location
    location / {
    		proxy_pass   http://kuangstudy;
    }
}

upstream music {
        server 127.0.0.1:8080 weight=1;
        server 127.0.0.1:8082 weight=1;
        keepalive 256;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
静态资源部署

什么是动静分离

静:img/js/css等静态资源 以实际文件存在的方式

动:服务器需要处理的请求

1、将静态资源static文件夹移到Nginx的html文件夹下

2、修改nginx的配置文件,添加一个server location配置,使静态资源可以正常的通过nginx访问,此配置应该放在负载均衡location的前面

server {
    listen       80;
    server_name  floramusic.com;

    location /static/ {
        root   /usr/share/nginx/html;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3、访问静态资源的路径带前缀static

五、域名配置

# 修改本地的hosts文件 使IP地址映射到指定的域名
cd /etc
sudo vim hosts
# 添加flora.com 点击i进入编辑模式 完成后点击esc 退出编辑模式 输入:wq 保存并退出
127.0.0.1       localhost flora.com
  • 1
  • 2
  • 3
  • 4
  • 5

六、Elasticsearch

详见:06、ElasticSearch.pdf

背景:用于快速存储、搜索、分析海量数据

1、存储结构

对比MySQL:

 索引(indices)----------------------Databases 数据库

  类型(type)(Elasticsearch8以后移除了该概念)--------------------------Table 数据表
 
 		文档(Document)----------------------Row 行

    	字段(Field)-------------------------Columns 列 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

2、重要概念:倒排索引

3、使用示例

PUT方法新增

GET方法查询

结果分析

4、应用:乐观锁

更新携带if_seq_no 并发修改时只有一个可以修改成功(修改后_seq_no加1,导致其他请求无效)

5、应用:版本号控制

_update新增

带_update元数据不变进行请求

不带_update元数据不变进行请求_

总结:带_update 对比元数据如果一样就不进行任何操作,文档 version 不增加 。

使用场景:

对于大并发更新,不带 update;

对于大并发查询偶尔更新,带 update;对比更新,重新计算分配规则。

6、常用方法

# 检索索引下的所有信息
GET bank/_search
# q=*查询所有 按account_number升序返回
GET bank/_search?q=*&sort=account_number:asc
# 以请求体的形式进行检索
GET bank/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "account_number": "asc"
    }
  ],
  "from": 0,
  "size": 3,
  "_source": [
    "balance",
    "city"
  ]
}
# 匹配
GET bank/_search
{
  "query": {"match": {
    "city": "Bennett"
  }}
}
GET bank/_search
{
  "query": {"match": {
    "address": "place 8"
  }}
}
# 复合查询bool
GET bank/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "state": "WA"
          }
        }
      ]
      , "should": [
        {"match": {
          "gender": "M"
        }}
      ]
      , "must_not": [
        {"match": {
          "firstname": "Bridget"
        }}
      ]
    }
  }
}
# 过滤 不计算相关性得分
GET bank/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "range": {
            "balance": {
              "gte": 10000,
              "lte": 20000
            }
          }
        }
      ]
    }
  }
}
# 全文检索字段用 match,其他非 text 字段匹配用 term
GET bank/_search
{
  "query": {
    "term": {
      "age": {
        "value": "34"
      }
    }
  }
}
# 聚合 匹配gender为M,以年龄聚合后再聚合平均薪资,同时以年龄的平均数聚合
GET bank/_search
{
  "query": {
    "match": {
      "gender": "M"
    }
  },
  "aggs": {
    "aggAge": {
      "terms": {
        "field": "age"
      },
      "aggs": {
        "aggAgeAndAvgBalance": {
          "avg": {
            "field": "balance"
          }
        }
      }
    },
    "aggAvg":{
      "avg": {
        "field": "age"
      }
    }
  }
}
  • 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
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117

7、Mapping 数据迁移

# mapping
GET bank/_mapping
# 创建索引并指定映射,之前是系统根据输入自定义数据类型
PUT /my_index
{
  "mappings": {
    "properties": {
      "age":{"type": "integer"},
      "email":{"type": "keyword"},
      "name":{"type": "text"}
    }
  }
}
# 添加新的映射,已经存在的映射只能通过添加的方式对其进行修改,index用于控制字段是否可检索
PUT my_index/_mapping
{
  "properties":{
    "employee_id":{
      "type":"keyword",
      "index":false
    }
  }
}
# 数据迁移-首先创建新的索引
GET bank/_search
GET bank/_mapping
PUT newbank
{
  "mappings": {
    "properties": {
      "account_number": {
        "type": "long"
      },
      "address": {
        "type": "text"
      },
      "age": {
        "type": "integer"
      },
      "balance": {
        "type": "long"
      },
      "city": {
        "type": "keyword"
      },
      "email": {
        "type": "keyword"
      },
      "employer": {
        "type": "keyword"
      },
      "firstname": {
        "type": "text"
      },
      "gender": {
        "type": "keyword"
      },
      "lastname": {
        "type": "text",
        "fields": {
          "keyword": {
            "type": "keyword",
            "ignore_above": 256
          }
        }
      },
      "state": {
        "type": "keyword"
      }
    }
  }
}
GET newbank
# 数据迁移-去除类型"type": "account" 迁移索引
POST _reindex
{
  "source": {
    "index": "bank",
    "type": "account"
  },
  "dest": {
    "index": "newbank"
  }
}
GET newbank/_search
  • 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
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85

8、分词器

1、下载-对应elasticsearch版本

https://github.com/medcl/elasticsearch-analysis-ik/releases/tag/v7.14.1

解压到elasticsearch的plugins目录下 /Users/qinxiang/Documents/docker/Elasticsearch/plugins/elasticsearch-analysis-ik-7.14.1

重启elasticsearch

# 分词 "analyzer": "ik_max_word" 最大程度的分词;"analyzer": "ik_smart" 智能分词
POST _analyze
{
  "analyzer": "ik_smart",
  "text": "我是中国人"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

自定义分词

可以维护一个txt,将该文件放到Nginx的html文件夹下

可通过网址直接访问

http://127.0.0.1/es/fenci.txt

修改ik分词器的远程地址

文件位置 /Users/qinxiang/Documents/docker/Elasticsearch/plugins/elasticsearch-analysis-ik-7.14.1/config/IKAnalyzer.cfg.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
	<comment>IK Analyzer 扩展配置</comment>
	<!--用户可以在这里配置自己的扩展字典 -->
	<entry key="ext_dict"></entry>
	 <!--用户可以在这里配置自己的扩展停止词字典-->
	<entry key="ext_stopwords"></entry>
	<!--用户可以在这里配置远程扩展字典 -->
	<entry key="remote_ext_dict">http://127.0.0.1/es/fenci.txt</entry>
	<!--用户可以在这里配置远程扩展停止词字典-->
	<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

9、整合springboot

1、导入pom依赖 这里的版本号只是控制plugin的版本

        <dependency>
            <groupId>org.elasticsearch.client</groupId>
            <artifactId>elasticsearch-rest-high-level-client</artifactId>
            <version>7.14.1</version>
        </dependency>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

由于springboot2.7.7整合的elasticsearch版本为7.17.8

在项目的pom文件中我们需要新增<elasticsearch.version>7.14.1</elasticsearch.version>

此时elasticsearch客户端的下面2项的版本号才会发生变化

2、编写配置

package com.flora.music.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Author qinxiang
 * @Date 2023/1/16-下午7:35
 * elasticsearch的配置
 * 当前配置的是本机的es 如果有多个,可以new多个,指定主机、端口号及协议名
 * 用@bean注入到容器
 */
@Configuration
public class ElasticsearchConfig {
    public static final RequestOptions COMMON_OPTIONS;
    static {
        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
        COMMON_OPTIONS = builder.build();
    }
    @Bean
    public RestHighLevelClient esRestClient(){
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                new HttpHost("localhost",9200,"http")
                ));
        return client;

    }
}

  • 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

可进行单元测试

package com.flora.music;

import com.alibaba.fastjson.JSON;
import com.flora.music.config.ElasticsearchConfig;
import com.flora.music.domain.Singer;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;

@SpringBootTest
class MusicApplicationTests {
    @Autowired
    private RestHighLevelClient client;
    // 获取操作elasticsearch的客户端
    @Test
    void contextLoads() {
        System.out.println(client);
    }
    // 添加
    @Test
    void indexData() throws IOException {
        Singer singer = new Singer();
        singer.setName("周杰伦");
        singer.setLocation("中国");
        singer.setIntroduction("华语乐坛天王");
        singer.setSex(new Byte("1"));
        String s = JSON.toJSONString(singer);
        IndexRequest indexRequest = new IndexRequest("singer");
        indexRequest.id("1");
        indexRequest.source(s, XContentType.JSON);
        IndexResponse index = client.index(indexRequest, ElasticsearchConfig.COMMON_OPTIONS);
        System.out.println(index);
    }
    //查询
    @Test
    void searchData() throws IOException {
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.indices("singer");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder.query(QueryBuilders.matchQuery("name","周杰伦"));
        searchRequest.source(searchSourceBuilder);
        SearchResponse search = client.search(searchRequest, ElasticsearchConfig.COMMON_OPTIONS);
        SearchHits hits = search.getHits();
        SearchHit[] searchHits = hits.getHits();
        for (SearchHit hit:searchHits){
//            Map<String, Object> map = hit.getSourceAsMap();
//            System.out.println(map);
            String sourceAsString = hit.getSourceAsString();
            // 把返回结果封装成Singer类
            Singer singer = JSON.parseObject(sourceAsString, Singer.class);
            System.out.println(singer);
        }
    }

}

  • 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
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72

七、thymeleaf

详见:7、模板引擎-Thymeleaf.md

用来开发Web的服务器端Java模板引擎,可在Java独立环境中运行,作为静态原型直接显示正确结果在浏览器中

JSP与Thymeleaf:

springboot不推荐JSP

一般的模板技术(Jsp和Freemarker)都会在页面添加 各种表达式、标签甚至是java代码,而这些都必须要经过后台服务器的渲染才能打开。但如果前端开发人员做页面调整,双击打开某个jsp或者ftl来查看效果,基本上是打不开的。

那么Thymeleaf的优势就出来了,因为Thymeleaf没有使用自定义的标签或语法,所有的模板语言都是扩展了标准H5标签的属性

1、整合springboot

1、导入pom依赖


      <!-- 配置devtools command+shift+F9 可以实时显示页面变化,不需要重启后台服务-->
      	<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
          	<optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2、改配置文件

Thymeleaf默认会开启页面缓存,提高页面并发能力。但会导致开发时修改页面不会立即被展现,因此关闭缓存:

# 关闭Thymeleaf的缓存
spring.thymeleaf.cache=false
  • 1
  • 2

另外,每次修改完毕页面,需要使用快捷键:Ctrl + Shift + F9来刷新工程。

thymeleaf解析方式与jsp类似:前缀 + 视图名 + 后缀。

在Thymeleaf的配置类中配置了默认的前缀和后缀:

  • 默认前缀:classpath:/templates/
  • 默认后缀:.html
  • 默认编码:UTF_8

所以如果我们返回视图:users,会指向到 classpath:/templates/users.html

3、resources目录下的templates新建一个html

hello.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>hello</title>
</head>
<body>
    <h1 th:text="${msg}">大家好</h1>
</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

注意,把html 的名称空间,改成:xmlns:th="http://www.thymeleaf.org 会有语法提示

4、写controller,控制视图跳转

@Controller
public class HelloController {

    @GetMapping("show1")
    public String show1(Model model){
        model.addAttribute("msg", "Hello, Thymeleaf!");
        return "hello";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

优化:

发送请求跳转到页面 采用springMVC viewcontroller:将请求和页面映射过来

写config,控制视图跳转

@Configuration
public class GulimallWebConfig implements WebMvcConfigurer {

    /**·
     * 视图映射:发送一个请求,直接跳转到一个页面
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {

        // registry.addViewController("/login.html").setViewName("login");
        registry.addViewController("/reg.html").setViewName("reg");
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

5、访问页面

http://localhost:8888/show1

返回hello.html页面

八、短信验证码

参考阿里云提供的API请求示例

https://market.aliyun.com/products/57126001/cmapi00037415.html?spm=5176.730005.recommend-commodity.d0.397b123eUwU0qx#sku=yuncode3141500001

1、在测试类中进行测试

先创建好HttpUtils类

//测试短信发送-阿里云
    @Test
    void sendCode() {
        String host = "https://gyytz.market.alicloudapi.com";
        String path = "/sms/smsSend";
        String method = "POST";
        String appcode = "762689c204704b2eb982e5647936edec";
        Map<String, String> headers = new HashMap<String, String>();
        //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
        headers.put("Authorization", "APPCODE " + appcode);
        Map<String, String> querys = new HashMap<String, String>();
        querys.put("mobile", "15545334996");
        querys.put("param", "**code**:888888,**minute**:5");
        querys.put("smsSignId", "2e65b1bb3d054466b82f0c9d125465e2");
        querys.put("templateId", "908e94ccf08b4476ba6c876d13f084ad");
        Map<String, String> bodys = new HashMap<String, String>();


        try {
            /**
             * 重要提示如下:
             * HttpUtils请从
             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
             * 下载
             *
             * 相应的依赖请参照
             * https://github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
             */
            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
            System.out.println(response.toString());
            //获取response的body
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 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

2、对该功能进行封装

2.1 短信组件类SmsComponent

package com.flora.music.component;

import com.flora.music.utils.HttpUtils;
import lombok.Data;
import org.apache.http.HttpResponse;
import org.apache.http.util.EntityUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * @Author qinxiang
 * @Date 2023/1/18-下午7:46
 */
@ConfigurationProperties(prefix = "spring.alicloud.sms")
@Data
@Component
public class SmsComponent {
    private String host;
    private String path;
    private String appcode;
    private String smsSignId;
    private String templateId;
    public void sendSmsCode(String phone,String code){
        String method = "POST";
        Map<String, String> headers = new HashMap<String, String>();
        //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105
        headers.put("Authorization", "APPCODE " + appcode);
        Map<String, String> querys = new HashMap<String, String>();
        querys.put("mobile", phone);
        querys.put("param", "**code**:"+code+",**minute**:5");
        querys.put("smsSignId", smsSignId);
        querys.put("templateId", templateId);
        Map<String, String> bodys = new HashMap<String, String>();


        try {
            HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys);
            System.out.println(response.toString());
            //获取response的body
            System.out.println(EntityUtils.toString(response.getEntity()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

  • 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

2.2 创建配置文件application.yml

spring:
  alicloud:
    sms:
      host: https://gyytz.market.alicloudapi.com
      path: /sms/smsSend
      appcode: 762689c204704b2eb982e5647936edec
      sms-sign-id: 2e65b1bb3d054466b82f0c9d125465e2
      template-id: 908e94ccf08b4476ba6c876d13f084ad
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.3 创建SmsSendController类

package com.flora.music.controller;

import com.flora.music.component.SmsComponent;
import com.flora.music.exception.BizCodeEnum;
import com.flora.music.utils.R;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;

/**
 * @Author qinxiang
 * @Date 2023/1/18-下午8:23
 */
@RestController
@RequestMapping("/sms")
public class SmsSendController {
    @Autowired
    private SmsComponent smsComponent;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @PostMapping("/sendcode")
    public R sendCode(HttpServletRequest request){
        String phone = request.getParameter("phone_num").trim();
        // 实现接口防刷
        String redisCode = redisTemplate.opsForValue().get("sms_code_" + phone);
        if (!StringUtils.isEmpty(redisCode)) {
            Long redisCodeTime = Long.parseLong(redisCode.split("_")[1]);
            if (System.currentTimeMillis() - redisCodeTime < 60000) {
                // 60秒内不能再次发送
                return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg());
            }
        }
        // 这种方式会带字母,不符合当前的短信内容格式
        // String code = UUID.randomUUID().toString().substring(0,6);
        String code = String.valueOf((int) (Math.random() * 900000 + 100000));
        // 验证码的再次校验,直接存在Redis
        redisTemplate.opsForValue().set("sms_code_"+phone,code+"_"+System.currentTimeMillis(),10, TimeUnit.MINUTES);


        System.out.println(code);
        smsComponent.sendSmsCode(phone,code);
        return R.ok();
    }
}
  • 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

2.4 前端发送请求,获取验证码

九、后端接收参数的方式

https://www.cnblogs.com/mask-xiexie/p/15994437.html

十、加密

1、MD5

Message Digest algorithm 5,信息摘要算法

• 压缩性:任意长度的数据,算出的MD5值长度都是固定的。

• 容易计算:从原数据计算出MD5值很容易。

• 抗修改性:对原数据进行任何改动,哪怕只修改1个字节,所得到的MD5值都有很大区别。

• 强抗碰撞:想找到两个不同的数据,使它们具有相同的MD5值,是非常困难的。

• 不可逆

加盐:

• 通过生成随机数与MD5生成字符串进行组合

• 数据库同时存储MD5值与salt值。验证正确性时使用salt进行MD5即可

2、对MD5加盐的实现类BCryptPasswordEncoder:

2.1 POM依赖

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-config</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

2.2 使用

BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
加密:
bcryptPasswordEncoder.encode(password);
 
//password是输入的密码,encodedPassword是通过bcryptPasswordEncoder进行加密的密码
解密:
bcrytPasswordEncoder.matches(password,encodedPassword)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

十一、equals与==

if(username == null || username.equals(""))
// 双重比较,username == null 表示username没有指向任何一块内存地址,username.equals("") 表示username指向了一块字符串为空的内存地址
  • 1
  • 2

以下几点需要了解:

  • null是任何引用类型的默认值
  • 任何含有null值的包装类在Java拆箱生成基本数据类型时候都会抛出一个空指针异常。(例如Integer拆箱成int时)
  • 如果使用了带有null值的引用类型变量,instanceof操作将会返回false。(instanceof:用来在运行时指出对象是否是特定类的一个实例,例如:Integer num = null,那么,调用 num instanceof Integer时会返回false)
  • 不能调用非静态方法来使用一个值为null的引用类型变量,它将会抛出空指针异常;可以使用静态方法来使用一个值为null的引用类型变量,因为静态方法使用静态绑定,不会抛出空指针异常。
// 举例

//假设我们现在有一个类,例如String,对于如下代码:
String str = null;
if (str.equals("Hello World!")){
    System.out.println("Yes");
}else {
    System.out.println("No");
}
// 就会抛出空指针异常,但是,如果我们把第二行的条件判断改为:

if ("Hello World!".equals(str))
  
//就不会抛出空指针异常,因为String的equals方法不是Static方法

// 测试equals结果
    // java.lang.NullPointerException
    @Test
    void equalsTest(){
        String username = null;
        if (username.equals("")){
            System.out.println("true");
        }
    }

		// true
    @Test
    void equalsTest(){
        String username = null;
        if (username == null || username.equals("")){
            System.out.println("true");
        }
    }

		// false
		@Test
    void equalsTest(){
        String username = null;
        if ("".equals(username)){
            System.out.println("true");
        }else {
            System.out.println("false");
        }
    }
  • 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

十二、社交登录

微博登录-待补充

https://open.weibo.com/

十三、Session

问题:分布式下,同一服务复制多份,session不能同步;不同服务session不能共享

解决方案:

1、采用session复制同步方案

限制:对于大型分布式,数据是所有session的总和,内存限制;数据传输,占用网络带宽

2、客户端存储

不占用服务器资源

限制:每次http请求,携带用户在cookie中的完整信息, 浪费网络带宽;session数据放在cookie中,cookie有长度限制

4K,不能保存大量信息;session数据放在cookie中,存在泄漏、篡改、 窃取等安全隐患

3、hash一致性

同一个用户ip_hash负载均衡到同一台服务器,只需要改Nginx配置

限制:服务器闪断和服务器水平拓展(分配规则发生变化)的问题

4、统一存储

Redis存储-springsession服务

限制:增加了一次网络调用,Redis获取数据比内存慢

1、SpringSession

背景:HTTP协议本身是无状态的,为了保存会话信息,浏览器Cookie通过SessionID标识会话请求,服务器以SessionID为key来存储会话信息。在单实例应用中,可以考虑应用进程自身存储,随着应用体量的增长,需要横向扩容,多实例session共享问题随之而来。

Spring Session就是为了解决多进程session共享的问题

核心原理:

 * @EnableRedisHttpSession导入RedisHttpSessionConfiguration配置

 * 1、给容器中添加了一个组件

 * SessionRepository->RedisOperationsSessionRepositoryRedis操作session,session的增删改查封装类

 * 2SessionRepositoryFilter->Filter:session存储过滤器,每个请求过来必须经过filter
   创建时自动从容器总获取SessionRepository
   原始的request response被包装为SessionRepositoryRequestWrapper SessionRepositoryResponseWrapper
   获取session->request.getSession()->SessionRepositoryRequestWrapper---wrappedRequest.getSession()---SessionRepository中获取到
   
核心源代码:   
 protected void doFilterInternal(HttpServletRequest request,
           HttpServletResponse response, FilterChain filterChain)
           throws ServletException, IOException {
       request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
       //包装了HttpServletRequest,覆写了HttpServletRequest中 getSession(boolean create)方法
       SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
               request, response, this.servletContext);
             ......
       try {
           filterChain.doFilter(strategyRequest, strategyResponse);
       }
       finally {
           //保证session持久化
           wrappedRequest.commitSession();
       }
   }
  • 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

使用方式:

1、pom

<dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
</dependency>

  • 1
  • 2
  • 3
  • 4
  • 5

2、配置文件

# 设置Redis连接信息
spring.redis.host=localhost
spring.redis.port=6379
# 指定存储类型为Redis
spring.session.store-type=redis

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

3、在主启动类上开启服务

@EnableRedisHttpSession
  • 1

4、使用示例

@RequestMapping()
    public void test(HttpServletRequest request, HttpServletResponse response) throws IOException {
        //现在操作的都是redis里面的session
        HttpSession session = request.getSession();
        List<String> list = (List<String>) session.getAttribute("list");
        if (list == null){
            list = new ArrayList<>();
            //放入redis
            session.setAttribute("list",list);
        }
        list.add("y2m");
      	//放入redis 同步
        session.setAttribute("list",list);
        response.getWriter().println("size:"+list.size());
        response.getWriter().println("sessionId:"+session.getId());
      	//移除session redis管理的话到redis移除
    		//request.getSession().invalidate();
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

5、SessionConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

@Configuration
public class GulimallSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
        //放大作用域 到一级域名
        cookieSerializer.setDomainName("gulimall.com");
        cookieSerializer.setCookieName("GULISESSION");
        return cookieSerializer;
    }
  	// 设置存储Redis的序列化机制,不需要每个传输类都去实现Serializable
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        return new GenericJackson2JsonRedisSerializer();
    }
}

  • 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

十四、SSO单点登录

背景:session只能解决一级域名下微服务的信息共享,对于多系统,需要使用单点登录,一处登录处处登录

SSO测试:下载gitee上的单点登录项目进行测试

https://gitee.com/xuxueli0323/xxl-sso.git

运行方式:

cd 到目标目录

java打包 跳过测试
mvn clean package -Dmaven.skip.test=true

cd 到jar包所在target文件夹
启动服务
java -jar  xxl-sso-server-1.1.1-SNAPSHOT.jar
可以指定端口,不指定时默认使用配置文件中的端口
java -jar  xxl-sso-server-1.1.1-SNAPSHOT.jar --server.port=8082
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

设计理念:

http协议是无状态协议。浏览器访问服务器时,要让服务器知道,你是谁,只有两种方式:

一是把“你是谁”写入 cookie。它会随每次 HTTP 请求带到服务端

二是在 URL、表单数据中带上你的用户信息(也可能在 HTTP 头部)。这种方式依赖于从特定的网页入口进入,因为只有走特定的入口,才有机会拼装出相应的信息,提交到服务端。

大部分 SSO 需求都希望不依赖特定的网页入口(集成门户除外),所以后一种方式有局限性。适应性强的方式是第一种,即在浏览器通过 cookie 保存用户信息相关凭据,随每次请求传递到服务端。我们采用的方案是第一种:Cookie接入方式

1、JWT

Json web token:JSON风格轻量级的授权与身份认证规范,可实现无状态、分布式的web应用授权

在这里插入图片描述

十五、RabbitMQ

详见:10、RabbitMQ.pdf

RabbitMQ是基于erlang开发,并发能力强,延时低,管理界面丰富。

应用:消息队列->异步处理、应用解耦、流量控制

  • 应用解耦:降低对数据库的依赖,没有消息中间件,用户端需要等待数据库处理并返回结果
  • 流量控制:对消费的消息条数进行控制,又称削峰填谷

概念

  • 消息代理
  • 目的地:队列(点对点)、主题(发布订阅)
  • AMQP:Advanced Message Queuing Protocol 一种消息代理的规范
  • 在这里插入图片描述

RabbitMQ的概念:

Exchange:分发消息策略不同则类型不同

  • direct:点对点
  • fanout:广播,不指定路由键
  • topic:广播,可指定路由键
  • headers:headers 匹配 AMQP 消息的 header 而不是路由键, headers 交换器和 direct 交换器完全一致,但性能差很多,目前几乎用不到

1、RabbitMQ测试

管理端rabbitmq的使用:
网址:http://localhost:15672
guest
guest

1、在rabbitMQ中创建交换机(指定类型)、队列、交换机绑定队列(指定路由键 如:music.# 满足规则队列才会接收)

2、在交换机中发布消息(指定路由键 如:如:music.news 则music.#的队列可以接收)

3、在队列中接收消息(指定Ack Mode)

2、整合Springboot

1、pom

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

RabbitAutoConfiguration就会自动生效,给容器中配置了RabbitTemplate、AmqpAdmin、CacheConnectionFactory、RabbitMessageTemplate

2、配置文件

spring:
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    virtual-host: /
  • 1
  • 2
  • 3
  • 4
  • 5

3、主启动类配置注解

// 注意:配置交换机、队列及绑定,以及发送消息都无需配置该注解,但监听消息的前提是配置该注解
@EnableRabbit
  • 1
  • 2

4、监听消息

方法一:@RabbitListener

// @RabbitListener 可以标注在类和方法上,但只能通过Message(原生详细信息 头+体)接收消息
// queues:声明需要监听的所有队列
    @RabbitListener(queues = {"hello-queue"})
    public void listenMsg(Message msg){
        byte[] body = msg.getBody();
        String s = new String(body);
        System.out.println("监听到的消息内容"+s+"监听到的原消息内容"+body);
      	// 监听到的消息内容{"id":null,"name":"xiaopengyou","sex":null,"pic":null,"birth":1674296295163,"location":null,"introduction":null}监听到的原消息内容[B@1b99cec9
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

方法二:@RabbitListener+@RabbitHandler

@RabbitListener(queues = {"hello-queue"})
@Service
public class SingerServiceImpl implements SingerService {
  	// @RabbitHandler 可以标注方法上,配合@RabbitListener(queues = {"hello-queue"})标注在类上,对不同的消息类型标注在不同的方法上进行不同的处理
    @RabbitHandler
    public void listenMsg2(Singer msg){
        System.out.println("监听到的消息内容"+msg);
        // 监听到的消息内容Singer(id=null, name=xiaopengyou, sex=null, pic=null, birth=Sat Jan 21 19:35:32 CST 2023, location=null, introduction=null)
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

5、其他配置

/**
 * RabbitMQ消息中间件的配置
 */
@Configuration
public class MyRabbitConfig {
    // 使用JSON格式接收消息
    @Bean
    public MessageConverter messageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

2.1 测试使用

// 测试rabbitMQ
    @Test
    void mq(){
        // 创建交换机
//        DirectExchange directExchange = new DirectExchange("hello-java-exchange", true, false);
//        amqpAdmin.declareExchange(directExchange);
//        // 注意:这种占位取值语法 log.info才适用 需要加lombok注解@Slf4j
//        log.info("exchange[{}]创建成功","hello-java-exchange");
        // 创建队列
//        Queue queue = new Queue("hello-queue",true,false,false);
//        amqpAdmin.declareQueue(queue);
//        log.info("queue[{}]创建成功","hello-queue");
        // 创建绑定
//        Binding binding = new Binding("hello-queue", Binding.DestinationType.QUEUE, "hello-java-exchange", "hello.java", null);
//        amqpAdmin.declareBinding(binding);
//        log.info("binding[{}]创建成功","hello.java");
        // 发送消息
//        String msg = "message message!";
//        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",msg);
//        log.info("消息[{}]创建成功",msg);
        // 发送实体类消息
        Singer singer = new Singer();
        singer.setName("xiaopengyou");
        singer.setBirth(new Date());
        rabbitTemplate.convertAndSend("hello-java-exchange","hello.java",singer);
        log.info("消息[{}]创建成功",singer);
        // 此时接收的消息是序列化的,想要转成JSON格式的数据接收,需要进行rabbitmq配置

    }
  • 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
创建交换机、队列、绑定可以直接在配置类中@Bean的方式进行
/**
 * @Author qinxiang
 * @Date 2023/1/21-下午5:26
 * RabbitMQ消息中间件的配置
 * 延迟队列的配置
 */
@Configuration
public class MyRabbitConfig3 {
    //测试
    // 首先在controller类的方法中给MQ发送消息,然后在到期后需要执行的方法上进行监听
    @RabbitListener(queues = "order.release.order.queue")
    public void listener(Message msg){
        System.out.println("");

    }
    /**
     * 死信队列
     * @return
     */
    @Bean
    public Queue orderDelayQueue(){
        //String name, boolean durable, boolean exclusive, boolean autoDelete, @Nullable Map<String, Object> arguments
        //            Queue(String name,  队列名字
        //            boolean durable,  是否持久化
        //            boolean exclusive,  是否排他
        //            boolean autoDelete, 是否自动删除
        //            Map<String, Object> arguments) 属性
        //x-dead-letter-exchange: order-event-exchange x-dead-letter-routing-key: order.release.order x-message-ttl: 60000
        HashMap<String, Object> args = new HashMap<>();
        args.put("x-dead-letter-exchange","order-event-exchange");
        args.put("x-dead-letter-routing-key","order.release.order");
        args.put("x-message-ttl",60000); // 消息过期时间 1分钟
        return new Queue("order.delay.queue",true,false,false,args);
    }
    /**
     * 普通队列
     *
     * @return
     */
    @Bean
    public Queue orderReleaseQueue() {

        Queue queue = new Queue("order.release.order.queue", true, false, false);

        return queue;
    }

    /**
     * TopicExchange
     *
     * @return
     */
    @Bean
    public TopicExchange orderEventExchange() {
        /*
         *   String name,
         *   boolean durable,
         *   boolean autoDelete,
         *   Map<String, Object> arguments
         * */
        return new TopicExchange("order-event-exchange", true, false);

    }


    @Bean
    public Binding orderCreateBinding() {
        /*
         * String destination, 目的地(队列名或者交换机名字)
         * DestinationType destinationType, 目的地类型(Queue、Exhcange)
         * String exchange,
         * String routingKey,
         * Map<String, Object> arguments
         * */
        return new Binding("order.delay.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.create.order",
                null);
    }

    @Bean
    public Binding orderReleaseBinding() {

        return new Binding("order.release.order.queue",
                Binding.DestinationType.QUEUE,
                "order-event-exchange",
                "order.release.order",
                null);
    }

}
  • 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
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92

2.2 消息确认机制

1、配置文件

# 开启消息确认机制
spring:
  rabbitmq:
  	# 开启发送端确认
    publisher-confirm-type: correlated
    # 开启发送端消息抵达队列的确认
    publisher-returns: true
    # 只要抵达队列,以异步发送优先回调我们这个returnconfirm
    template:
      mandatory: true
    # 接收端默认是自动确认,无论消息是否被处理成功 只要接收了就被删除,配置成手动确认,防止消息丢失
    listener:
      simple:
        acknowledge-mode: manual
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

2、设置确认和失败回调

@Configuration
public class MyRabbitConfig2 {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    /**
     * 定制RabbitTemplate
     * 1、服务收到消息就会回调
     *      1、spring.rabbitmq.publisher-confirms: true
     *      2、设置确认回调
     * 2、消息正确抵达队列就会进行回调
     *      1、spring.rabbitmq.publisher-returns: true
     *         spring.rabbitmq.template.mandatory: true
     *      2、设置确认回调ReturnCallback
     *
     * 3、消费端确认(保证每个消息都被正确消费,此时才可以broker删除这个消息)
     *
     */
    @PostConstruct  //MyRabbitConfig对象创建完成以后,执行这个方法
    public void initRabbitTemplate() {

        /**
         * 1、只要消息抵达Broker就ack=true
         * correlationData:当前消息的唯一关联数据(这个是消息的唯一id)
         * ack:消息是否成功收到
         * cause:失败的原因
         */
        //设置确认回调
        rabbitTemplate.setConfirmCallback((correlationData,ack,cause) -> {
            System.out.println("confirm...correlationData["+correlationData+"]==>ack:["+ack+"]==>cause:["+cause+"]");
        });
      	/**
         * 只要消息没有投递给指定的队列,就触发这个失败回调
         * message:投递失败的消息详细信息
         * replyCode:回复的状态码
         * replyText:回复的文本内容
         * exchange:当时这个消息发给哪个交换机
         * routingKey:当时这个消息用哪个路邮键
         */
        rabbitTemplate.setReturnCallback((message,replyCode,replyText,exchange,routingKey) -> {
            System.out.println("Fail Message["+message+"]==>replyCode["+replyCode+"]" +
                    "==>replyText["+replyText+"]==>exchange["+exchange+"]==>routingKey["+routingKey+"]");
        });
    }

}

  • 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

3、接收端手动确认消息

@RabbitListener(queues = {"hello-queue"})
@Service
public class SingerServiceImpl implements SingerService {
  	// @RabbitHandler 可以标注方法上,配合@RabbitListener(queues = {"hello-queue"})标注在类上,对不同的消息类型标注在不同的方法上进行不同的处理
    @RabbitHandler
    public void listenMsg2(Message message, Singer msg, Channel channel){
        System.out.println("监听到的消息内容"+msg);
        // Channel内按顺序自增-deliveryTag
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 签收信息 不批量签收(业务成功,签收)
            channel.basicAck(deliveryTag,false);
            // 拒签信息 不批量拒签(业务失败,拒签,信息可被重新处理)
            channel.basicNack(deliveryTag,false,true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

3、延迟队列

背景:

定时任务轮询数据库会消耗系统内存、增加数据库的压力。存在较大的时间误差

解决:

RabbitMQ的消息TTL和死信exchange(即消息过期变成死信,由交换机路由给指定的队列进行消费)

Tips:一般是给队列设置过期时间,因为消息需要按顺序进行消费,即使单独设置时间也没有作用
在这里插入图片描述

优化:共用一个交换机

4、相关问题

4.1 消息中间件有什么缺点

  • 系统的可用性降低–理解:mq宕机的情况会导致系统不可用
    解决方案:兜底,代码中try catch异常,捕捉后直接进行数据库的操作
    搭建高可用集群,kafka集群、rocketmq集群
  • 提高复杂性–消息重复、消息丢失(主要集中在rabbitmq,因为其他2种会持久化到数据库)、消息的顺序(解决:单个消费单个生产单个队列 一个主题下)
  • 一致性问题–用分布式事务来控制,rocketmq提供了,还可以使用seata

4.2 对比常用的mq

kafka:
  • 高吞吐量、低延迟:每秒可以处理几十万条消息,它的延迟最低只有几毫秒
  • 可扩展性:kafka集群支持热扩展
  • 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失
  • 容错性:允许集群中节点故障(若副本数量为n,则允许n-1个节点故障)
  • 高并发:支持数千个客户端同时读写
  • kafka只支持主要的mq功能,不提供消息查询和回溯,在大数据领域应用广泛(日志采集功能)

应用场景

  • 日志收集:收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer
  • 消息系统:解耦生产者和消费者、缓存消息
  • 用户活动跟踪:用来记录web用户或app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后消费者通过订阅这些topic来做实时的监控分析,也可以保存到数据库
  • 运营指标:收集各种分布式应用的数据,生产各种操作的集中反馈,如报警和报告
4.2.1 性能:每秒吞吐量

在这里插入图片描述
底层实现:kafka数据的处理采用sendfile的零拷贝技术

在这里插入图片描述
在这里插入图片描述

4.2.2 消息的匹配优先选择rabbitMQ:
RabbitMQ 是允许在消息中添加 routing_key 或者自定义消息头,然后通过一些特殊的 Exchange,很简单的就实现了消息匹配分发。开发几乎不用成本。
  • 1
4.2.3 消息的超时优先选择rabbitMQ:
rabbitMQ引入交换机,消息延迟时间到后将消息放入队列
Kafka:你先需要把消息先放入一个临时的 topic。
然后得自己开发一个做中转的消费者。让这个中间的消费者先去把消息从这个临时的 topic 取出来。
取出来,这消息还不能马上处理啊,因为没到时间呢。也没法保存在自己的内存里,怕崩溃了,消息没了。所以,就得把没有到时间的消息存入到数据库里。
存入数据库中的消息需要在时间到了之后再放入到 Kafka 里,以便真正的消费者去执行真正的业务逻辑。
  • 1
  • 2
  • 3
  • 4
  • 5

在这里插入图片描述

4.2.4 消息的保存优先选择kafka
RabbitMQ 消息被人取出来就被删除,无法被重复消费
Kafka:消息会被持久化一个专门的日志文件里。不会因为被消费了就被删除。
  • 1
  • 2
4.2.5 消息的错误处理优先选择rabbitMQ
kafka:当单个分区中的消息一旦出现消费失败,就只能停止而不是跳过这条失败的消息继续消费后面的消息。即不允许消息空洞。
在数据统计不要求十分精确的场景下选了Kafka
RabbitMQ:会在消息出问题或者消费错误的时候,可以重新入队或者移动消息到死信队列,继续消费后面的
  • 1
  • 2
  • 3

4.3 解决消息的重复

消费端幂等性
MVCC方式–乐观锁的一种实现,更新时对version进行更新并对version进行判断
去重表方式–在表中构建唯一索引
在这里插入图片描述

4.4 解决消息的顺序

rabbitMQ存在的问题:

  • 为了实现发布订阅功能,从而使用的消息复制,会降低性能并耗费更多资源
  • 多个消费者无法严格保证消息顺序
  • 大量的订单集中在一个队列,吞吐量受到了限制

kafka:

  • Kafka 的发布订阅并不会复制消息,因为 Kafka 的发布订阅就是消费者直接去获取被 Kafka 保存在日志文件中的消息就好。无论是多少消费者,他们只需要主动去找到消息在文件中的位置即可。

  • Kafka 不会出现消费者出错后,把消息重新入队的现象。

  • Kafka 可以对订单进行分区,把不同订单分到多个分区中保存,这样,吞吐量能更好。

4.5 解决消息的丢失

接收:消息确认机制(消费端手动ack)
发送:每一个发送的消息在数据库做好记录,定期将失败的消息再发送一遍

十六、接口幂等性

详见: 02、接口幂等性.pdf

接口幂等性:用户对于同一操作发起的一次请求或者多次请求的结果是一致的

方案:

  • token机制:带着服务器提供的token去执行业务(服务器会把token存在Redis),保证token的获取、比较、删除是原子性的(使用lua脚本 if redis.call(‘get’, KEYS[1]) == ARGV[1] then return redis.call(‘del’, KEYS[1]) else return 0 end )脚本解读:如果Redis使用get方式通过key得到的值和传过来的值相等,Redis删除这个key返回1,否则返回0

  • String collectToken = request.getParameter("collectToken"); // 传过来的值
    String key = Consts.CONSUMER_COLLECT_TOKEN_PREFIX + userId;
    // 原子验证令牌和删除
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script,Long.class), Arrays.asList(key),collectToken);
    if (result == 1){
    // 原子验证令牌和删除成功
    // 继续执行业务
    }else {
    // 原子验证令牌和删除失败
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
  • 锁机制:悲观锁(select…for update)、乐观锁(version)、分布式锁

  • 唯一约束:数据库(唯一索引)、Redis(set防重)、

  • 防重表:(防重表和业务表在同一个数据库,这样保证在同一个事务)

  • 全局请求唯一id:

十七、分布式事务

详见: 03、本地事务&分布式事务.pdf

1、背景

本地事务在同一个方法中可以使用@Transactional注解通过异常机制保证事务的一致性,即如果没有完全执行成功会自动回滚,但在分布式系统中只能控制住自己的回滚,控制不了其他事务的回滚

在分布式远程调用的场景下会出现以下问题:
在这里插入图片描述

2、CAP定理:

C:一致性
A:可用性
P:分区容错性

由于分区容错性是指分区之间的通信失败,不可避免,所有必须保证P

而C和A无法同时做到(例如:现状有3台机器进行数据同步与通信,现在3号机器宕机,数据还没收到,此时访问3号机器,如果满足一致性,则应该读取到新值,即3号机器不可用(因为如果可用应该读到旧值);如果满足可用性,则应该读到旧值,即3号机器不满足一致性)

raft 算法(CP):

  • 领导选举:在没有领导者时,节点自旋,先自旋结束的变成领导者,先将唯一票投给自己,然后给其他节点发投票信号,投票结束后,节点继续开始自旋,大多数原则确定领导者
  • 日志复制:在每个心跳时间内,领导者给随从者发送日志,随从者收到后重置自旋状态,领导者不断给随从者发送日志(突发状况:领导者宕机,随从者处于自旋状态,重复领导选举过程;分区通信失败,在各自的分区中进行领导选举和日志复制,当分区通信恢复时,选举次数多的领导成为所有节点的领导,执行日志复制,此时原先脱落组织的节点会进行分区通信失败期间的变更日志回滚,接收新领导的日志)

问题:分区通信失败时如果刚好是6个节点,3个节点在一个区,这样领导选举时永远无法满足大多数原则(即6中收到4票)

目前网络故障、节点故障时常态,一般选择保证AP

BASE理论(AP):

经过软状态、保证基本可用,达到最终一致性即可

3、解决方案

  • 二阶提交(2PC):预提交、提交;问题:性能不理想
  • TCC事务补偿型:(3个接口try、commit、cancel)实现最终一致性
  • 最大努力通知型:MQ设置最大通知次数
  • 可靠消息+最终一致性:提交前和提交后分别发送消息给MQ(MQ负责记录消息,发送消息)

4、Seata(SpringCloud Alibaba)

适用于简单的分布式远程调用场景,不适用高并发的场景(原因:等待方法完成才能接收下一个请求,效率低)

概念:

  • TC:事务协调者:维护全局和分支事务的状态,驱动全局事务提交或回滚
  • TM:事务管理器:定义全局事务范围,开始全局事务、提交或回滚全局事务
  • RM:资源管理器:管理分支事务的资源

机制:(AT模式)

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源

  • 二阶段:提交异步化,快速完成;回滚通过回滚日志进行反向补偿

5、RabbitMQ自动解锁

高并发场景下,应用RabbitMQ延迟队列

十八、秒杀

设计:

  • 服务独立部署
  • 链接加密:带随机码验证访问系统
  • 库存预热:库存缓存Redis,通过信号量控制秒杀请求
  • 动静分离:Nginx
  • 非法请求拦截:网关、过滤器
  • 流量错峰:验证码、先加入购物车
  • 队列削峰:秒杀成功消息进RabbitMQ队列
  • 限流&熔断&降级:熔断:被调用方故障,触发系统主动规则;降级:全局考虑主动停止一些正常服务;限流:控制请求流量压力;使用组件Sentinel

1、Sentinel(SpringCloud Alibaba)

1.1 整合SpringBoot

官网:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel

1、pom

<dependency> 
  <groupId>com.alibaba.cloud</groupId> 
  <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> 
</dependency>
  • 1
  • 2
  • 3
  • 4

2、下载启动sentinel控制台

3、配置

spring:
  cloud:
    sentinel:
      transport:
      	#默认传输端口
        port: 8719
        #配置启动控制台的端口
        dashboard: localhost:8080
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

4、在控制台调整参数(默认所有的流控设置保存在内存中,重启失效)

5、信息展示-Endpoint支持

问题:控制台实时监控没有图表数据;返回信息是默认信息

解决:使用Endpoint支持,拿到Springboot统计的相关信息实现图表展示

5.1 pom

<dependency> 
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-actuator</artifactId> 
</dependency>
  • 1
  • 2
  • 3
  • 4

5.2 配置

management.endpoints.web.exposure.include=*
  • 1

5.3 自定义阻塞返回信息

@Configuration
public class GulimallSeckillSentinelConfig {

    public GulimallSeckillSentinelConfig() {

        WebCallbackManager.setUrlBlockHandler(new UrlBlockHandler() {
            @Override
            public void blocked(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws IOException {
                R error = R.error(BizCodeEnum.TO_MANY_REQUEST.getCode(), BizCodeEnum.TO_MANY_REQUEST.getMessage());
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json");
                response.getWriter().write(JSON.toJSONString(error));

            }
        });

    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

6、远程调用熔断-引入feign支持

6.1 pom

<dependency> 
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-actuator</artifactId> 
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

6.2 配置

# 调用方的熔断保护
feign.sentinel.enabled=true
# 调用方还可以在控制台手动指定远程服务的降级策略,远程服务被降级处理,触发我们的熔断回调方法
# 在服务的提供方即远程服务指定降级策略,一般在超大浏览时牺牲一些远程服务,提供方是在运行的,但不运行自己的业务逻辑,返回的是默认的降级数据
  • 1
  • 2
  • 3
  • 4

6.3 使用实例

@FeignClient(name = "service-provider", fallback = EchoServiceFallback.class, configuration = FeignConfiguration.class)
public interface EchoService {
    @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
    String echo(@PathVariable("str") String str);
}

class FeignConfiguration {
    @Bean
    public EchoServiceFallback echoServiceFallback() {
        return new EchoServiceFallback();
    }
}

class EchoServiceFallback implements EchoService {
    @Override
    public String echo(@PathVariable("str") String str) {
        return "echo fallback";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

6.4 自定义资源

详见:https://github.com/alibaba/Sentinel/wiki/%E6%B3%A8%E8%A7%A3%E6%94%AF%E6%8C%81

7、网关流控-gateway支持

7.1 pom

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

7.2 配置

spring.cloud.sentinel.filter.enabled=false
  • 1

7.3 自定义回调

@Configuration
public class SentinelGatewayConfig {

    public SentinelGatewayConfig() {
        GatewayCallbackManager.setBlockHandler(new BlockRequestHandler() {
            //网关限流了请求,就会调用此回调
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {

                R error = R.error(BizCodeEnum.TO_MANY_REQUEST.getCode(), BizCodeEnum.TO_MANY_REQUEST.getMessage());
                String errorJson = JSON.toJSONString(error);

                Mono<ServerResponse> body = ServerResponse.ok().body(Mono.just(errorJson), String.class);
                return body;
            }
        });
    }

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

十九、链路追踪

详见: 14、SpringCloud组件.pdf

1、Sleuth(SpringCloud Alibaba)

背景:一个请求调用多个服务,快速定位

原理图:

1.1 整合SpringBoot

1、pom

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
  • 1
  • 2
  • 3
  • 4

2、整合zipkin可视化

3、zipkin数据持久化

选用elasticsearch存储数据库

二十、Netty

详见:https://github.com/dongzl/netty-handbook.git

netty是一个异步的、基于事件驱动的网络应用框架。用以快速开发高性能、高可靠性的网络IO程序

1、应用

  • RPC基础通信框架—阿里分布式服务框架 DubboRPC 框 架使用 Dubbo 协议进行节点间通信,Dubbo 协议默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信

dubbo的负载均衡算法:

  • 随机负载均衡
  • 轮询负载均衡
  • 最少活跃调用数(使慢的服务器收到更少的请求,活跃数指调用前后计数差,慢的活跃数大)
  • 一致性哈希负载均衡(相同参数的请求落在同一台机器上)

2、I/O模型

用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能

Java 共支持 3 种网络编程模型 I/O 模式:

  • BIO(同步并阻塞(传统阻塞型):一个连接一个线程)、适用于连接数目比较小且固定的架构
  • NIO(同步非阻塞:一个线程处理多个请求(连接))、适用于连接数目多且连接比较短(轻操作)的架构
  • AIO(异步非阻塞)适用于连接数目多且连接比较长(重操作)的架构

3、NIO

NIO 有三大核心部分: Channel(通道)、Buffer(缓冲区)、Selector(选择器)

  • 数据的读取写入是通过 Buffer,这个和 BIO不同,BIO 中要么是输入流,或者是输出流,不能双向,但是 NIOBuffer 是可以读也可以写,需要 flip 方法切换 Channel 是双向的,可以返回底层操作系统的情况,比如 Linux,底层的操作系统通道就是双向的。
  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

4、NIO 非阻塞网络编程原理分析

  • 当客户端连接时,会通过 ServerSocketChannel 得到 SocketChannel
  • socketChannel 注册到 Selector 上,register(Selector sel, int ops),一个 Selector 上可以注册多个 SocketChannel
  • 注册后返回一个 SelectionKey,会和该 Selector 关联(集合)。
  • Selector 进行监听 select 方法,返回有事件发生的通道的个数。
  • 进一步得到各个 SelectionKey(有事件发生)。
  • 在通过 SelectionKey 反向获取 SocketChannel,方法 channel()
  • 可以通过得到的 channel,完成业务处理。

5、零拷贝

传统IO模型:硬盘-(DMA直接内存拷贝)->内核缓冲区-(CPU拷贝)->用户缓冲区-(CPU拷贝)->socket缓冲区-(DMA拷贝)->协议引擎

零拷贝:硬盘-(DMA拷贝)->内核缓冲区-(DMA拷贝)->协议引擎

6、为什么使用Netty

解决原生NIO存在的问题:学习成本高,开发效率低,NettyJDK自带的NIOAPI 进行了封装

  • 设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型-单线程,一个或多个线程池。
  • 使用方便:详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK5(Netty3.x)6(Netty4.x)就足够了。
  • 高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
  • 安全:完整的 SSL/TLSStartTLS 支持。
  • 社区活跃、不断更新:社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入。

7、netty的原理

在这里插入图片描述

  • Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写
  • BossGroupWorkerGroup 类型都是 NioEventLoopGroup
  • NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop
  • NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯
  • NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop
  • 每个 BossNioEventLoop 循环执行的步骤有 3
    • 轮询 accept 事件
    • 处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到某个 worker NIOEventLoop 上的 Selector
    • 处理任务队列的任务,即 runAllTasks
  • 每个 Worker NIOEventLoop 循环执行的步骤
    • 轮询 readwrite 事件
    • 处理 I/O 事件,即 readwrite 事件,在对应 NioScocketChannel 处理
    • 处理任务队列的任务,即 runAllTasks
  • 每个 Worker NIOEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel,即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器

8、netty源码解读:

8.1 Netty 启动过程梳理

  • 创建 2EventLoopGroup 线程池数组。数组默认大小 CPU * 2,方便 chooser 选择线程池时提高性能
  • BootStrapboss 设置为 group 属性,将 worker 设置为 childer 属性
  • 通过 bind 方法启动,内部重要方法为 initAndRegisterdobind 方法
  • initAndRegister 方法会反射创建 NioServerSocketChannel 及其相关的 NIO 的对象,pipelineunsafe,同时也为 pipeline 初始了 head 节点和 tail 节点。
  • register0 方法成功以后调用在 dobind 方法中调用 doBind0 方法,该方法会调用 NioServerSocketChanneldoBind 方法对 JDKchannel 和端口进行绑定,完成 Netty 服务器的所有启动,并开始监听连接事件。

8.2 Netty 接受请求过程梳理

总体流程:接受连接 --> 创建一个新的 NioSocketChannel --> 注册到一个 workerEventLoop 上 --> 注册 selecotRead 事件。

  • 服务器轮询 Accept 事件,获取事件后调用 unsaferead 方法,这个 unsafeServerSocket 的内部类,该方法内部由 2 部分组成
  • doReadMessages 用于创建 NioSocketChannel 对象,该对象包装 JDKNioChannel 客户端。该方法会像创建 ServerSocketChanel 类似创建相关的 pipelineunsafeconfig
  • 随后执行执行 pipeline.fireChannelRead 方法,并将自己绑定到一个 chooser 选择器选择的 workerGroup 中的一个 EventLoop。并且注册一个 0,表示注册成功,但并没有注册读(1)事件

9、RPC

在这里插入图片描述

PRC 调用流程说明

  1. 服务消费方(client)以本地调用方式调用服务
  2. client stub 接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体
  3. client stub 将消息进行编码并发送到服务端
  4. server stub 收到消息后进行解码
  5. server stub 根据解码结果调用本地的服务
  6. 本地服务执行并将结果返回给 server stub
  7. server stub 将返回导入结果进行编码并发送至消费方
  8. client stub 接收到消息并进行解码
  9. 服务消费方(client)得到结果

小结:RPC 的目标就是将 2 - 8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用

Dubbo 底层使用了 Netty 作为网络通讯框架,可用 Netty 实现一个简单的 RPC 框架

@环境命令

1、Homebrew

参考

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/576020
推荐阅读
相关标签