开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

springboot中mybatis多数据源动态切换实现

发表于 2021-07-20

在开发中,动态数据源配置还是用的比较多的,比如在多数据源使用方面,又或者是在多个DB之间切换方面。这里给出一个动态数据源的配置方案,两个DB均以mysql为例。

多数据源配置引入

mybatis和mysql在springboot中的引入这里就不在说了,不了解的可以参见springboot中mysql与mybatis的引入。

数据源配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
yml复制代码datasource:
master:
type: com.alibaba.druid.pool.DruidDataSource
jdbc-url: jdbc:mysql://127.0.0.1:3306/sbac_master?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
log:
type: com.alibaba.druid.pool.DruidDataSource
jdbc-url: jdbc:mysql://127.0.0.1:3306/sbac_log?autoReconnect=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver

mybatis的配置引入如下:

1
2
3
yml复制代码mybatis:
config-location: classpath:mybatis-config.xml
mapper-locations: classpath:com/lazycece/sbac/mysql/data/dao/*/mapper/*.xml

这里已然使用的是springboot的自动配置功能配置mybatis信息,只是手动指定了数据源的。如下所示,指定了master和log两个数据源,设置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
28
29
30
java复制代码@Configuration
public class MultiDataSource {

public static final String MASTER_DATA_SOURCE = "masterDataSource";
public static final String LOG_DATA_SOURCE = "logDataSource";

@Bean(name = MultiDataSource.MASTER_DATA_SOURCE)
@ConfigurationProperties(prefix = "datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}

@Bean(name = MultiDataSource.LOG_DATA_SOURCE)
@ConfigurationProperties(prefix = "datasource.log")
public DataSource logDataSource() {
return DataSourceBuilder.create().build();
}

@Primary
@Bean(name = "dynamicDataSource")
public DynamicDataSource dataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setDefaultTargetDataSource(masterDataSource());
Map<Object, Object> dataSourceMap = new HashMap<>(4);
dataSourceMap.put(MASTER_DATA_SOURCE, masterDataSource());
dataSourceMap.put(LOG_DATA_SOURCE, logDataSource());
dynamicDataSource.setTargetDataSources(dataSourceMap);
return dynamicDataSource;
}
}

动态数据源路由实现

引入了配置信息之后,便是该说如何实现多数据源切换了。我们是通过实现AbstractRoutingDataSource类的determineCurrentLookupKey方法来实现数据源的动态路由,设置ThreadLocal线程保护变量存储数据源key,确保线程间不受影响。

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
java复制代码package com.lazycece.sbac.mysql.multi.config;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
* @author lazycece
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSource.class);

private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>();

static void changeDataSource(String dataSourceKey) {
DATA_SOURCE_KEY.set(dataSourceKey);
}

static void clearDataSource() {
DATA_SOURCE_KEY.remove();
}

@Override
protected Object determineCurrentLookupKey() {
String key = DATA_SOURCE_KEY.get();
LOGGER.info("current data-source is {}", key);
return key;
}
}

随后,便是用AOP的方式来实现数据源的动态切换,注解和切面定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataSource {

String value();
}

@Component
@Aspect
public class DataSourceConfig {

@Before("@annotation(dataSource)")
public void beforeSwitchDataSource(DataSource dataSource) {
DynamicDataSource.changeDataSource(dataSource.value());
}

@After("@annotation(DataSource)")
public void afterSwitchDataSource() {
DynamicDataSource.clearDataSource();
}
}

动态数据源切换使用

动态数据源切换只需要在业务中使用@DataSource注解来标明需要使用的数据源即可,如下所示(这里只贴出关键代码):

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
java复制代码@Service
public class DynamicDataSourceServiceImpl implements DynamicDataSourceService {

@Resource
private UserDao userDao;
@Resource
private SystemLogDao systemLogDao;

@Override
@DataSource(value = MultiDataSource.MASTER_DATA_SOURCE)
public void addUserInfo(User user) {
userDao.insert(user);
}

@Override
@DataSource(value = MultiDataSource.MASTER_DATA_SOURCE)
public User getUserInfo(String username) {
return userDao.findByUsername(username);
}

@Override
@DataSource(value = MultiDataSource.LOG_DATA_SOURCE)
public void addSystemLog(SystemLog systemLog) {
systemLogDao.insert(systemLog);
}

@Override
@DataSource(value = MultiDataSource.LOG_DATA_SOURCE)
public List<SystemLog> getSystemLogInfo(Date beginTime, Date endTime) {
return systemLogDao.findByCreateTime(beginTime, endTime);
}
}

案例源码

案例源码地址: github.com/lazycece/sp…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

IDEA(2021)最全常用快捷键《必须收藏》

发表于 2021-07-20

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

目录

前言:

新手必须掌握:

Ctrl:

Alt:

Shift:

Ctrl + Alt:

Ctrl + Shift:

Alt + Shift:

Ctrl + Shift + Alt:

其他:

前言:

IDEA对新手来说难,可能其中一个原因就是快捷键组合多而且复杂但是它也很全,基本所有功能都可以通过快捷键来完成,如果你掌握了所有IDEA的快捷键使用,那么你完全可以丢掉鼠标,而且不影响开发效率。达到开发事半功倍的效果。

新手必须掌握:

快捷键名称 快捷键介绍
Alt+Insert 快速生成构造器/Getter/Setter等
Ctrl+N 快速打开类
Ctrl+R 替换文本
Ctrl+F 查找文本
Ctrl+X 删除行
Ctrl+D 复制行
Ctrl+O 重写方法
Ctrl+I 实现方法
Ctrl+Y 删除当前行
Shift+Ente 向下插入新行
Ctrl+Shift+F 全局查找
Ctrl+”+/-” 当前方法展开、折叠
Ctrl+Shift+”+/-” 全部展开、折叠
Ctrl+Enter 上插一行

Ctrl:

Ctrl + F 当前文件文本查找
Ctrl + R 当前文件文本替换
Ctrl + Z 撤销
Ctrl + Y 删除光标行
Ctrl + X 剪切光标行
Ctrl + C 复制光标行
Ctrl + D 复制光标行
Ctrl + E 显示最近打开的文件记录列表
Ctrl + N 根据输入的类名查找类文件
Ctrl + G 在当前文件跳转到指定行处
Ctrl + J 插入自定义动态代码模板
Ctrl + P 方法参数提示显示
Ctrl + Q 光标所在的变量 / 类名 / 方法名等上面(也可以在提示补充的时候按),显示文档内容
Ctrl + U 前往当前光标所在的方法的父类的方法 / 接口定义
Ctrl + B 进入光标所在的方法/变量的接口或是定义出,等效于 Ctrl + 左键单击
Ctrl + H 显示当前类的层次结构
Ctrl + / 注释光标所在行代码,会根据当前不同文件类型使用不同的注释符号
Ctrl + [ 移动光标到当前所在代码的花括号开始位置
Ctrl + ] 移动光标到当前所在代码的花括号结束位置
Ctrl + F1 在光标所在的错误代码出显示错误信息
Ctrl + F3 调转到所选中的词的下一个引用位置
Ctrl + F4 关闭当前编辑文件
Ctrl + F8 Debug 模式下,设置光标当前行为断点,如果当前已经是断点则去掉断点
Ctrl + F9 执行 Make Project 操作
Ctrl + F11 选中文件 / 文件夹,使用助记符设定
Ctrl + F12 弹出当前文件结构层,可以在弹出的层上直接输入,进行筛选
Ctrl + Tab 编辑窗口切换,如果在切换的过程又加按上delete,则是关闭对应选中的窗口
Ctrl + Enter 智能分隔行
Ctrl + End 跳到文件尾
Ctrl + Home 跳到文件头
Ctrl + Space 基础代码补全,默认在 Windows 系统上被输入法占用,需要进行修改,建议修改为 Ctrl + 逗号 (必备)
Ctrl + Delete 删除光标后面的单词或是中文句
Ctrl + BackSpace 删除光标前面的单词或是中文句
Ctrl + 1,2,3…9 定位到对应数值的书签位置
Ctrl + 左键单击 在打开的文件标题上,弹出该文件路径
Ctrl + 光标定位 按 Ctrl 不要松开,会显示光标所在的类信息摘要
Ctrl + 左方向键 光标跳转到当前单词 / 中文句的左侧开头位置
Ctrl + 右方向键 光标跳转到当前单词 / 中文句的右侧开头位置
Ctrl + 前方向键 等效于鼠标滚轮向前效果
Ctrl + 后方向键 等效于鼠标滚轮向后效果

Alt:

Alt + ` 显示版本控制常用操作菜单弹出层
Alt + Q 弹出一个提示,显示当前类的声明 / 上下文信息
Alt + F1 显示当前文件选择目标弹出层,弹出层中有很多目标可以进行选择
Alt + F2 对于前面页面,显示各类浏览器打开目标选择弹出层
Alt + F3 选中文本,逐个往下查找相同文本,并高亮显示
Alt + F7 查找光标所在的方法 / 变量 / 类被调用的地方
Alt + F8 在 Debug 的状态下,选中对象,弹出可输入计算表达式调试框,查看该输入内容的调试结果
Alt + Home 定位 / 显示到当前文件的 Navigation Bar
Alt + Enter IntelliJ IDEA 根据光标所在问题,提供快速修复选择,光标放在的位置不同提示的结果也不同
Alt + Insert 代码自动生成,如生成对象的 set / get 方法,构造函数,toString() 等
Alt + 左方向键 按左方向切换当前已打开的文件视图
Alt + 右方向键 按右方向切换当前已打开的文件视图
Alt + 前方向键 当前光标跳转到当前文件的前一个方法名位置

Shift:

Shift + F1 如果有外部文档可以连接外部文档
Shift + F2 跳转到上一个高亮错误 或 警告位置
Shift + F3 在查找模式下,查找匹配上一个
Shift + F4 对当前打开的文件,使用新Windows窗口打开,旧窗口保留
Shift + F6 对文件 / 文件夹 重命名
Shift + F7 在 Debug 模式下,智能步入。断点所在行上有多个方法调用,会弹出进入哪个方法
Shift + F8 在 Debug 模式下,跳出,表现出来的效果跟 F9 一样
Shift + F9 等效于点击工具栏的 Debug 按钮
Shift + F10 等效于点击工具栏的 Run 按钮
Shift + F11 弹出书签显示层
Shift + Tab 取消缩进
Shift + ESC 隐藏当前 或 最后一个激活的工具窗口

Ctrl + Alt:

Ctrl + Alt + L 格式化代码,可以对当前文件和整个包目录使用
Ctrl + Alt + O 优化导入的类,可以对当前文件和整个包目录使用
Ctrl + Alt + I 光标所在行 或 选中部分进行自动代码缩进,有点类似格式化
Ctrl + Alt + T 对选中的代码弹出环绕选项弹出层
Ctrl + Alt + J 弹出模板选择窗口,讲选定的代码加入动态模板中
Ctrl + Alt + H 调用层次
Ctrl + Alt + B 在某个调用的方法名上使用会跳到具体的实现处,可以跳过接口
Ctrl + Alt + V 快速引进变量
Ctrl + Alt + Y 同步、刷新
Ctrl + Alt + S 打开 IntelliJ IDEA 系统设置
Ctrl + Alt + F7 显示使用的地方。寻找被该类或是变量被调用的地方,用弹出框的方式找出来
Ctrl + Alt + F11 切换全屏模式
Ctrl + Alt + Enter 光标所在行上空出一行,光标定位到新行
Ctrl + Alt + Home 弹出跟当前文件有关联的文件弹出层
Ctrl + Alt + Space 类名自动完成

Ctrl + Shift:

Ctrl + Shift + F 根据输入内容查找整个项目 或 指定目录内文件
Ctrl + Shift + R 根据输入内容替换对应内容,范围为整个项目 或 指定目录内文件
Ctrl + Shift + J 自动将下一行合并到当前行末尾
Ctrl + Shift + Z 取消撤销
Ctrl + Shift + W 递进式取消选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展取消选中范围
Ctrl + Shift + N 通过文件名定位 / 打开文件 / 目录,打开目录需要在输入的内容后面多加一个正斜杠
Ctrl + Shift + U 对选中的代码进行大 / 小写轮流转换
Ctrl + Shift + T 对当前类生成单元测试类,如果已经存在的单元测试类则可以进行选择
Ctrl + Shift + C 复制当前文件磁盘路径到剪贴板
Ctrl + Shift + V 弹出缓存的最近拷贝的内容管理器弹出层
Ctrl + Shift + E 显示最近修改的文件列表的弹出层
Ctrl + Shift + H 显示方法层次结构
Ctrl + Shift + B 跳转到类型声明处
Ctrl + Shift + I 快速查看光标所在的方法 或 类的定义
Ctrl + Shift + A 查找动作 / 设置
Ctrl + Shift + F7 高亮显示所有该选中文本,按Esc高亮消失
Ctrl + Shift + F8 在 Debug 模式下,指定断点进入条件
Ctrl + Shift + F9 编译选中的文件 / 包 / Module
Ctrl + Shift + F12 编辑器最大化
Ctrl + Shift + Space 智能代码提示
Ctrl + Shift + Enter 自动结束代码,行末自动添加分号

Alt + Shift:

Alt + Shift + N 选择 / 添加 task
Alt + Shift + F 显示添加到收藏夹弹出层
Alt + Shift + C 查看最近操作项目的变化情况列表
Alt + Shift + F 添加到收藏夹
Alt + Shift + I 查看项目当前文件
Alt + Shift + F7 在 Debug 模式下,下一步,进入当前方法体内,如果方法体还有方法,则会进入该内嵌的方法中,依此循环进入
Alt + Shift + F9 弹出 Debug 的可选择菜单
Alt + Shift + 前方向键 移动光标所在行向上移动
Alt + Shift + 后方向键 移动光标所在行向下移动

其他:

F2 跳转到下一个高亮错误 或 警告位置
F3 在查找模式下,定位到下一个匹配处
F4 编辑源
F7 在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则进入当前方法体内,如果该方法体还有方法,则不会进入该内嵌的方法中
F8 在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则不进入当前方法体内
F9 在 Debug 模式下,恢复程序运行,但是如果该断点下面代码还有断点则停在下一个断点上
F11 添加书签
F12 回到前一个工具窗口
Tab 缩进
ESC 从工具窗口进入代码文件窗口

好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,下期见~~


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文把Servlet整的明明白白 一、Cookie 二、Se

发表于 2021-07-20

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

一、Cookie

1.1、Cookie概述

​ Cookie(饼干) 是服务器通知客户端保存键值对的一种技术。客户端有了 Cookie 后,每次请求都发送给服务器。每个 Cookie 的大小不能超过 4kb。

1.2、创建Cookie

image-20210118212323112

1
2
3
4
5
6
java复制代码protected void createCookie(HttpServletRequest req, HttpServletResponse resp) throws ServletException,	IOException {
//1 创建 Cookie 对象
Cookie cookie = new Cookie("key","value");
//2 相应给浏览器(客户端)
Cookie cookie1 = new Cookie("key2","value2");
}

1.3、服务器获取Cookie

image-20210118214300401

​ 服务器获取客户端的 Cookie 只需要一行代码:req.getCookies();,他返回的是一个Cookie数组。

1.4、Cookie工具类

​ 我们只做一个工具类,用于查找指定名称的 Cookie 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码public class CookieUtils {
/**
* 查找指定名称的 Cookie 对象
* @param name
* @param cookies
* @return
*/
public static Cookie findCookie(String name , Cookie[] cookies){
if (name == null || cookies == null || cookies.length == 0) {
return null;
}
for (Cookie cookie : cookies) {
if (name.equals(cookie.getName())) {//getName 方法返回 Cookie 的 key(名)
return cookie;// getValue 方法返回 Cookie 的 value 值
}
}
return null;
}
}

1.5、Cookie值的修改

1.5.1、方案一

  1. 先创建一个要修改的同名(指的就是 key)的 Cookie 对象。
  2. 调用 response.addCookie( Cookie );
1
2
3
4
java复制代码// Cookie cookie = new Cookie("key1","Value1");
Cookie cookie = new Cookie("key1","newValue1");
// 调用 response.addCookie( Cookie ); 通知 客户端 保存修改
resp.addCookie(cookie);

1.5.2、方案二

  1. 先查找到需要修改的 Cookie 对象。
  2. 调用 setValue()方法赋于新的 Cookie 值。

1.6、浏览器查看Cookie

1.6.1、谷歌

image-20210119142227292

1.6.2、火狐

image-20210119142241876

1.7、Cookie的生命控制

​ 我们可以管理Cookie 什么时候被销毁,这就是Cookie的生命控制。我们用setMaxAge()方法来控制。

参数值 意义
正数 表示在指定的秒数后过期
负数 表示浏览器一关闭,Cookie 就会被删除(默认值是-1)
零 表示马上删除Cookie
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
java复制代码/**
* 设置存活 1 个小时的 Cooie
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void life3600(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
Cookie cookie = new Cookie("life3600", "life3600");
cookie.setMaxAge(60 * 60); // 设置 Cookie 一小时之后被删除。无效
resp.addCookie(cookie);
resp.getWriter().write("已经创建了一个存活一小时的 Cookie");
}

/**
* 马上删除一个 Cookie
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void deleteNow(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
// 先找到你要删除的 Cookie 对象
Cookie cookie = CookieUtils.findCookie("key4", req.getCookies());
if (cookie != null) {
// 调用 setMaxAge(0);
cookie.setMaxAge(0); // 表示马上删除,都不需要等待浏览器关闭
// 调用 response.addCookie(cookie);
resp.addCookie(cookie);
resp.getWriter().write("key4 的 Cookie 已经被删除");
}
}

/**
* 默认的会话级别的 Cookie
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void defaultLife(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
Cookie cookie = new Cookie("defalutLife","defaultLife");
cookie.setMaxAge(-1);//设置存活时间,浏览器一关闭就销毁
resp.addCookie(cookie);
}

1.8、Cookie 有效路径的设置

​ Cookie 的 path 属性可以有效的过滤哪些 Cookie 可以发送给服务器,哪些不发。path 属性是通过请求的地址来进行有效的过滤。

二、Session

2.1、Session概述

​ Session 就是会话,Session 本质上是一个借口(HttpSession)。它是用来维护一个客户端和服务器之间关联的一种技术。

​ 每个客户端都有自己的一个 Session 会话,我们经常用来保存用户登录之后的信息。

2.2、Session 域数据的存取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* 往 Session 中保存数据
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void setAttribute(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
req.getSession().setAttribute("key1", "value1");
resp.getWriter().write("已经往 Session 中保存了数据");
}
/**
* 获取 Session 域中的数据
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
protected void getAttribute(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
Object attribute = req.getSession().getAttribute("key1");
resp.getWriter().write("从 Session 中获取出 key1 的数据是:" + attribute);
}

2.3、Session 生命周期控制

​ public void setMaxInactiveInterval(int interval):设置 Session 的超时时间(以秒为单位),超过指定的时长,Session就会被销毁。值为正数的时候,设定 Session 的超时时长。负数表示永不超时(极少使用)。

​ public int getMaxInactiveInterval():获取 Session 的超时时间。Session 默认的超时时间长为 30 分钟。

​ public void invalidate() :让当前Session 会话马上超时无效。

image-20210119160945481

1
2
3
4
5
6
7
8
9
10
java复制代码protected void life3(HttpServletRequest req, HttpServletResponse resp) throws ServletException,IOException {
// 先获取 Session 对象
HttpSession session = req.getSession();
// 设置当前 Session3 秒后超时
session.setMaxInactiveInterval(3);
resp.getWriter().write("当前 Session 已经设置为 3 秒后超时");
// 让 Session 会话马上超时
session.invalidate();
resp.getWriter().write("Session 已经设置为超时(无效)");
}

2.4、Session原理

​ Session 技术,底层其实是基于 Cookie 技术来实现的。

image-20210119161129071

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java封装OkHttp3工具类

发表于 2021-07-20

Java封装OkHttp3工具类,适用于Java后端开发者

说实在话,用过挺多网络请求工具,有过java原生的,HttpClient3和4,但是个人感觉用了OkHttp3之后,之前的那些完全不想再用了。怎么说呢,代码轻便,使用起来很很很灵活,响应快,比起HttpClient好用许多。当然,这些是我个人观点,不喜勿喷。

准备工作

Maven项目在pom文件中引入jar包

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.10.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>

引入json是因为工具类中有些地方用到了,现在通信都流行使用json传输,也少不了要这个jar包

工具类代码

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
scss复制代码import com.alibaba.fastjson.JSON;
import okhttp3.*;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.URLEncoder;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class OkHttpUtils {
private static volatile OkHttpClient okHttpClient = null;
private static volatile Semaphore semaphore = null;
private Map<String, String> headerMap;
private Map<String, String> paramMap;
private String url;
private Request.Builder request;

/**
* 初始化okHttpClient,并且允许https访问
*/
private OkHttpUtils() {
if (okHttpClient == null) {
synchronized (OkHttpUtils.class) {
if (okHttpClient == null) {
TrustManager[] trustManagers = buildTrustManagers();
okHttpClient = new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.sslSocketFactory(createSSLSocketFactory(trustManagers), (X509TrustManager) trustManagers[0])
.hostnameVerifier((hostName, session) -> true)
.retryOnConnectionFailure(true)
.build();
addHeader("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36");
}
}
}
}

/**
* 用于异步请求时,控制访问线程数,返回结果
*
* @return
*/
private static Semaphore getSemaphoreInstance() {
//只能1个线程同时访问
synchronized (OkHttpUtils.class) {
if (semaphore == null) {
semaphore = new Semaphore(0);
}
}
return semaphore;
}

/**
* 创建OkHttpUtils
*
* @return
*/
public static OkHttpUtils builder() {
return new OkHttpUtils();
}

/**
* 添加url
*
* @param url
* @return
*/
public OkHttpUtils url(String url) {
this.url = url;
return this;
}

/**
* 添加参数
*
* @param key 参数名
* @param value 参数值
* @return
*/
public OkHttpUtils addParam(String key, String value) {
if (paramMap == null) {
paramMap = new LinkedHashMap<>(16);
}
paramMap.put(key, value);
return this;
}

/**
* 添加请求头
*
* @param key 参数名
* @param value 参数值
* @return
*/
public OkHttpUtils addHeader(String key, String value) {
if (headerMap == null) {
headerMap = new LinkedHashMap<>(16);
}
headerMap.put(key, value);
return this;
}

/**
* 初始化get方法
*
* @return
*/
public OkHttpUtils get() {
request = new Request.Builder().get();
StringBuilder urlBuilder = new StringBuilder(url);
if (paramMap != null) {
urlBuilder.append("?");
try {
for (Map.Entry<String, String> entry : paramMap.entrySet()) {
urlBuilder.append(URLEncoder.encode(entry.getKey(), "utf-8")).
append("=").
append(URLEncoder.encode(entry.getValue(), "utf-8")).
append("&");
}
} catch (Exception e) {
e.printStackTrace();
}
urlBuilder.deleteCharAt(urlBuilder.length() - 1);
}
request.url(urlBuilder.toString());
return this;
}

/**
* 初始化post方法
*
* @param isJsonPost true等于json的方式提交数据,类似postman里post方法的raw
* false等于普通的表单提交
* @return
*/
public OkHttpUtils post(boolean isJsonPost) {
RequestBody requestBody;
if (isJsonPost) {
String json = "";
if (paramMap != null) {
json = JSON.toJSONString(paramMap);
}
requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
} else {
FormBody.Builder formBody = new FormBody.Builder();
if (paramMap != null) {
paramMap.forEach(formBody::add);
}
requestBody = formBody.build();
}
request = new Request.Builder().post(requestBody).url(url);
return this;
}

/**
* 同步请求
*
* @return
*/
public String sync() {
setHeader(request);
try {
Response response = okHttpClient.newCall(request.build()).execute();
assert response.body() != null;
return response.body().string();
} catch (IOException e) {
e.printStackTrace();
return "请求失败:" + e.getMessage();
}
}

/**
* 异步请求,有返回值
*/
public String async() {
StringBuilder buffer = new StringBuilder("");
setHeader(request);
okHttpClient.newCall(request.build()).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
buffer.append("请求出错:").append(e.getMessage());
}

@Override
public void onResponse(Call call, Response response) throws IOException {
assert response.body() != null;
buffer.append(response.body().string());
getSemaphoreInstance().release();
}
});
try {
getSemaphoreInstance().acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
return buffer.toString();
}

/**
* 异步请求,带有接口回调
*
* @param callBack
*/
public void async(ICallBack callBack) {
setHeader(request);
okHttpClient.newCall(request.build()).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
callBack.onFailure(call, e.getMessage());
}

@Override
public void onResponse(Call call, Response response) throws IOException {
assert response.body() != null;
callBack.onSuccessful(call, response.body().string());
}
});
}

/**
* 为request添加请求头
*
* @param request
*/
private void setHeader(Request.Builder request) {
if (headerMap != null) {
try {
for (Map.Entry<String, String> entry : headerMap.entrySet()) {
request.addHeader(entry.getKey(), entry.getValue());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}


/**
* 生成安全套接字工厂,用于https请求的证书跳过
*
* @return
*/
private static SSLSocketFactory createSSLSocketFactory(TrustManager[] trustAllCerts) {
SSLSocketFactory ssfFactory = null;
try {
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new SecureRandom());
ssfFactory = sc.getSocketFactory();
} catch (Exception e) {
e.printStackTrace();
}
return ssfFactory;
}

private static TrustManager[] buildTrustManagers() {
return new TrustManager[]{
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) {
}

@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) {
}

@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[]{};
}
}
};
}

/**
* 自定义一个接口回调
*/
public interface ICallBack {

void onSuccessful(Call call, String data);

void onFailure(Call call, String errorMsg);

}
}

使用教程

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
typescript复制代码public static void main(String[] args) {
// get请求,方法顺序按照这种方式,切记选择post/get一定要放在倒数第二,同步或者异步倒数第一,才会正确执行
OkHttpUtils.builder().url("请求地址,http/https都可以")
// 有参数的话添加参数,可多个
.addParam("参数名", "参数值")
.addParam("参数名", "参数值")
// 也可以添加多个
.addHeader("Content-Type", "application/json; charset=utf-8")
.get()
// 可选择是同步请求还是异步请求
//.async();
.sync();

// post请求,分为两种,一种是普通表单提交,一种是json提交
OkHttpUtils.builder().url("请求地址,http/https都可以")
// 有参数的话添加参数,可多个
.addParam("参数名", "参数值")
.addParam("参数名", "参数值")
// 也可以添加多个
.addHeader("Content-Type", "application/json; charset=utf-8")
// 如果是true的话,会类似于postman中post提交方式的raw,用json的方式提交,不是表单
// 如果是false的话传统的表单提交
.post(true)
.sync();

// 选择异步有两个方法,一个是带回调接口,一个是直接返回结果
OkHttpUtils.builder().url("")
.post(false)
.async();

OkHttpUtils.builder().url("").post(false).async(new OkHttpUtils.ICallBack() {
@Override
public void onSuccessful(Call call, String data) {
// 请求成功后的处理
}

@Override
public void onFailure(Call call, String errorMsg) {
// 请求失败后的处理
}
});
}

结语

封装的明明白白,使用的简简单单,简单的几下就能做请求,用建造者模式是真的舒服

作者:如漩涡
来源:blog.csdn.net/m0_37701381

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java菜鸟教程,Java学习路线图(视频+笔记+工具)

发表于 2021-07-20

不知不觉踏入互联网行业已经四年了。

回顾当初第一次接触Java,是在大学的课堂,晦涩难懂的知识点,现在还能想起被期末考试支配的恐惧。

直到踏入互联网这个行业,再回想一下自己整个入行到工作的历程,总结了一下学习Java的路线,希望可以帮到各位初入行的Java小白。

一、大纲

多数事情,都要有目标的行动,行动为了完成目标。还可以把目标写下来或打印出来,贴到身边明显的地方,有利于督促自己。

image.png

学Java,但是不能只学Java,因为在计算机这棵大树中,Java只是一片叶子。

一个合格的Coder,除了Java,还需要熟悉操作系统、计算机网络、数据库、前端、中间件、框架等等这些东西,如果要成为一个Top Coder,项目管理、英语、沟通、算法也很重要。

就针对就业来说,个人觉得掌握 Java基础、计算机基础、工具的使用、数据库、web前端,Javaweb,框架使用、Linux、中间件,就算一个入门级的Coder了。

一、Java基础

我把 Java 基础部分真正要学的内容列一下。

01、Java 简介

  • Java语言概述
  • Java中JDK、JRE、JVM三者之间的关系
  • Java中public class与class
  • Java标识符与关键字
  • Java 变量
  • Java 数据类型
  • Java运算符
  • Java 表达式 & 语句 & 代码块
  • Java中的注释

02、Java 控制语句

  • Java if else
  • Java switch 语句
  • Java for 循环
  • Java while 循环
  • Java break 语句
  • Java continue 语句

03、Java 数组

  • Java 数组
  • 多维数组
  • Java 数组复制

04、Java 面向对象(1)

  • Java 类和对象
  • Java 方法
  • Java 方法重载
  • Java 构造方法
  • Java 字符串
  • Java 访问权限
  • Java this 关键字
  • Java final 关键字
  • Java 递归
  • Java instanceof 操作符

05、Java 面向对象(2)

  • Java 继承
  • Java 方法重写
  • Java super 关键字
  • 抽象类 & 抽象方法
  • Java 接口
  • Java 多态
  • Java 封装

06、Java 面向对象(3)

  • 嵌套&内部类
  • Java static 关键字
  • Java 匿名内部类
  • Java 单例
  • Java 枚举类
  • Java 枚举构造方法
  • Java 枚举字符串
  • Java 反射

07、Java 异常处理

  • Java 异常简介
  • Java 异常处理
  • Java try catch
  • Java throw 和 throws
  • Java 捕获多个异常
  • Java try-with-resources
  • Java 注解
  • Java 注解类型
  • Java 日志
  • Java 断言

08、Java 集合

  • Java 集合框架
  • Java 集合接口
  • Java List 接口
  • Java ArrayList
  • Java Vector
  • Java Stack

09、Java 队列

  • Java 队列接口
  • Java 优先级队列
  • Java 双端队列接口
  • Java LinkedList
  • Java 数组队列
  • Java 阻塞队列接口
  • Java ArrayBlockingQueue
  • Java LinkedBlockingQueue

10、Java Map

  • Java Map 接口
  • Java HashMap
  • Java LinkedHashMap
  • Java WeakHashMap
  • Java EnumMap
  • Java SortedMap 接口
  • Java NavigableMap 接口
  • Java TreeMap
  • Java ConcurrentMap 接口
  • Java ConcurrentHashMap

11、Java Set

  • Java Set 接口
  • Java HashSet
  • Java EnumSet
  • Java LinkedHashSet
  • Java SortedSet 接口
  • Java NavigableSet 接口
  • Java TreeSet
  • Java 集合算法
  • Java 迭代器接口
  • Java ListIterator 接口

12、Java 字节流

  • Java IO 流简介
  • Java InputStream
  • Java OutputStream
  • Java FileInputStream
  • Java FileOutputStream
  • Java ByteArrayInputStream
  • Java ByteArrayOutputStream
  • Java ObjectInputStream
  • Java ObjectOutputStream
  • Java BufferedInputStream
  • Java BufferedOutputStream
  • Java PrintStream

13、Java 字符流

  • Java Reader
  • Java Writer
  • Java InputStreamReader
  • Java OutputStreamWriter
  • Java FileReader
  • Java FileWriter
  • Java BufferedReader
  • Java BufferedWriter
  • Java StringWriter
  • Java PrintWriter

14、Java 并发编程

  • 进程与线
  • 多线程的入门类和接口
  • 线程组和线程优先级
  • 线程的状态及主要转化方法
  • 线程间的通信
  • 重排序和 happens-before
  • volatile
  • synchronized 与锁
  • CAS 与原子操作
  • AQS
  • 计划任务
  • Stream 并行计算原理
  • Frok/Join
  • 通信工具类
  • CopyOnWrite
  • 并发集合容器
  • 锁接口和类
  • 阻塞队列
  • 线程池原理

15、Java 虚拟机

  • Java 内存结构
  • 堆
  • 栈
  • 垃圾回收
  • JVM 内存区域
  • Java 虚拟机栈
  • class 文件
  • 字节码指令
  • JVM 参数调优
  • Java 对象模型
  • HotSpot
  • 类加载机制
  • 编译和反编译
  • 反编译工具(javap)
  • JIT
  • 虚拟机性能监控和故障处理工具(jps、jstack、jmap、jstat、jconsole、javap)

怎么系统化的学习呢?

推荐一套视频,动力节点老杜讲的Java零基础教程,在 B 站上看。

www.bilibili.com/video/BV1Rx…

二、计算机基础

计算机基础都包括哪些呢?

计算机组成原理、操作系统、计算机网络、数据结构与算法。

计算机组成原理

先说计算机组成原理,这部分内容主要涉及

  • 计算机系统概述
  • 数据与运算
  • CPU 概述
  • 存储子系统概述
  • 总线和 IO 概述

计算机基础知识的学习建议学习《计算机专业导论》

计算机专业导论可以帮助你对即将学习的学科有一个大致的了解,知识注重广度而非深度。

软件工程专业的同学也可以去看《软件工程导论》

三、工具的使用

如果你既想写出质量杠杠的 Java 代码,又想追求开发效率,用 Intellij IDEA 准没错!

可以去 B 站上看一下这个 Intellij IDEA 的教学视频。

www.bilibili.com/video/BV14t…

大家都知道,版本控制系统非常重要!!!!!!

即便你只是一个人在编码,它也可以帮助你创建项目的快照、记录每个改动、创建不同的分支等等。

如果你参与的是多人协作,它更是一个无价之宝,你不仅可以看到别人对代码的修改,还可以同时解决由于并行开发带来的冲突。

版本控制系统有很多,其中最突出的代表就是 Git。

想要把 Git 学好的话,可以看看这套Git教学视频。

www.bilibili.com/video/BV1iv…

四、数据库

Java 实习工作,不外乎增删改查嘛,不要抱太多幻想,基本上任何一个人的实习经历,都是从 CRUD 开始的。 要学习MySQL 的话,推荐看下边这套

www.bilibili.com/video/BV1fx…

B站上很经典的视频教程,好评如潮,涵盖MySQL的全部知识点了

这一套组合拳打下来,找一份实习工作我认为是完全没问题了。

这也是一个 Java 后端程序员必须掌握的技能点,缺一不可!

五、web前端

虽然是作为Java后端开发Coder,但是面对一个完整的项目,与前端有着不可或缺的关系。

简单的前端知识我们还是需要了解的。

还有就是,也不是所有的公司都是区分前后端的,全栈工程师显然更厉害。

前端基础技术(HTML/CSS/JavaScript)

HTML:

www.bilibili.com/video/BV11t…

CSS:

www.bilibili.com/video/BV1tt…

JavaScript:

www.bilibili.com/video/BV1Ft…

另外推荐一些你会遇到的知识学习教程

Linux基础知识(用于做web服务器)

www.bilibili.com/video/BV1Li…

Vue.js(最容易上手的前端框架)

www.bilibili.com/video/BV1q5…

六、JavaWeb

Web阶段过后,就是JavaWeb了。

推荐这两套视频,不同版本的,直接跟着视频学

JavaWeb【IDEA版本】

www.bilibili.com/video/BV1Yz…

JavaWeb【Eclipse经典版】

www.bilibili.com/video/BV18z…

七、框架

要找到一份 Java 实习工作的话,Spring 的系列框架是要懂一些,不要求多熟练,如果可以掌握一个框架,尤其是 Spring Boot,那对你也有帮助。因为如果你不会这玩意的话,基本上是做不了项目的。

主要涉及的内容有:

  • Spring
  • Spring MVC
  • MyBatis
  • Spring Boot

关于 SSM(Spring+Spring MVC+MyBatis)的学习,可以看下面这个视频。

www.bilibili.com/video/BV1Ug…

关于 Spring Boot 的学习,可以看下边这个视频

www.bilibili.com/video/BV1pK…

学了 SSM + Spring Boot,就可以上手实战项目了,像 GitHub 上的 vhr 和 mall,都是不错的练手项目,强烈推荐。

八、互联网分布式技术

这时候你的水平还是仅仅存留在对框架的简单运用上,要想进一步学习,还要找一些框架的源码,进行深入了解。

除此之外还有这个时候的你应该对设计模式了如指掌,还需要看一些关于代码编写优化的书,提高自己的代码能力。

可以学习分布式架构、微服务等提升自己的技术。

随着我们的业务量越来越大和越重要,单体的架构模式已经无法对应大规模的应用场景,而且系统中决不能存在单点故障导致整体不可用,所以只有垂直或是水平拆分业务系统,使其形成一个分布式的架构,利用分布式架构来冗余系统消除单点的故障,从而提高整个系统的可用性。

同时分布式系统的模块重用度更高,速度更快,扩展性更高是大型的项目必不可少的环节。

而微服务架构引入策略 – 对传统企业而言,开始时可以考虑引入部分合适的微服务架构原则对已有系统进行改造或新建微服务应用,逐步探索及积累微服务架构经验,而非全盘实施微服务架构。

既然已经踏入互联网圈子,成为程序员中的一员,就要秉持“一直学习”的观念,经常更新自己的技术库,对于专业的java程序员来说真的不夸张!

推荐视频:

  • Dubbo视频教程:www.bilibili.com/video/BV1Sk…
  • Redis视频教程:www.bilibili.com/video/BV14t…
  • Maven多模块管理:www.bilibili.com/video/BV1kg…
  • Linux视频教程:www.bilibili.com/video/BV1dt…
  • SpringCloud视频教程:www.bilibili.com/video/BV1ZV…
  • SpringCloud Alibaba视频教程:www.bilibili.com/video/BV1nK…
  • Nginx视频教程:www.bilibili.com/video/BV11V…
  • Spring Session视频教程:www.bilibili.com/video/BV1iK…
  • FastDFS视频教程:www.bilibili.com/video/BV1ta…
  • RabbitMQ视频教程:www.bilibili.com/video/BV1Ap…
  • MyCat视频教程:www.bilibili.com/video/BV1gK…
  • Docker视频教程:www.bilibili.com/video/BV1QA…
  • Kubernetes(k8s)视频教程:www.bilibili.com/video/BV1KU…
  • MySQL集群视频教程:www.bilibili.com/video/BV1Rg…
  • SVN视频教程:www.bilibili.com/video/BV1ux…
  • Apache Shiro视频教程:www.bilibili.com/video/BV14T…
  • 大型互联网电商项目:www.bilibili.com/video/BV1RQ…

总之,先把主要知识点掌握彻底掌握,慢一点是正常的,求快得不偿失,等把底层基础知识打牢,在学东西的时候就会快很多,以后完全可以多找项目练手,学习效率嗖嗖的,有时候慢就是快。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java并发:轻轻松松吃透fork/join 一、概述 二、

发表于 2021-07-20

Fork / Join 是一个工具框架 , 其核心思想在于将一个大运算切成多个小份 , 最大效率的利用资源 , 其主要涉及到三个类 : ForkJoinPool / ForkJoinTask / RecursiveTask

QQ截图20210720145823.png

一、概述

ava.util.concurrent.ForkJoinPool由Java大师Doug Lea主持编写,它可以将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出。本文中对Fork/Join框架的讲解,基于JDK1.8+中的Fork/Join框架实现,参考的Fork/Join框架主要源代码也基于JDK1.8+。

文章将首先先谈谈recursive task,然后讲解Fork/Join框架的基本使用;接着结合Fork/Join框架的工作原理来理解其中需要注意的使用要点;最后再讲解使用Fork/Join框架解决一些实际问题。

二、说一说 RecursiveTask

RecursiveTask 是一种 ForkJoinTask 的递归实现 , 例如可以用于计算斐波那契数列 :

1
2
3
4
5
6
7
8
9
10
11
12
scala复制代码class Fibonacci extends RecursiveTask<Integer> {
final int n;
Fibonacci(int n) { this.n = n; }
Integer compute() {
if (n <= 1)
return n;
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
Fibonacci f2 = new Fibonacci(n - 2);
return f2.compute() + f1.join();
}
}

RecursiveTask 继承了 ForkJoinTask 接口 ,其内部有几个主要的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
csharp复制代码
// Node 1 : 返回结果 , 存放最终结果
V result;

// Node 2 : 抽象方法 compute , 用于计算最终结果
protected abstract V compute();

// Node 3 : 获取最终结果
public final V getRawResult() {
return result;
}

// Node 4 : 最终执行方法 , 这里是需要调用具体实现类compute
protected final boolean exec() {
result = compute();
return true;
}

三、 Fork/Join框架基本使用

这里是一个简单的Fork/Join框架使用示例,在这个示例中我们计算了1-1001累加后的值:

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
csharp复制代码/**
* 这是一个简单的Join/Fork计算过程,将1—1001数字相加
*/
public class TestForkJoinPool {

private static final Integer MAX = 200;

static class MyForkJoinTask extends RecursiveTask<Integer> {
// 子任务开始计算的值
private Integer startValue;

// 子任务结束计算的值
private Integer endValue;

public MyForkJoinTask(Integer startValue , Integer endValue) {
this.startValue = startValue;
this.endValue = endValue;
}

@Override
protected Integer compute() {
// 如果条件成立,说明这个任务所需要计算的数值分为足够小了
// 可以正式进行累加计算了
if(endValue - startValue < MAX) {
System.out.println("开始计算的部分:startValue = " + startValue + ";endValue = " + endValue);
Integer totalValue = 0;
for(int index = this.startValue ; index <= this.endValue ; index++) {
totalValue += index;
}
return totalValue;
}
// 否则再进行任务拆分,拆分成两个任务
else {
MyForkJoinTask subTask1 = new MyForkJoinTask(startValue, (startValue + endValue) / 2);
subTask1.fork();
MyForkJoinTask subTask2 = new MyForkJoinTask((startValue + endValue) / 2 + 1 , endValue);
subTask2.fork();
return subTask1.join() + subTask2.join();
}
}
}

public static void main(String[] args) {
// 这是Fork/Join框架的线程池
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Integer> taskFuture = pool.submit(new MyForkJoinTask(1,1001));
try {
Integer result = taskFuture.get();
System.out.println("result = " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace(System.out);
}
}
}

以上代码很简单,在关键的位置有相关的注释说明。这里本文再对以上示例中的要点进行说明。首先看看以上示例代码的可能执行结果:

1
2
3
4
5
6
7
8
9
ini复制代码开始计算的部分:startValue = 1;endValue = 126
开始计算的部分:startValue = 127;endValue = 251
开始计算的部分:startValue = 252;endValue = 376
开始计算的部分:startValue = 377;endValue = 501
开始计算的部分:startValue = 502;endValue = 626
开始计算的部分:startValue = 627;endValue = 751
开始计算的部分:startValue = 752;endValue = 876
开始计算的部分:startValue = 877;endValue = 1001
result = 501501

四、工作顺序图

下图展示了以上代码的工作过程概要,但实际上Fork/Join框架的内部工作过程要比这张图复杂得多,例如如何决定某一个recursive task是使用哪条线程进行运行;再例如如何决定当一个任务/子任务提交到Fork/Join框架内部后,是创建一个新的线程去运行还是让它进行队列等待。

所以如果不深入理解Fork/Join框架的运行原理,只是根据之上最简单的使用例子观察运行效果,那么我们只能知道子任务在Fork/Join框架中被拆分得足够小后,并且其内部使用多线程并行完成这些小任务的计算后再进行结果向上的合并动作,最终形成顶层结果。不急,一步一步来,我们先从这张概要的过程图开始讨论。
,只是根据之上最简单的使用例子观察运行效果,那么我们只能知道子任务在Fork/Join框架中被拆分得足够小后,并且其内部使用多线程并行完成这些小任务的计算后再进行结果向上的合并动作,最终形成顶层结果。不急,一步一步来,我们先从这张概要的过程图开始讨论。

image.png
图中最顶层的任务使用submit方式被提交到Fork/Join框架中,后者将前者放入到某个线程中运行,工作任务中的compute方法的代码开始对这个任务T1进行分析。如果当前任务需要累加的数字范围过大(代码中设定的是大于200),则将这个计算任务拆分成两个子任务(T1.1和T1.2),每个子任务各自负责计算一半的数据累加,请参见代码中的fork方法。如果当前子任务中需要累加的数字范围足够小(小于等于200),就进行累加然后返回到上层任务中。

1、ForkJoinPool构造函数

ForkJoinPool有四个构造函数,其中参数最全的那个构造函数如下所示:

1
2
3
4
java复制代码public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode)
  • parallelism:可并行级别,Fork/Join框架将依据这个并行级别的设定,决定框架内并行执行的线程数量。并行的每一个任务都会有一个线程进行处理,但是千万不要将这个属性理解成Fork/Join框架中最多存在的线程数量,也不要将这个属性和ThreadPoolExecutor线程池中的corePoolSize、maximumPoolSize属性进行比较,因为ForkJoinPool的组织结构和工作方式与后者完全不一样。而后续的讨论中,读者还可以发现Fork/Join框架中可存在的线程数量和这个参数值的关系并不是绝对的关联(有依据但并不全由它决定)。
  • factory:当Fork/Join框架创建一个新的线程时,同样会用到线程创建工厂。只不过这个线程工厂不再需要实现ThreadFactory接口,而是需要实现ForkJoinWorkerThreadFactory接口。后者是一个函数式接口,只需要实现一个名叫newThread的方法。在Fork/Join框架中有一个默认的ForkJoinWorkerThreadFactory接口实现:DefaultForkJoinWorkerThreadFactory。
  • handler:异常捕获处理器。当执行的任务中出现异常,并从任务中被抛出时,就会被handler捕获。
  • asyncMode:这个参数也非常重要,从字面意思来看是指的异步模式,它并不是说Fork/Join框架是采用同步模式还是采用异步模式工作。Fork/Join框架中为每一个独立工作的线程准备了对应的待执行任务队列,这个任务队列是使用数组进行组合的双向队列。即是说存在于队列中的待执行任务,即可以使用先进先出的工作模式,也可以使用后进先出的工作模式。

image.png

  • 当asyncMode设置为ture的时候,队列采用先进先出方式工作;反之则是采用后进先出的方式工作,该值默认为false
1
2
3
erlang复制代码......
asyncMode ? FIFO_QUEUE : LIFO_QUEUE,
......

ForkJoinPool还有另外两个构造函数,一个构造函数只带有parallelism参数,既是可以设定Fork/Join框架的最大并行任务数量;另一个构造函数则不带有任何参数,对于最大并行任务数量也只是一个默认值——当前操作系统可以使用的CPU内核数量(Runtime.getRuntime().availableProcessors())。实际上ForkJoinPool还有一个私有的、原生构造函数,之上提到的三个构造函数都是对这个私有的、原生构造函数的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码......
private ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
int mode,
String workerNamePrefix) {
this.workerNamePrefix = workerNamePrefix;
this.factory = factory;
this.ueh = handler;
this.config = (parallelism & SMASK) | mode;
long np = (long)(-parallelism); // offset ctl counts
this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
}
......

如果你对Fork/Join框架没有特定的执行要求,可以直接使用不带有任何参数的构造函数。也就是说推荐基于当前操作系统可以使用的CPU内核数作为Fork/Join框架内最大并行任务数量,这样可以保证CPU在处理并行任务时,尽量少发生任务线程间的运行状态切换(实际上单个CPU内核上的线程间状态切换基本上无法避免,因为操作系统同时运行多个线程和多个进程)。

2、fork方法和join方法

Fork/Join框架中提供的fork方法和join方法,可以说是该框架中提供的最重要的两个方法,它们和parallelism“可并行任务数量”配合工作,可以导致拆分的子任务T1.1、T1.2甚至TX在Fork/Join框架中不同的运行效果。例如TX子任务或等待其它已存在的线程运行关联的子任务,或在运行TX的线程中“递归”执行其它任务,又或者启动一个新的线程运行子任务……

fork方法用于将新创建的子任务放入当前线程的work queue队列中,Fork/Join框架将根据当前正在并发执行ForkJoinTask任务的ForkJoinWorkerThread线程状态,决定是让这个任务在队列中等待,还是创建一个新的ForkJoinWorkerThread线程运行它,又或者是唤起其它正在等待任务的ForkJoinWorkerThread线程运行它。

这里面有几个元素概念需要注意,ForkJoinTask任务是一种能在Fork/Join框架中运行的特定任务,也只有这种类型的任务可以在Fork/Join框架中被拆分运行和合并运行。ForkJoinWorkerThread线程是一种在Fork/Join框架中运行的特性线程,它除了具有普通线程的特性外,最主要的特点是每一个ForkJoinWorkerThread线程都具有一个独立的任务等待队列(work queue) ,这个任务队列用于存储在本线程中被拆分的若干子任务。


join方法用于让当前线程阻塞,直到对应的子任务完成运行并返回执行结果。或者,如果这个子任务存在于当前线程的任务等待队列(work queue)中,则取出这个子任务进行“递归”执行。其目的是尽快得到当前子任务的运行结果,然后继续执行。

五、使用Fork/Join解决实际问题

之前所举的的例子是使用Fork/Join框架完成1-1000的整数累加。这个示例如果只是演示Fork/Join框架的使用,那还行,但这种例子和实际工作中所面对的问题还有一定差距。本篇文章我们使用Fork/Join框架解决一个实际问题,就是高效排序的问题。

1.使用归并算法解决排序问题

排序问题是我们工作中的常见问题。目前也有很多现成算法是为了解决这个问题而被发明的,例如多种插值排序算法、多种交换排序算法。而并归排序算法是目前所有排序算法中,平均时间复杂度较好(O(nlgn)),算法稳定性较好的一种排序算法。它的核心算法思路将大的问题分解成多个小问题,并将结果进行合并。

image.png
整个算法的拆分阶段,是将未排序的数字集合,从一个较大集合递归拆分成若干较小的集合,这些较小的集合要么包含最多两个元素,要么就认为不够小需要继续进行拆分。

那么对于一个集合中元素的排序问题就变成了两个问题:1、较小集合中最多两个元素的大小排序;2、如何将两个有序集合合并成一个新的有序集合。第一个问题很好解决,那么第二个问题是否会很复杂呢?实际上第二个问题也很简单,只需要将两个集合同时进行一次遍历即可完成——比较当前集合中最小的元素,将最小元素放入新的集合,它的时间复杂度为O(n):

image.png
以下是归并排序算法的简单实现:

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
ini复制代码package test.thread.pool.merge;

import java.util.Arrays;
import java.util.Random;

/**
* 归并排序
* @author yinwenjie
*/
public class Merge1 {

private static int MAX = 10000;

private static int inits[] = new int[MAX];

// 这是为了生成一个数量为MAX的随机整数集合,准备计算数据
// 和算法本身并没有什么关系
static {
Random r = new Random();
for(int index = 1 ; index <= MAX ; index++) {
inits[index - 1] = r.nextInt(10000000);
}
}

public static void main(String[] args) {
long beginTime = System.currentTimeMillis();
int results[] = forkits(inits);
long endTime = System.currentTimeMillis();
// 如果参与排序的数据非常庞大,记得把这种打印方式去掉
System.out.println("耗时=" + (endTime - beginTime) + " | " + Arrays.toString(results));
}

// 拆分成较小的元素或者进行足够小的元素集合的排序
private static int[] forkits(int source[]) {
int sourceLen = source.length;
if(sourceLen > 2) {
int midIndex = sourceLen / 2;
int result1[] = forkits(Arrays.copyOf(source, midIndex));
int result2[] = forkits(Arrays.copyOfRange(source, midIndex , sourceLen));
// 将两个有序的数组,合并成一个有序的数组
int mer[] = joinInts(result1 , result2);
return mer;
}
// 否则说明集合中只有一个或者两个元素,可以进行这两个元素的比较排序了
else {
// 如果条件成立,说明数组中只有一个元素,或者是数组中的元素都已经排列好位置了
if(sourceLen == 1
|| source[0] <= source[1]) {
return source;
} else {
int targetp[] = new int[sourceLen];
targetp[0] = source[1];
targetp[1] = source[0];
return targetp;
}
}
}

/**
* 这个方法用于合并两个有序集合
* @param array1
* @param array2
*/
private static int[] joinInts(int array1[] , int array2[]) {
int destInts[] = new int[array1.length + array2.length];
int array1Len = array1.length;
int array2Len = array2.length;
int destLen = destInts.length;

// 只需要以新的集合destInts的长度为标准,遍历一次即可
for(int index = 0 , array1Index = 0 , array2Index = 0 ; index < destLen ; index++) {
int value1 = array1Index >= array1Len?Integer.MAX_VALUE:array1[array1Index];
int value2 = array2Index >= array2Len?Integer.MAX_VALUE:array2[array2Index];
// 如果条件成立,说明应该取数组array1中的值
if(value1 < value2) {
array1Index++;
destInts[index] = value1;
}
// 否则取数组array2中的值
else {
array2Index++;
destInts[index] = value2;
}
}

return destInts;
}
}

以上归并算法对1万条随机数进行排序只需要2-3毫秒,对10万条随机数进行排序只需要20毫秒左右的时间,对100万条随机数进行排序的平均时间大约为160毫秒(这还要看随机生成的待排序数组是否本身的凌乱程度)。可见归并算法本身是具有良好的性能的。使用JMX工具和操作系统自带的CPU监控器监视应用程序的执行情况,可以发现整个算法是单线程运行的,且同一时间CPU只有单个内核在作为主要的处理内核工作:
JMX中观察到的线程情况:

image.png
CPU的运作情况:

image.png

2.使用Fork/Join运行归并算法

但是随着待排序集合中数据规模继续增大,以上归并算法的代码实现就有一些力不从心了,例如以上算法对1亿条随机数集合进行排序时,耗时为27秒左右。

接着我们可以使用Fork/Join框架来优化归并算法的执行性能,将拆分后的子任务实例化成多个ForkJoinTask任务放入待执行队列,并由Fork/Join框架在多个ForkJoinWorkerThread线程间调度这些任务。如下图所示:

image.png
以下为使用Fork/Join框架后的归并算法代码,请注意joinInts方法中对两个有序集合合并成一个新的有序集合的代码,是没有变化的可以参见本文上一小节中的内容。所以在代码中就不再赘述了:

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
ini复制代码......
/**
* 使用Fork/Join框架的归并排序算法
* @author yinwenjie
*/
public class Merge2 {

private static int MAX = 100000000;

private static int inits[] = new int[MAX];

// 同样进行随机队列初始化,这里就不再赘述了
static {
......
}

public static void main(String[] args) throws Exception {
// 正式开始
long beginTime = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
MyTask task = new MyTask(inits);
ForkJoinTask<int[]> taskResult = pool.submit(task);
try {
taskResult.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace(System.out);
}
long endTime = System.currentTimeMillis();
System.out.println("耗时=" + (endTime - beginTime));
}

/**
* 单个排序的子任务
* @author yinwenjie
*/
static class MyTask extends RecursiveTask<int[]> {

private int source[];

public MyTask(int source[]) {
this.source = source;
}

/* (non-Javadoc)
* @see java.util.concurrent.RecursiveTask#compute()
*/
@Override
protected int[] compute() {
int sourceLen = source.length;
// 如果条件成立,说明任务中要进行排序的集合还不够小
if(sourceLen > 2) {
int midIndex = sourceLen / 2;
// 拆分成两个子任务
MyTask task1 = new MyTask(Arrays.copyOf(source, midIndex));
task1.fork();
MyTask task2 = new MyTask(Arrays.copyOfRange(source, midIndex , sourceLen));
task2.fork();
// 将两个有序的数组,合并成一个有序的数组
int result1[] = task1.join();
int result2[] = task2.join();
int mer[] = joinInts(result1 , result2);
return mer;
}
// 否则说明集合中只有一个或者两个元素,可以进行这两个元素的比较排序了
else {
// 如果条件成立,说明数组中只有一个元素,或者是数组中的元素都已经排列好位置了
if(sourceLen == 1
|| source[0] <= source[1]) {
return source;
} else {
int targetp[] = new int[sourceLen];
targetp[0] = source[1];
targetp[1] = source[0];
return targetp;
}
}
}

private int[] joinInts(int array1[] , int array2[]) {
// 和上文中出现的代码一致
}
}
}

使用Fork/Join框架优化后,同样执行1亿条随机数的排序处理时间大约在14秒左右,当然这还和待排序集合本身的凌乱程度、CPU性能等有关系。但总体上这样的方式比不使用Fork/Join框架的归并排序算法在性能上有30%左右的性能提升。以下为执行时观察到的CPU状态和线程状态:

JMX中的内存、线程状态:

image.png
CPU使用情况:

image.png
除了归并算法代码实现内部可优化的细节处,使用Fork/Join框架后,我们基本上在保证操作系统线程规模的情况下,将每一个CPU内核的运算资源同时发挥了出来。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

用10个真实案列带你掌握MySQL调优 前言

发表于 2021-07-20

前言

在应用开发的早期,数据量少,开发人员开发功能时更重视功能上的实现,随着生产数据的增长,很多SQL语句开始暴露出性能问题,对生产的影响也越来越大,有时可能这些有问题的SQL就是整个系统性能的瓶颈。

点击领取腾讯架构师人手一本的MySQL归纳笔记完整版

SQL优化一般步骤

1、通过慢查日志等定位那些执行效率较低的SQL语句

2、explain 分析SQL的执行计划

需要重点关注type、rows、filtered、extra。

type由上至下,效率越来越高

  • ALL 全表扫描
  • index 索引全扫描
  • range 索引范围扫描,常用语<,<=,>=,between,in等操作
  • ref 使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中
  • eq_ref 类似ref,区别在于使用的是唯一索引,使用主键的关联查询
  • const/system 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询
  • null MySQL不访问任何表或索引,直接返回结果

虽然上至下,效率越来越高,但是根据cost模型,假设有两个索引idx1(a, b, c),idx2(a, c),SQL为select * from t where a = 1 and b in (1, 2) order by c;如果走idx1,那么是type为range,如果走idx2,那么type是ref;当需要扫描的行数,使用idx2大约是idx1的5倍以上时,会用idx1,否则会用idx2

Extra

  • Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行。
  • Using temporary:使用了临时表保存中间结果,性能特别差,需要重点优化
  • Using index:表示相应的 select 操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!如果同时出现 using where,意味着无法直接通过索引查找来查询到符合条件的数据。
  • Using index condition:MySQL5.6之后新增的ICP,using index condtion就是使用了ICP(索引下推),在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。

3、show profile 分析

了解SQL执行的线程的状态及消耗的时间。
默认是关闭的,开启语句“set profiling = 1;”

1
2
ini复制代码SHOW PROFILES ;
SHOW PROFILE FOR QUERY #{id};

4、trace

trace分析优化器如何选择执行计划,通过trace文件能够进一步了解为什么优惠券选择A执行计划而不选择B执行计划。

1
2
3
ini复制代码set optimizer_trace="enabled=on";
set optimizer_trace_max_mem_size=1000000;
select * from information_schema.optimizer_trace;

5、确定问题并采用相应的措施

  • 优化索引
  • 优化SQL语句:修改SQL、IN 查询分段、时间查询分段、基于上一次数据过滤
  • 改用其他实现方式:ES、数仓等
  • 数据碎片处理

场景分析

案例1、最左匹配

索引

1
go复制代码KEY `idx_shopid_orderno` (`shop_id`,`order_no`)

SQL语句

1
csharp复制代码select * from _t where orderno=''

查询匹配从左往右匹配,要使用order_no走索引,必须查询条件携带shop_id或者索引(shop_id,order_no)调换前后顺序

案例2、隐式转换

索引

1
go复制代码KEY `idx_mobile` (`mobile`)

SQL语句

1
csharp复制代码select * from _user where mobile=12345678901

隐式转换相当于在索引上做运算,会让索引失效。mobile是字符类型,使用了数字,应该使用字符串匹配,否则MySQL会用到隐式替换,导致索引失效。

案例3、大分页

索引

1
go复制代码KEY `idx_a_b_c` (`a`, `b`, `c`)

SQL语句

1
sql复制代码select * from _t where a = 1 and b = 2 order by c desc limit 10000, 10;

对于大分页的场景,可以优先让产品优化需求,如果没有优化的,有如下两种优化方式,
一种是把上一次的最后一条数据,也即上面的c传过来,然后做“c < xxx”处理,但是这种一般需要改接口协议,并不一定可行。
另一种是采用延迟关联的方式进行处理,减少SQL回表,但是要记得索引需要完全覆盖才有效果,SQL改动如下

1
sql复制代码select t1.* from _t t1, (select id from _t where a = 1 and b = 2 order by c desc limit 10000, 10) t2 where t1.id = t2.id;

案例4、in + order by

索引

1
go复制代码KEY `idx_shopid_status_created` (`shop_id`, `order_status`, `created_at`)

SQL语句

1
sql复制代码select * from _order where shop_id = 1 and order_status in (1, 2, 3) order by created_at desc limit 10

in查询在MySQL底层是通过n*m的方式去搜索,类似union,但是效率比union高。
in查询在进行cost代价计算时(代价 = 元组数 * IO平均值),是通过将in包含的数值,一条条去查询获取元组数的,

因此这个计算过程会比较的慢,所以MySQL设置了个临界值(eq_range_index_dive_limit),5.6之后超过这个临界值后该列的cost就不参与计算了。因此会导致执行计划选择不准确。默认是200,即in条件超过了200个数据,会导致in的代价计算存在问题,可能会导致Mysql选择的索引不准确。

处理方式,可以(order_status, created_at)互换前后顺序,并且调整SQL为延迟关联。

案例5、范围查询阻断,后续字段不能走索引

索引

1
go复制代码KEY `idx_shopid_created_status` (`shop_id`, `created_at`, `order_status`)

SQL语句

1
csharp复制代码select * from _order where shop_id = 1 and created_at > '2021-01-01 00:00:00' and order_status = 10

范围查询还有“IN、between”

案例6、不等于、不包含不能用到索引的快速搜索。(可以用到ICP)

1
2
csharp复制代码select * from _order where shop_id=1 and order_status not in (1,2)
select * from _order where shop_id=1 and order_status != 1

在索引上,避免使用NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等

案例7、优化器选择不使用索引的情况

如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的蛮大一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据。

1
csharp复制代码select * from _order where  order_status = 1

查询出所有未支付的订单,一般这种订单是很少的,即使建了索引,也没法使用索引。

案例8、复杂查询

1
2
csharp复制代码select sum(amt) from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01';
select * from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01' limit 10;

如果是统计某些数据,可能改用数仓进行解决;
如果是业务上就有那么复杂的查询,可能就不建议继续走SQL了,而是采用其他的方式进行解决,比如使用ES等进行解决。

案例9、asc和desc混用

1
2
sql复制代码select * from _t where a=1 order by b desc, c asc
desc 和asc混用时会导致索引失效

案例10、大数据

对于推送业务的数据存储,可能数据量会很大,如果在方案的选择上,最终选择存储在MySQL上,并且做7天等有效期的保存。
那么需要注意,频繁的清理数据,会照成数据碎片,需要联系DBA进行数据碎片处理。


文章就写到这了,如果还不明白的话可以看看《高性能MySQL》这本书,电子档可以点击传送门领取。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中如何优雅的使用线程池?

发表于 2021-07-20

为什么要用线程池?

线程是不是越多越好?

  1. 线程在java中是一个对象,更是操作系统的资源,线程创建、销毁需要时间。如果创建时间+小会时间>执行任务时间就很不合算。
  2. java对象占用堆内存,操作系统线程占用系统内存,根据jvm规范,一个线程默认最大栈大小1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多的内存。
  3. 操作系统需要频繁切换线程上下文(每个线都想被运行),影响性能。

线程池的推出,就是为了方便边的控制线程数量。

线程池

线程池基本概念

线程池包括以下四个基本组成部分:

  1. 线程池管理器:用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务;
  2. 工作线程:线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;
  3. 任务接口:每个任务必须实现的借口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
  4. 任务队列:用于存放没有处理的任务。提供一种缓冲机制。

线程池接口定义和实现类

可以认为ScheduledThreadPoolExector是最丰富的实现类。

ExecutorService

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
java复制代码public interface ExecutorService extends Executor {
/**
* 优雅关闭线程池,之前提交的任务将被执行,但是不会接受新的任务。
*/
void shutdown();

/**
* 尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行任务的列表。
*/
List<Runnable> shutdownNow();

/**
* 如果此线程池已关闭,则返回true.
*/
boolean isShutdown();

/**
* 如果关闭后的所有任务都已完成,则返回true
*/
boolean isTerminated();

/**
* 监测ExecutorService是否已经关闭,直到所有任务完成执行,或超时发生,或当前线程被中断。
*/
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

/**
* 提交一个用于执行的Callable返回任务,并返回一个Future,用于获取Callable执行结果。
*/
<T> Future<T> submit(Callable<T> task);

/**
* 提交可运行任务以执行,并返回Future,执行结果为传入的result
*/
<T> Future<T> submit(Runnable task, T result);

/**
* 提交可运行任务以执行,并返回Future对象,执行结果为null
*/
Future<?> submit(Runnable task);

/**
* 执行给定的任务集合,执行完毕后,则返回结果。
*/
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

/**
* 执行给定的任务集合,执行完毕或者超时后,则返回结果,其他任务终止。
*/
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;

/**
* 执行给定的任务,任意一个执行成功则返回结果,其他任务终止。
*/
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;

/**
* 执行给定的任务,任意一个执行成功或者超时后,则返回结果,其他任务终止
*/
<T> T invokeAny(Collection<? extends Callable<T>> tasks,long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException;
}

ScheduledExecutorService

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
java复制代码public interface ScheduledExecutorService extends ExecutorService {

/**
* 创建并执行一个一次性任务,过了延迟时间就会被执行
*/
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);

/**
* 创建并执行一个一次性任务,过了延迟时间就会被执行
*/
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);

/**
* 创建并执行一个周期性任务,过了给定的初始化延迟时间,会第一次被执行。执行过程中发生了异常,那么任务停止
* 一次任务执行时长超过了周期时间,下一次任务会等到该次任务执行结束后,立刻执行,这也是它和scheduleWithTixedDelay的重要区别
*/
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);

/**
* 创建并执行一个周期性任务,过了给定的初始化延迟时间,会第一次被执行。执行过程中发生了异常,那么任务停止
* 一次任务执行时长超过了周期时间,下一次任务会在该次任务执行结束的时间基础上,计算执行延时。
* 对于超时周期的长时间处理任务的不同处理方式,这是它和scheduleAtFixedRate的重要区别
*/
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);

}

线程池工具类

在使用过程中,可以自己实例化线程池,也可以用Executors创建线程池的工厂累,常用方法如下:

newFixedThreadPool(int nThreads)

创建一个固定大小、任务队列容量误解的线程池。核心线程数=最大线程数。

newCachedThreadPool()

创建的是一个大小无界的缓冲线程池。它的任务队列是一个同步队列。任务加入到池中,如果池中有空闲线程,则用空闲线程执行,如无则创建新线程执行。池中的线程空闲时间超过60秒,将被销毁释放。线程数随任务的多少变化。适用于执行耗时较小的异步任务。池的核心线程数=0,最大线程=Integer.MAX_VALUE

newSingleThreadExecutor()

只有一个线程来执行无界任务队列的单一线程池。该线程池确保任务加入的顺序一个一个一次执行。当唯一的线程因任务异常中止时,将创建一个新的线程来继续执行后续的任务。与newFixedThreadPool(1)的区别在于,单一线程池的池大小在newSingleThreadExecutor方法中硬编码,不能再改变的。

newScheduledThreadPool(int corePoolSize)

能定时执行任务的线程池。该池的核心线程数由参数指定,最大线程数=Integer.MAX_VALUE

任务线程池执行过程

如何确认合适的线程数量?

  • 如果是CPU密集型应用,则线程池大小设置为N+1 (N为CPU总核数)
  • 如果是IO密集型应用,则线程池大小设置为2N+1 (N为CPU总核数)
  • 线程等待时间(IO)所占比例越高,需要越多线程。
  • 线程CPU时间所占比例越高,需要越少线程。

一个系统最快的部分是CPU,所以决定一个系统吞吐量上限的是CPU。增强CPU处理能力,可以提高系统吞吐量上限。但根据短板效应,真实的系统吞吐量并不能单纯根据CPU来计算。那要提高系统吞吐量,就需要从“系统短板”(比如网络延迟、IO)着手:

  1. 尽量提高短板操作的并行化比率,比如多线程下载技术;
  2. 增强短板能力,比如用NIO替代IO;

线程池的使用分析

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
java复制代码public class ExecutorsUse {
/**
* 测试: 提交15 个执行时间需要3秒的任务,看线程池的状况
*
* @param threadPoolExecutor 传入不同的线程池,看不同的结果
* @throws Exception
*/
public void testCommon(ThreadPoolExecutor threadPoolExecutor) throws Exception {
// 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
for (int i = 0; i < 15; i++) {
int n = i;
threadPoolExecutor.submit(() -> {
try {
System.out.println("开始执行:" + n);
Thread.sleep(3000L);
System.err.println("执行结束:" + n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
);
System.out.println("任务提交成功 :" + i);
}
// 查看线程数量,查看队列等待数量
Thread.sleep(500L);
System.out.println("当前线程池线程数量为:" + threadPoolExecutor.getPoolSize());
System.out.println("当前线程池等待的数量为:" + threadPoolExecutor.getQueue().size());
// 等待15秒,查看线程数量和队列数量(理论上,会被超出核心线程数量的线程自动销毁)
Thread.sleep(15000L);
System.out.println("当前线程池线程数量为:" + threadPoolExecutor.getPoolSize());
System.out.println("当前线程池等待的数量为:" + threadPoolExecutor.getQueue().size());
}

/**
* 1、线程池信息: 核心线程数量5,最大数量10,无界队列,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略
*
* @throws Exception
*/
private void threadPoolExecutorTest1() throws Exception {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>());
testCommon(threadPoolExecutor);
// 预计结果:线程池线程数量为:5,超出数量的任务,其他的进入队列中等待被执行
}

/**
* 2、 线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
*
* @throws Exception
*/
private void threadPoolExecutorTest2() throws Exception {
// 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
// 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("有任务被拒绝执行了");
}
});
testCommon(threadPoolExecutor);
// 预计结果:
// 1、 5个任务直接分配线程开始执行
// 2、 3个任务进入等待队列
// 3、 队列不够用,临时加开5个线程来执行任务(5秒没活干就销毁)
// 4、 队列和线程池都满了,剩下2个任务,没资源了,被拒绝执行。
// 5、 任务执行,5秒后,如果无任务可执行,销毁临时创建的5个线程
}

/**
* 3、 线程池信息: 核心线程数量5,最大数量5,无界队列,超出核心线程数量的线程存活时间:5秒
*
* @throws Exception
*/
private void threadPoolExecutorTest3() throws Exception {
// 和Executors.newFixedThreadPool(int nThreads)一样的
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
testCommon(threadPoolExecutor);
// 预计结:线程池线程数量为:5,超出数量的任务,其他的进入队列中等待被执行
}

/**
* 4、 线程池信息:
* 核心线程数量0,最大数量Integer.MAX_VALUE,SynchronousQueue队列,超出核心线程数量的线程存活时间:60秒
*
* @throws Exception
*/
private void threadPoolExecutorTest4() throws Exception {

// SynchronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列。
// 在使用SynchronousQueue作为工作队列的前提下,客户端代码向线程池提交任务时,
// 而线程池中又没有空闲的线程能够从SynchronousQueue队列实例中取一个任务,
// 那么相应的offer方法调用就会失败(即任务没有被存入工作队列)。
// 此时,ThreadPoolExecutor会新建一个新的工作者线程用于对这个入队列失败的任务进行处理(假设此时线程池的大小还未达到其最大线程池大小maximumPoolSize)。

// 和Executors.newCachedThreadPool()一样的
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
testCommon(threadPoolExecutor);
// 预计结果:
// 1、 线程池线程数量为:15,超出数量的任务,其他的进入队列中等待被执行
// 2、 所有任务执行结束,60秒后,如果无任务可执行,所有线程全部被销毁,池的大小恢复为0
Thread.sleep(60000L);
System.out.println("60秒后,再看线程池中的数量:" + threadPoolExecutor.getPoolSize());
}

/**
* 5、 定时执行线程池信息:3秒后执行,一次性任务,到点就执行 <br/>
* 核心线程数量5,最大数量Integer.MAX_VALUE,DelayedWorkQueue延时队列,超出核心线程数量的线程存活时间:0秒
*
* @throws Exception
*/
private void threadPoolExecutorTest5() throws Exception {
// 和Executors.newScheduledThreadPool()一样的
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
threadPoolExecutor.schedule(new Runnable() {
@Override
public void run() {
System.out.println("任务被执行,现在时间:" + System.currentTimeMillis());
}
}, 3000, TimeUnit.MILLISECONDS);
System.out.println(
"定时任务,提交成功,时间是:" + System.currentTimeMillis() + ", 当前线程池中线程数量:" + threadPoolExecutor.getPoolSize());
// 预计结果:任务在3秒后被执行一次
}

/**
* 6、 定时执行线程池信息:线程固定数量5 ,<br/>
* 核心线程数量5,最大数量Integer.MAX_VALUE,DelayedWorkQueue延时队列,超出核心线程数量的线程存活时间:0秒
*
* @throws Exception
*/
private void threadPoolExecutorTest6() throws Exception {
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
// 周期性执行某一个任务,线程池提供了两种调度方式,这里单独演示一下。测试场景一样。
// 测试场景:提交的任务需要3秒才能执行完毕。看两种不同调度方式的区别
// 效果1: 提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,完毕后立刻执行)。
// 也就是说这个代码中是,3秒钟执行一次(计算方式:每次执行三秒,间隔时间1秒,执行结束后马上开始下一次执行,无需等待)
threadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务-1 被执行,现在时间:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);

// 效果2:提交后,2秒后开始第一次执行,之后每间隔1秒,固定执行一次(如果发现上次执行还未完毕,则等待完毕,等上一次执行完毕后再开始计时,等待1秒)。
// 也就是说这个代码钟的效果看到的是:4秒执行一次。 (计算方式:每次执行3秒,间隔时间1秒,执行完以后再等待1秒,所以是 3+1)
threadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(3000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务-2 被执行,现在时间:" + System.currentTimeMillis());
}
}, 2000, 1000, TimeUnit.MILLISECONDS);
}

/**
* 7、 终止线程:线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
*
* @throws Exception
*/
private void threadPoolExecutorTest7() throws Exception {
// 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
// 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("有任务被拒绝执行了");
}
});
// 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
for (int i = 0; i < 15; i++) {
int n = i;
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("开始执行:" + n);
Thread.sleep(3000L);
System.err.println("执行结束:" + n);
} catch (InterruptedException e) {
System.out.println("异常:" + e.getMessage());
}
}
});
System.out.println("任务提交成功 :" + i);
}
// 1秒后终止线程池
Thread.sleep(1000L);
threadPoolExecutor.shutdown();
// 再次提交提示失败
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println("追加一个任务");
}
});
// 结果分析
// 1、 10个任务被执行,3个任务进入队列等待,2个任务被拒绝执行
// 2、调用shutdown后,不接收新的任务,等待13任务执行结束
// 3、 追加的任务在线程池关闭后,无法再提交,会被拒绝执行
}

/**
* 8、 立刻终止线程:线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
*
* @throws Exception
*/
private void threadPoolExecutorTest8() throws Exception {
// 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
// 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.err.println("有任务被拒绝执行了");
}
});
// 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
for (int i = 0; i < 15; i++) {
int n = i;
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
try {
System.out.println("开始执行:" + n);
Thread.sleep(3000L);
System.err.println("执行结束:" + n);
} catch (InterruptedException e) {
System.out.println("异常:" + e.getMessage());
}
}
});
System.out.println("任务提交成功 :" + i);
}
// 1秒后终止线程池
Thread.sleep(1000L);
List<Runnable> shutdownNow = threadPoolExecutor.shutdownNow();
// 再次提交提示失败
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
System.out.println("追加一个任务");
}
});
System.out.println("未结束的任务有:" + shutdownNow.size());

// 结果分析
// 1、 10个任务被执行,3个任务进入队列等待,2个任务被拒绝执行
// 2、调用shutdownnow后,队列中的3个线程不再执行,10个线程被终止
// 3、 追加的任务在线程池关闭后,无法再提交,会被拒绝执行
}

public static void main(String[] args) throws Exception {
// new ExecutorsUse().threadPoolExecutorTest1();
// new ExecutorsUse().threadPoolExecutorTest2();
// new ExecutorsUse().threadPoolExecutorTest3();
new ExecutorsUse().threadPoolExecutorTest4();
// new ExecutorsUse().threadPoolExecutorTest5();
// new ExecutorsUse().threadPoolExecutorTest6();
// new ExecutorsUse().threadPoolExecutorTest7();
// new ExecutorsUse().threadPoolExecutorTest8();
}
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

北京大公司:你是熟悉Map集合吗?

发表于 2021-07-20

文章以纯面试的角度去讲解,所以有很多的细节是未铺垫的。

鉴于很多同学反馈没看懂【对线面试官】系列,基础相关的知识我确实写过文章讲解过啦,但有的同学就是不爱去翻。

为了让大家有更好的体验,我把基础文章也找出来(重要的知识点我还整理过电子书,比如说像多线程、集合、Spring这种面试必考的早就已经转成PDF格式啦)

我把这些上传到网盘,你们有需要直接下载就好了。做到这份上了,不会还想白嫖吧?点赞和转发又不用钱。

链接:pan.baidu.com/s/1pQTuKBYs… 密码:3wom

欢迎关注我的微信公众号【Java3y】来聊聊Java面试


【对线面试官】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

值得收藏,揭秘 MySQL 多版本并发控制实现原理

发表于 2021-07-20

MySQL 中多版本并发控制(MVCC),是现代数据库引擎实现中常用的处理读写冲突的手段,MVCC 作为 MySQL 高级应用特性,目的在于提高数据库高并发场景下的吞吐性能。

一、MVCC出现背景是什么?

事务的4个隔离级别以及对应的3种异常:

  • 脏读:一个事务读取到了另外一个事务没有提交的数据;
  • 不可重复读:在同一事务中,两次读取同一数据,得到内容不同;
  • 幻读:同一事务中,用同样的操作读取两次,得到的记录数不相同。

在 MySQL 中, 默认的隔离级别是可重复读,可以解决脏读和不可重复读的问题,但不能解决幻读问题。如果我们想要解决幻读问题,就需要采用串行化的方式,也就是将隔离级别提升到最高,但这样一来就会大幅降低数据库的事务并发能力。

而MVCC就是通过乐观锁的方式来解决不可重复读和幻读问题,它可以在大多数情况下替代行级锁,降低系统的开销。

MySQL 并发事务会引起更新丢失问题,解决办法是锁,主要分两类:

  • 乐观锁:

其实现如同它的名字一样,是假设比较好的情况。

每次取数据的时候都认为他人不会对其修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。

  • 悲观锁:

悲观锁也如同它的名字一样,总是假设比较坏的情况,每次取数据的时候都认为他人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。

二、什么是MVCC,它解决了什么问题?

MVCC 是通过数据行的多个版本管理来实现数据库的并发控制,简单来说它的思想就是保存数据的历史版本。

我们可以通过比较版本号决定数据是否显示出来(具体的规则后面会介绍到),读取数据的时候不需要加锁也可以保证事务的隔离效果。

通过 MVCC 我们可以解决以下几个问题:

(1)读写之间阻塞的问题,通过 MVCC 可以让读写互相不阻塞,即读不阻塞写,写不阻塞读,这样就可以提升事务并发处理能力。

(2)降低了死锁的概率。这是因为 MVCC 采用了乐观锁的方式,读取数据时并不需要加锁,对于写操作,也只锁定必要的行。

(3)解决一致性读的问题。一致性读也被称为快照读,当我们查询数据库在某个时间点的快照时,只能看到这个时间点之前事务提交更新的结果,而不能看到这个时间点之后事务提交的更新结果。

解释一下可能难以理解的几个词汇:

  • 快照读:

读取的是快照数据,不加锁的简单的SELECT都属于快照读(只是普通的读操作)。

  • 当前读:

当前读就是读取最新数据,而不是历史版本的数据。

加锁的SELECT,或者对数据进行增删改都会进行当前读(包括加锁的读取和DML操作)。

三、应用举例分析

为了更好地让大家理解MVCC,我们用一个示例场景来说明。

假设有个账户金额表 user_balance,包括三个字段,分别是 username 用户名、balance 余额和 bankcard 卡号,表数据如下所示:

用户 A 和用户 B 之间进行转账,此时数据库管理员想要查询 user_balance 表中的总金额,两个场景存在并发情况,在没有MVCC的情况下,会出现哪些问题呢。

Case1:因为需要采用加行锁的方式,用户 A 给 B 转账时间等待很久,如下图所示。

Case2:当我们读取的时候用了加行锁,可能会出现死锁的情况,如下图所示。

比如当我们读到 A 有 1000 元的时候,此时 B 开始执行给 A 转账。

四、InnoDB如何实现MVCC?

当查询一条记录的时候,执行流程如下:

  1. 首先获取事务自己的版本号,也就是事务 ID;
  2. 获取 Read View;
  3. 查询得到的数据,然后与 Read View 中的事务版本号进行比较;
  4. 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
  5. 最后返回符合规则的数据。

相关概念

1. 事务版本号

一个自增长的事务ID,用于标记事务执行的先后顺序。

2. Read View

在 MVCC 机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在 Undo Log 里。如果一个事务想要查询这个行记录,需要读取哪个版本的行记录呢?

这时就需要用到 Read View 了,它帮我们解决了行的可见性问题。Read View 保存了当前事务开启时所有活跃(还没有提交)的事务列表,换个角度,可以理解为 Read View 保存了不应该让这个事务看到的其他的事务 ID 列表。

Read VIew 中的几个重要属性:

  • up_limit_id,活跃的事务中最小的事务 ID;
  • trx_ids,系统当前正在活跃的事务 ID 集合;
  • low_limit_id,活跃的事务中最大的事务 ID;
  • creator_trx_id,创建这个 Read View 的事务 ID。

3. 行记录的隐藏列

InnoDB 的叶子节点段存储了数据页,数据页中保存了行记录,在这些行记录中有一些重要的隐藏字段:

  • DB_ROW_ID :

6-byte,记录操作该数据事务的事务ID;

  • DB_TRX_ID :

6-byte,当创建表没有合适的索引作为聚集索引时,会用该隐藏ID创建聚集索引;

  • DB_ROLL_PTR :

7-byte,回滚指针,指向上一个版本数据在undo log 里的位置指针;

4. 聚集索引

聚集索引是指数据库表行中数据的物理顺序与键值的逻辑(索引)顺序相同。一个表只能有一个聚集索引,因为一个表的物理顺序只有一种情况,所以,对应的聚集索引只能有一个。

5. Undo Log

InnoDB 将行记录快照保存在 Undo Log,可以在回滚段中找到它们,主要用于记录数据被修改之前的日志,在对表信息做修改之前先会把数据拷贝到Undo Log里,当事务进行回滚时可以通过Undo Log里的日志进行数据还原。

回滚段中回滚指针间关联关系,如下图所示:

五、InnoDB是如何解决幻读的?

1、在读已提交的情况下,即使采用了 MVCC 方式也会出现幻读

我们同时开启事务 A 和事务 B,先在事务 A 中进行某个条件范围的查询,读取的时候采用排它锁,在事务 B 中增加一条符合该条件范围的数据,并进行提交,然后我们在事务 A 中再次查询该条件范围的数据,就会发现结果集中多出一个符合条件的数据,这样就出现了幻读。

出现幻读的原因是在读已提交的情况下,InnoDB 只采用记录锁(Record Locking)。

InnoDB 三种行锁的方式:

  • 记录锁:

针对单个行记录添加锁。

  • 间隙锁(Gap Locking):

可以锁住一个范围(索引之间的空隙),但不包括记录本身。

采用间隙锁的方式可以防止幻读情况的产生。

  • Next-Key 锁:

锁住一个范围,同时锁定记录本身,相当于间隙锁 + 记录锁,可以解决幻读的问题。

2、在可重复读的情况下,InnoDB 可以通过 Next-Key 锁 +MVCC 来解决幻读问题。

想插入球员艾利克斯·伦(身高 2.16 米)的时候,事务 B 会超时,无法插入该数据。

这是因为采用了 Next-Key 锁,会将 height>2.08 的范围都进行锁定,就无法插入符合这个范围的数据了。然后事务 A 重新进行条件范围的查询,就不会出现幻读的情况。

六、总结

MVCC 的核心就是 Undo Log+ Read View。

  • “MV”就是通过 Undo Log 来保存数据的历史版本,实现多版本的管理;
  • “CC”是通过 Read View 来实现管理,通过 Read View 原则来决定数据是否显示。

同时针对不同的隔离级别,Read View 的生成策略不同,也就实现了不同的隔离级别。

作者:架构精进之路,十年研发风雨路,大厂架构师,CSDN 博客专家,专注架构技术沉淀学习及分享,职业与认知升级,坚持分享接地气儿的干货文章,期待与你一起成长

关注并私信我回复“01”,送你一份程序员成长进阶大礼包,欢迎勾搭。

Thanks for reading!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…599600601…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%