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

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


  • 首页

  • 归档

  • 搜索

Java之Excel导出功能快速实现

发表于 2021-11-10

前言

我们都知道Java解析、生成Excel比较有名的框架有Apache poi、jxl,但他们都存在一个严重的问题就是非常的耗内存,而且最重要的是这些工具类搭建比较麻烦。因为我们都是打工的仔嘛,当然需要讲究效率啦,如果可以5分钟完成这个excel功能,那领导还不得敬你三分哈哈!

所以有了本篇文章,帮助大家快速实现基于SpringBoot的excel导出功能,我们选用的是alibaba的easyexcel框架,这个框架在解析excel简直yyds,本文主要给大家介绍如何快速接入实现。

一、引入依赖

1
2
3
4
5
6
xml复制代码<!-- excel导出 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.2.11</version>
</dependency>

二、核心工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
typescript复制代码/**
* 下载
*
* @param response web响应
* @param datas 导出数据
* @param clazz 实体类
* @param fileName 导出文件名称
* @return
*/
public static void download(HttpServletResponse response, List datas, Class clazz, String fileName) {
// parseHead(获取实体类定义的属性名称,也就是excel表头)
download(response, datas, clazz, parseHead(clazz), fileName);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码/**
* 解析实体类
*
* @param clazz 实体类
* @return
*/
private static List<List<String>> parseHead(Class clazz) {
Field[] fields = clazz.getDeclaredFields();
List<List<String>> heads = new ArrayList<>();

for (Field field : fields) {
List<String> head = new ArrayList<>();
// 在开发我们应该少不了swagger, 这个注解是swagger提供的,当然我们也可以自定义一个注解。(作用是为了属性名映射中文名称输出到excel表头)
ApiModelProperty apiAnnotation = field.getAnnotation(ApiModelProperty.class);
head.add(apiAnnotation.value());
heads.add(head);
}
return heads;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码/**
* 下载
*
* @param response web响应
* @param datas 导出数据
* @param head 表头
* @param fileName 导出文件名称
* @return
*/
public static void download(HttpServletResponse response, List datas, Class clazz, List<List<String>> head, String fileName) {
// 设置web响应输出的文件名称
setResponseHeader(response, fileName);
try {
// 核心中的核心
export(response.getOutputStream(), head, datas, clazz);
} catch (IOException e) {
log.info("无法获取响应流", e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typescript复制代码/**
* 设置web响应输出的文件名称
* @param response web响应
* @param fileName 导出文件名称
*/
private static void setResponseHeader(HttpServletResponse response, String fileName) {
response.reset();
response.setContentType("application/vnd.ms-excel;charset=utf-8");
try {
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName + ".xls", "UTF-8"));
} catch (UnsupportedEncodingException e) {
log.info("不支持的编码", e);
}
response.setCharacterEncoding("UTF-8");
}

核心中的核心来了!!!

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复制代码/**
*
* @param os 字节输出流
* @param head 表头
* @param datas 导出数据
* @param clazz 实体类
* @throws IOException
*/
private static void export(OutputStream os, List<List<String>> head, List datas, Class clazz) throws IOException {
if (datas == null) {
datas = new ArrayList();
}
ExcelWriter excelWriter = null;
BufferedOutputStream bos = new BufferedOutputStream(os);
try {
excelWriter = EasyExcel.write(bos, clazz).build();
WriteSheet testSheet = EasyExcel.writerSheet("sheet1")
.head(head)
.build();
excelWriter.write(datas, testSheet);
} catch (Exception e) {
log.info("easyexcel初始化错误", e);
} finally {
bos.flush();
if (excelWriter != null) {
excelWriter.finish();
}
}
}

到这里我们就把整个核心逻辑搭建好啦,是不是很简单,接下来给大家跑个例子。

三、实战

  1. 定义一个http接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
less复制代码@RequestMapping("/excel")
@RestController
@Api(tags = "excel解析")
@Slf4j
public class ExcelTestController {

@PostMapping("/export")
public void export(HttpServletResponse response) {
List<ExcelExportResponseDTO> list = Lists.newArrayList();
ExcelExportResponseDTO info1 = new ExcelExportResponseDTO();
info1.setName("rose");
info1.setAge("23");
list.add(info1);
ExcelUtil.download(response, list, ExcelExportResponseDTO.class, "个人信息");
}

}
  1. 访问http接口

  1. 输出excel结果

四、总结

到这里就完成了整个Java之Excel导出功能的实现,大家只需要Ctrl+C、Ctrl+V就可以快速应用啦,希望可以帮助到大家,下期输出excel导入功能的实现!

加油打工人!奥利给😎

我是rose,感谢各位的观看,各位的点赞就是rose输出的最大动力,我们下篇文章见!

注:如果本篇博客有任何错误和建议,欢迎人才们留言!

五、系列推荐

《Java之Excel导入功能快速实现》

本文转载自: 掘金

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

方法入参很复杂,每次调用都要构造BO入参?一招教你自动构造入

发表于 2021-11-10

场景

同在互联网打工的小伙伴们肯定都面临这样一种场景:

通用逻辑(被多处调用)我们通常会封装成一个方法,那这个方法入参正常来说都不会少,(在开发规范中,经常会看到一条”方法入参正常不超过3个”。)面对入参超过3个的方法我们正常都会封装成一个bo类(这样扩展方法入参,再也不怕被领导diss了)。

如下图,假设现在有三个逻辑需要去调用这个方法,我们是不是都需要去build入参呀,这也是很多小伙伴会选择的调用方式。

大家都知道,在咱们这个行业最不缺的就是挑刺儿的人,这些人经常也被称为有工匠精神的码农哈哈哈!!!

那么就有工匠精神的码农看不惯红框内的代码的(怎么看都是重复代码),每次调用方法都去build入参,太麻烦了!!!

连调用方法都嫌传参麻烦,难道这就是有工匠精神的码农? 不能理解!!! 不过倒也挺好奇究竟是怎么做到自动构造参数的,所以也就有了本篇文章。

优化

现在我们假设上面提到的那个方法是一个订单价格的方法,其实这是我最近遇到的一个真实业务场景: 这里我们只需要了解影响订单价格的因素有订单明细单价、工艺单价、价格类型、订单明细长宽高,以及对应明细下单数量。面对这么多参数,而且这个方法很多地方用到,每个地方去构建参数确实是比较麻烦。 那怎么去做到自动构建入参呢,接下来就是文章的重点了。

  1. 我们首先要思考一个问题,这些参数的源头是哪里呢? (1)可能是下单时候前端传过来的参数。(controller层用XXXDTO接收) (2)也可能是修改某个订单明细导致订单价格须重新计算,那这个时候需要从数据库查询该订单其他订单明细重新计算,这个时候我们定义XXXPOJO类接收查询结果。(这个为啥不是entity呢,因为这些参数来自不同的实体类) 这里扯多了,不理解上面场景的小伙伴,可以理解成入参来自不同的类。
  2. 参数来自不同的类,而这个方法又得去兼容这多个类。

面对这样的场景我们不禁联想到万能数据线。Type-C接口、苹果接口、安卓接口共同连接着USB接口,USB接口才是连接电源的入口。至于这三种接口怎么和USB连接才可以工作就是线内部要做事情了,外部只要关心不同类型手机对应不同的接口就可以了。 大家仔细品一下上面这个我们再常见不过的例子,我觉得太能类比到我们这个场景了。 (1) Type-C接口、苹果接口、安卓接口 –> 想调用方法的不同类(不同数据来源) (2) USB接口 –> 方法入参 (3) 电源 –> 方法主体

也就是说我们要让这多个类与入参产生关联才可以无缝调用方法主体。

  1. 我们不妨把方法的入参定义成一个接口Adapter形式(接口定义各个我们上面说的决定订单价格因素的获取方法),那么这样不同类(数据来源)就可以去实现这个接口,必定重写方法,这就相当于把构建入参的操作移到dto去做。

上面可能说的太抽象了,所以给大家再上个图,看一下具体结构

光说光看,还不如来个实操理解快呢,早知道会这么想,接下来,上代码!!!

(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
33
34
csharp复制代码/**
* 方法入参
*/
public interface OrderAmountCalAdapter {
/**
* 单价 单位:分
*/
BigDecimal obtainPrice();

/**
* 工艺单价 单位:分
*/
BigDecimal obtainCraftPrice();

/**
* 价格类型 2平方价格 3立方价格
*/
Integer obtainPriceType();

/**
* 长度 单位:毫米 mm
*/
Integer obtainLength();

/**
* 宽度 单位:毫米 mm
*/
Integer obtainWidth();

/**
* 厚度 单位:毫米 mm
*/
Integer obtainHeight();
}

(2)主方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
csharp复制代码/**
* 计算方法
*/
public long calculate(OrderAmountCalAdapter adapter, Integer qty) {
if (adapter == null) {
throw new ApiException("计算订单金额时载体对象不能为空");
}
if (adapter.obtainPrice() == null) {
throw new ApiException("计算订单金额时金额不能为空");
}
if (adapter.obtainPriceType() == null) {
throw new ApiException("计算订单金额时价格类型不能为空");
}
if (adapter.obtainLength() == null || adapter.obtainWidth() == null || adapter.obtainHeight() == null) {
throw new ApiException("计算订单金额时商品属性信息不能为空");
}
if (qty == null) {
throw new ApiException("计算订单金额时数量不能为空");
}

return doCalculate(adapter, qty).longValue();
}

(3)不同DTO类(不同数据源)

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
kotlin复制代码/**
* 不同DTO类,通过实现OrderAmountCalAdapter接口,重写方法获取DTO属性值,也就是上面说的把构建入参移到具体的DTO类来做
*/
@Data
public class OrderProductProperty implements OrderAmountCalAdapter {

@ApiModelProperty(value = "明细ID")
private Long id;

@ApiModelProperty(value = "明细单价")
private Long price;

@ApiModelProperty(value = "下单数量")
private Integer orderQuantity;

@ApiModelProperty(value = "工艺单价")
private Long craftPrice;

@ApiModelProperty(value = "长度(单位mm)")
private Integer length;

@ApiModelProperty(value = "宽度(单位mm)")
private Integer width;

@ApiModelProperty(value = "高度(单位mm)")
private Integer height;

@ApiModelProperty("价格类型 2平方 3立方")
private Integer priceType;


@Override
public BigDecimal obtainPrice() {
return BigDecimal.valueOf(getPrice() == null ? 0L : getPrice());
}

@Override
public BigDecimal obtainCraftPrice() {
return BigDecimal.valueOf(getCraftPrice() == null ? 0L : getCraftPrice());
}

@Override
public Integer obtainPriceType() {
return getPriceType();
}

@Override
public Integer obtainLength() {
return getLength();
}

@Override
public Integer obtainWidth() {
return getWidth();
}

@Override
public Integer obtainHeight() {
return getHeight();
}

}

这就达到我们上面所说的不用构建方法的入参就可以调用方法了,是不是很精妙哈哈哈。

其实仔细想一想,这不就是适配者设计模式。

1
复制代码适配器模式的主要作⽤就是把原本不兼容的接⼝,通过适配修改做到统⼀。

原本只是想说偷个懒,没想到不知不觉还用上了设计模式,代码b格一下提高了,leader连忙称赞小伙子不错不错!

总结

其实本文说的招数,其实就是适配器模式,相信大家也理解到这么做的好处了。

在业务开发中我们会经常的需要做不同接⼝或者方法的兼容,那么这个时候就要想到适配器模式。

设计模式其实是前辈们开发中总结的一下通用范式,我们要合理的学习每种设计模式适合场景,解决什么问题。也不要因设计模式而在写代码时强行使用。

在面对各种各样的开发场景,我们要多思考,多沉淀,才可以让自己的代码更优雅,更好扩展。

加油打工人!奥利给😎

我是rose,感谢各位的观看,各位的点赞就是rose输出的最大动力,我们下篇文章见!

本文转载自: 掘金

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

又谈mysql,面试官问表结构设计要注意啥? 字段类型注意事

发表于 2021-11-10

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

字段类型注意事项

数字类型

1.整型

MySQL 的整型类型所占用的存储空间及取值范围:

类型 所占空间 范围(signed) 范围(unsigned)
tinyint 1 -128~127 0~255
smallint 2 -32768~32767 0~65535
mediumint 3 -8388608~8388607 0~16777215
int 4 -2147483648~2147483647 0~4294967295
bigint 8 -9223372036854775808~-9223372036854775807 0~18446744073709551615

2. 注意 unsigned 属性

  • MySQL 要求 unsigned 数值相减之后依然为 unsigned,否则就会报错
    (BIGINT UNSIGNED value is out of range in...)
  • 为了避免这个错误,需要对数据库参数 sql_mode 设置为 NO_UNSIGNED_SUBTRACTION,允许相减的结果为 signed。
1
mysql复制代码SET sql_mode='NO_UNSIGNED_SUBTRACTION';

3. 浮点类型和高精度型

  1. 从 MySQL 8.0.17 版本开始,MySQL 将不建议使用浮点类型 Float 或 Double,高精度 DECIMAL 类型可以使用。
  2. 在海量并发的互联网业务中使用,金额字段的我们并不推荐使用 DECIMAL 类型,推荐使用整型类型。
  • 资金以分单位代替元单位存储
  • 类型 DECIMAL 是通过二进制实现的一种编码方式,计算效率远不如整型来的高效。因此,推荐使用 BIG INT 来存储金额相关的字段。

4. 用自增整型做主键,一律使用 BIGINT,而不是 INT

  1. INT 的范围最大在 42 亿的级别,但是对于海量的数据存储,INT 类型的上限很快就会达到。不要为了节省 4 个字节使用 INT,否则在后期再修改表结构代价是巨大的。
  2. 当达到 INT 上限后,再次进行自增插入时,会报重复错误。

字符串类型

char 和 varchar

  • CHAR(N) 用来保存固定长度的字符,N 的范围是 0 ~ 255,注意,N 表示的是字符,不是字节。
  • VARCHAR(N) 用来保存变长字符,N 的范围为 0 ~ 65536, N 表示字符。
  • 超出 65536 个字符时,可以使用更大的字符类型 TEXT 或 BLOB,两者最大存储长度为 4G,其区别是 BLOB 没有字符集属性,纯属二进制存储。

字符集

  • 常见的字符集有 GBK、UTF8,一般我们会把默认字符集设置为 UTF8。但是某些 emoji 表情字符无法在 UTF8 字符集下存储,所以推荐把 MySQL 的默认字符集设置为 UTF8MB4。
  • 修改列字符集的命令应该使用

ALTER TABLE ... CONVERT TO CHARSET ...

才能将已经存在的列的字符集进行修改。

排序规则

排序规则(Collation)是比较和排序字符串的一种规则,每个字符集都会有默认的排序规则,可以使用命令 SHOW CHARSET 来查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql复制代码mysql> SHOW CHARSET LIKE 'utf8%';
+---------+---------------+--------------------+--------+
| Charset | Description | Default collation | Maxlen |
+---------+---------------+--------------------+--------+
| utf8 | UTF-8 Unicode | utf8_general_ci | 3 |
| utf8mb4 | UTF-8 Unicode | utf8mb4_0900_ai_ci | 4 |
+---------+---------------+--------------------+--------+


mysql> SHOW COLLATION LIKE 'utf8mb4%';
+----------------------------+---------+-----+---------+----------+---------+---------------+
| Collation | Charset | Id | Default | Compiled | Sortlen | Pad_attribute |
+----------------------------+---------+-----+---------+----------+---------+---------------+
| utf8mb4_0900_ai_ci | utf8mb4 | 255 | Yes | Yes | 0 | NO PAD |
| utf8mb4_0900_as_ci | utf8mb4 | 305 | | Yes | 0 | NO PAD |
| utf8mb4_0900_as_cs | utf8mb4 | 278 | | Yes | 0 | NO PAD |
| utf8mb4_0900_bin | utf8mb4 | 309 | | Yes | 1 | NO PAD |
| utf8mb4_bin | utf8mb4 | 46 | | Yes | 1 | PAD SPACE |
......
  • 排序规则以 _ci 结尾,表示不区分大小写(Case Insentive)
  • 排序规则以 _cs 表示大小写敏感
  • 排序规则以 _bin 表示通过存储字符的二进制进行比较

其他

CHECK 约束功能

对于 性别 或 表示状态的字段,推荐使用 CHECK 约束功能。

  • MySQL 8.0.16 版本开始,数据库原生提供 CHECK 约束功能。
  • 避免了使用 tinyint 类型产生的表达不清(值代表实际意义不明确)、脏数据(可能会存入其他值)的产生。
  • 如下,约束定义列 sex 的取值范围,只能是 M 或者 F。同时,当插入非法数据时,MySQL 会显式地抛出违法约束的提示(Check constraint 'user_chk_1' is violated.)。
1
2
3
4
5
6
7
8
mysql复制代码CREATE TABLE User (
id BIGINT NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
sex CHAR(1) NOT NULL,
password VARCHAR(1024) NOT NULL,
CHECK (sex = 'M' OR sex = 'F'),
PRIMARY KEY(id)
);

JSON 类型

从 MySQL 5.7 版本开始支持JSON 类型,无须预定义字段,很方便的对产品进行描述。

  • JSON 类型比较适合存储一些修改较少、相对静态的数据。
  • MySQL 8.0.17 版本开始支持 Multi-Valued Indexes,用于在 JSON 数组上创建索引,通过函数 member of、json_contains、json_overlaps 可以快速检索索引数据。
  • 存储的 JSON 内容,上限是1G。

日期类型

DATETIME

从 MySQL 5.6 版本开始,DATETIME 类型支持毫秒,DATETIME(N) 中的 N 表示毫秒的精度。

TIMESTAMP

  • TIMESTAMP 时间戳类型,存储的内容为‘1970-01-01 00:00:00’到现在的毫秒数。
  • MySQL 中,由于类型 TIMESTAMP 占用 4 个字节,因此其存储的时间上限只能到‘2038-01-19 03:14:07’。
  • 若带有毫秒时,类型 TIMESTAMP 占用 7 个字节,而 DATETIME 无论是否存储毫秒信息,都占用 8 个字节。

选择

推荐日期类型使用 DATETIME,而不是 TIMESTAMP 和 INT 类型;

  1. INT 类型也是存毫秒数,本质和 TIMESTAMP 一样,因此用 INT 不如直接使用 TIMESTAMP。
  2. 距离 TIMESTAMP 的可用最大值‘2038-01-19 03:14:07’已经很近。业务上用 TIMESTAMP 存在风险。
  3. 使用 TIMESTAMP 必须显式地设置时区,不要使用默认系统时区,否则存在性能问题,推荐在配置文件中设置参数 time_zone = '+08:00'。
  • 性能问题 : 则每次通过时区计算时间时,要调用操作系统底层系统函数 __tz_convert(),这个函数需要额外的加锁操作,以确保这时操作系统时区没有修改。所以,当大规模并发访问时,由于热点资源竞争。导致性能不如 DATETIME。

三范式与反三范式

第一范式(1NF)

概念:数据表的每一列都要保持它的原子特性,也就是列不能再被分割。

第二范式(2NF)

概念:属性必须完全依赖于主键。
在第一范式的基础上更进一步,解决部分依赖,目标是确保表中的每列都和主键相关。

第三范式(3NF)

概念:所有的非主属性不依赖于其他的非主属性。
在第二范式的基础上更进一步,解决传递依赖,目标是确保表中的列都和主键直接相关,而不是间接相关。

反范式化

我们应从业务角度出发,设计出符合范式准则要求的表结构。

  • 反范式化指的是通过增加冗余或重复的数据来换时间增加效率,违反第二第三范式。
  • 反范式化可以减少关联查询时,join表的次数。
  • 在一些场景下,可以通过 JSON 数据类型进行反范式设计,提升存储效率。

本文转载自: 掘金

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

Java六种异步转同步方案,总有一款适合你

发表于 2021-11-10

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

一、问题

应用场景

应用中通过框架发送异步命令时,不能立刻返回命令的执行结果,而是异步返回命令的执行结果。

那么,问题来了,针对应用中这种异步调用,能不能像同步调用一样立刻获取到命令的执行结果,如何实现异步转同步?

二、分析

首先,解释下同步和异步

  • 同步,就是发出一个调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。
  • 异步,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。

对于异步调用,调用的返回并不受调用者控制。

异步转同步主要实现思路:所有实现原理类似,是在发出调用的线程中进行阻塞等待结果,调用完成后通过回调、设置共享状态或通知进行阻塞状态的解除,继续执行后续操作。

三、实现方法

通常,实现中,不会无限的等待,一般会设定一个超时时间,具体超时时间根据具体场景确定。

下面以回调的方式介绍几种常用实现异步转同步的方法:

1.轮询与休眠重试机制

采用轮询与休眠重试机制,线程将反复在休眠和测试状态条件中之间切换,直到超时或者状态条件满足继续向下执行。这种方式,超时时间控制不准确,sleep时间需要在响应性和CPU使用率之间进行权衡。

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
}复制代码private static long MILLIS_OF_WAIT_TIME = 300000L;// 等待时间 5分钟
private final Object lock = new Object();

//3.结果返回后进行回调,解除阻塞
@Override
public void callback(AsynResponse response){
synchronized(lock){
//设置状态条件
}

public Result getResult() throws ErrorCodeException {
// 1.异步调用

// 2.阻塞等待异步响应
long future = System.currentTimeMillis() + MILLIS_OF_WAIT_TIME;
long remaining = MILLIS_OF_WAIT_TIME;//剩余等待时间
while(remaining > 0){
synchronized(lock){
if(状态条件未满足){
remaining = future - System.currentTimeMillis();
Thread.sleep(时间具体场景确定);
}
}
````}

//4.超时或结果正确返回,对结果进行处理

return result;
}

2.wait/notify

任意一个Java对象,都拥有一组监视器方法(wait、notify、notifyAll等方法),这些方法和synchronized同步关键字配合,可以实现等待/通知模式。但是使用wait/notify,使线程的阻塞/唤醒对线程本身来说是被动的,要准确的控制哪个线程是很困难的,所以是要么随机唤醒等待在条件队列上一个线程(notify),要么唤醒所有的(notifyAll,但是很低效)。当多个线程基于不同条件在同一条件队列上等待时,如果使用notify而不是notifyAll,很容易导致信号丢失的问题,所以必须谨慎使用wait/notify方法。

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
java复制代码private static long MILLIS_OF_WAIT_TIME = 300000L;// 等待时间 5分钟
private final Object lock = new Object();

//3.结果返回后进行回调,解除阻塞
@Override
public void callback(AsynResponse response){
synchronized(lock){
lock.notifyAll();
}

public Result getResult() throws ErrorCodeException {
// 1.异步调用

// 2.阻塞等待异步响应
long future = System.currentTimeMillis() + MILLIS_OF_WAIT_TIME;
long remaining = MILLIS_OF_WAIT_TIME;//剩余等待时间
synchronized(lock){
while(条件未满足 && remaining > 0){ //被通知后要检查条件
lock.wait(remaining);
remaining = future - System.currentTimeMillis();
}
````}

//4.超时或结果正确返回,对结果进行处理
return result;
}

3.Lock Condition

使用Lock的Condition队列的实现方式和wait/notify方式类似,但是Lock支持多个Condition队列,并且支持等待状态中响应中断。

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复制代码private static long SECONDS_OF_WAIT_TIME = 300L;// 等待时间 5分钟
private final Lock lock = new ReentrantLock();
private final Condition condition = lock.newCondition();

//3.结果返回后进行回调,解除阻塞
@Override
public void callback(AsynResponse response){
lock.lock();//这是前提
try {
condition.signal();
}finally {
lock.unlock();
}
}

public Result getResult() throws ErrorCodeException {
// 1.异步调用
// 2.阻塞等待异步响应
lock.lock();//这是前提
try {
condition.await();
} catch (InterruptedException e) {
//TODO
}finally {
lock.unlock();
}
//4.超时或结果正确返回,对结果进行处理
return result;
}

4.CountDownLatch

使用CountDownLatch可以实现异步转同步,它好比计数器,在创建实例CountDownLatch对象的时候传入数字,每使用一次 countDown() 方法计数减1,当数字减到0时, await()方法后的代码将可以执行,未到0之前将一直阻塞等待。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码private static long SECONDS_OF_WAIT_TIME = 300L;// 等待时间 5分钟
private final CountDownLatch countDownLatch = new CountDownLatch(1);

//3.结果返回后进行回调,解除阻塞
@Override
public void callback(AsynResponse response){
countDownLatch.countDown();
}

public Result getResult() throws ErrorCodeException {
// 1.异步调用

// 2.阻塞等待异步响应
try {
countDownLatch.await(SECONDS_OF_WAIT_TIME, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//TODO
}
//4.超时或结果正确返回,对结果进行处理
return result;
}

5.CyclicBarrier

让一组线程达到一个屏障(也可以叫同步点)时被阻塞,直到等待最后一个线程到达屏障时,屏障才开门,所有被屏障拦截的线程才会继续执行。

每个线程通过调用await方法告诉CyclicBarrier我已经到达了屏障,然后当前的的线程被阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码private static long SECONDS_OF_WAIT_TIME = 300L;// 等待时间 5分钟
private final CountDownLatch cyclicBarrier= new CyclicBarrier(2);//设置屏障拦截的线程数为2

//3.结果返回后进行回调,解除阻塞
@Override
public void callback(AsynResponse response){
//我也到达屏障了,可以开门了
cyclicBarrier.await();
}

public Result getResult() throws ErrorCodeException {
// 1.异步调用
// 2.阻塞等待异步响应
try {
//我到达屏障了,还没开门,要等一等
cyclicBarrier.await(SECONDS_OF_WAIT_TIME, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//TODO
}
//4.超时或结果正确返回,对结果进行处理
return result;
}

CountDownLatch和CyclicBarrier实现类似,区别是CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset重置,

所以CyclicBarrier能处理更为复杂的业务场景。在异步转同步中,计数器不会重用,所以使用CountDownLatch实现更适合。

6.LockSupport

LockSupport定义了一组公共静态方法,提供了最基本的线程阻塞和唤醒的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码private static long NANOS_OF_WAIT_TIME = 300000000L;// 等待时间 5分钟
private final LockSupport lockSupport = new LockSupport();

//3.结果返回后进行回调,解除阻塞
@Override
public void callback(AsynResponse response){
lockSupport.unpark();
}

public Result getResult() throws ErrorCodeException {
// 1.异步调用

// 2.阻塞等待异步响应
try {
lockSupport.parkNanos(NANOS_OF_WAIT_TIME);
} catch (InterruptedException e) {
//TODO
}
//4.超时或结果正确返回,对结果进行处理
return result;

}

今天多学一点,明天就少说一句求人的话!加油

本文转载自: 掘金

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

jvm——堆

发表于 2021-11-10

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战」

什么时候会用到堆内存

通过new关键字创建的对象都会使用堆内存。

概念

对于java应用程序来讲,堆是jvm所管理的内存中最大的一块。是被所有线程共享的一块区域,并在虚拟机启动时创建。此区域的唯一目的就是存放对象实例,java程序里“几乎”所有的对象实例都会在这里创建并分配内存。

在jvm规范中对堆的描述是“所有的对象实例以及数组都应当在堆上分配”

jvm规范中规定:堆可以处于物理上不连续的内存空间中,但在逻辑上它应该 被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。

堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的Java虚拟机都是按照可扩 展来实现的(通过参数 -Xmx 和 -Xms 设定)。假如堆中没有内存完成实例分配了,并且也无法进行扩展时 jvm 就会抛出 OutOfMemoryError 异常。

特点

1. 是线程共享的,所以堆中对象都需要考虑线程的安全问题。

2. 有垃圾回收机制

3. 会内存溢出

堆内存溢出

虽然有垃圾回收机制,当对象不会再被使用时就会被当作垃圾回收释放掉。

但是对象不断被产生并且任然有在使用时,这个时候这些对象并不会被当作垃圾回收掉,当对象达到一定的数量时就会使得堆内存被耗尽,从而产生堆内存溢出问题。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码public static void main(String[] args){
int i = 0;
try{
List<String> stringList = new ArrayList<>();
String a = "hello";
while(true){
stringList.add(a);
a = a + a;
i++;
}
}catch(Throwable a){
e.printStackTrace();
Sytem.out.println(i);
}
}

在运行这个例子的时候我把堆内存设置成了 8M -Xmx8m ,从而能更快的让jvm抛出溢出信息。

image.png

运行以上例子抛出了内存溢出的信息:java.lang.OutOfMemoryError:Java heap space

前面是内存溢出的错误信息,后面 Java heap space 翻译过来是 Java 堆空间,才是定位具体是不是堆内存溢出信息。

image.png

这个例子就是对象一直被引用不断被插入信息使得 stringList 大小超出了堆内存的大小。

本文转载自: 掘金

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

这sql优化的15招,让性能提升了100倍

发表于 2021-11-10

前言

sql优化是一个大家都比较关注的热门话题,无论你在面试,还是工作中,都很有可能会遇到。

如果某天你负责的某个线上接口,出现了性能问题,需要做优化。那么你首先想到的很有可能是优化sql语句,因为它的改造成本相对于代码来说也要小得多。

那么,如何优化sql语句呢?

这篇文章从15个方面,分享了sql优化的一些小技巧,希望对你有所帮助。

图片

1 避免使用select *

很多时候,我们写sql语句时,为了方便,喜欢直接使用select *,一次性查出表中所有列的数据。

反例:

1
sql复制代码select * from user where id=1;

在实际业务场景中,可能我们真正需要使用的只有其中一两列。查了很多数据,但是不用,白白浪费了数据库资源,比如:内存或者cpu。

此外,多查出来的数据,通过网络IO传输的过程中,也会增加数据传输的时间。

还有一个最重要的问题是:select *不会走覆盖索引,会出现大量的回表操作,而从导致查询sql的性能很低。

那么,如何优化呢?

正例:

1
sql复制代码select name,age from user where id=1;

sql语句查询时,只查需要用到的列,多余的列根本无需查出来。

2 用union all代替union

我们都知道sql语句使用union关键字后,可以获取排重后的数据。

而如果使用union all关键字,可以获取所有数据,包含重复的数据。

反例:

1
2
3
sql复制代码(select * from user where id=1) 
union 
(select * from user where id=2);

排重的过程需要遍历、排序和比较,它更耗时,更消耗cpu资源。

所以如果能用union all的时候,尽量不用union。

正例:

1
2
3
sql复制代码(select * from user where id=1) 
union all
(select * from user where id=2);

除非是有些特殊的场景,比如union all之后,结果集中出现了重复数据,而业务场景中是不允许产生重复数据的,这时可以使用union。

3 小表驱动大表

小表驱动大表,也就是说用小表的数据集驱动大表的数据集。

假如有order和user两张表,其中order表有10000条数据,而user表有100条数据。

这时如果想查一下,所有有效的用户下过的订单列表。

可以使用in关键字实现:

1
2
sql复制代码select * from order
where user_id in (select id from user where status=1)

也可以使用exists关键字实现:

1
2
sql复制代码select * from order
where exists (select 1 from user where order.user_id = user.id and status=1)

前面提到的这种业务场景,使用in关键字去实现业务需求,更加合适。

为什么呢?

因为如果sql语句中包含了in关键字,则它会优先执行in里面的子查询语句,然后再执行in外面的语句。如果in里面的数据量很少,作为条件查询速度更快。

而如果sql语句中包含了exists关键字,它优先执行exists左边的语句(即主查询语句)。然后把它作为条件,去跟右边的语句匹配。如果匹配上,则可以查询出数据。如果匹配不上,数据就被过滤掉了。

这个需求中,order表有10000条数据,而user表有100条数据。order表是大表,user表是小表。如果order表在左边,则用in关键字性能更好。

总结一下:

  • in 适用于左边大表,右边小表。
  • exists 适用于左边小表,右边大表。

不管是用in,还是exists关键字,其核心思想都是用小表驱动大表。

4 批量操作

如果你有一批数据经过业务处理之后,需要插入数据,该怎么办?

反例:

1
2
3
css复制代码for(Order order: list){
   orderMapper.insert(order):
}

在循环中逐条插入数据。

1
2
sql复制代码insert into order(id,code,user_id) 
values(123,'001',100);

该操作需要多次请求数据库,才能完成这批数据的插入。

但众所周知,我们在代码中,每次远程请求数据库,是会消耗一定性能的。而如果我们的代码需要请求多次数据库,才能完成本次业务功能,势必会消耗更多的性能。

那么如何优化呢?

正例:

1
makefile复制代码orderMapper.insertBatch(list):

提供一个批量插入数据的方法。

1
2
sql复制代码insert into order(id,code,user_id) 
values(123,'001',100),(124,'002',100),(125,'003',101);

这样只需要远程请求一次数据库,sql性能会得到提升,数据量越多,提升越大。

但需要注意的是,不建议一次批量操作太多的数据,如果数据太多数据库响应也会很慢。批量操作需要把握一个度,建议每批数据尽量控制在500以内。如果数据多于500,则分多批次处理。

5 多用limit

有时候,我们需要查询某些数据中的第一条,比如:查询某个用户下的第一个订单,想看看他第一次的首单时间。

反例:

1
2
3
4
sql复制代码select id, create_date 
 from order 
where user_id=123 
order by create_date asc;

根据用户id查询订单,按下单时间排序,先查出该用户所有的订单数据,得到一个订单集合。然后在代码中,获取第一个元素的数据,即首单的数据,就能获取首单时间。

1
2
ini复制代码List<Order> list = orderMapper.getOrderList();
Order order = list.get(0);

虽说这种做法在功能上没有问题,但它的效率非常不高,需要先查询出所有的数据,有点浪费资源。

那么,如何优化呢?

正例:

1
2
3
4
5
sql复制代码select id, create_date 
 from order 
where user_id=123 
order by create_date asc 
limit 1;

使用limit 1,只返回该用户下单时间最小的那一条数据即可。

此外,在删除或者修改数据时,为了防止误操作,导致删除或修改了不相干的数据,也可以在sql语句最后加上limit。

例如:

1
2
bash复制代码update order set status=0,edit_time=now(3) 
where id>=100 and id<200 limit 100;

这样即使误操作,比如把id搞错了,也不会对太多的数据造成影响。

6 in中值太多

对于批量查询接口,我们通常会使用in关键字过滤出数据。比如:想通过指定的一些id,批量查询出用户信息。

sql语句如下:

1
2
csharp复制代码select id,name from category
where id in (1,2,3...100000000);

如果我们不做任何限制,该查询语句一次性可能会查询出非常多的数据,很容易导致接口超时。

这时该怎么办呢?

1
2
3
bash复制代码select id,name from category
where id in (1,2,3...100)
limit 500;

可以在sql中对数据用limit做限制。

不过我们更多的是要在业务代码中加限制,伪代码如下:

1
2
3
4
5
6
7
8
9
kotlin复制代码public List<Category> getCategory(List<Long> ids) {
   if(CollectionUtils.isEmpty(ids)) {
      return null;
   }
   if(ids.size() > 500) {
      throw new BusinessException("一次最多允许查询500条记录")
   }
   return mapper.getCategoryList(ids);
}

还有一个方案就是:如果ids超过500条记录,可以分批用多线程去查询数据。每批只查500条记录,最后把查询到的数据汇总到一起返回。

不过这只是一个临时方案,不适合于ids实在太多的场景。因为ids太多,即使能快速查出数据,但如果返回的数据量太大了,网络传输也是非常消耗性能的,接口性能始终好不到哪里去。

最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。

我以往的技术群里技术氛围非常不错,大佬很多。

image.png

加微信:su_san_java,备注:加群,即可加入该群。

7 增量查询

有时候,我们需要通过远程接口查询数据,然后同步到另外一个数据库。

反例:

1
sql复制代码select * from user;

如果直接获取所有的数据,然后同步过去。这样虽说非常方便,但是带来了一个非常大的问题,就是如果数据很多的话,查询性能会非常差。

这时该怎么办呢?

正例:

1
2
3
bash复制代码select * from user 
where id>#{lastId} and create_time >= #{lastCreateTime} 
limit 100;

按id和时间升序,每次只同步一批数据,这一批数据只有100条记录。每次同步完成之后,保存这100条数据中最大的id和时间,给同步下一批数据的时候用。

通过这种增量查询的方式,能够提升单次查询的效率。

8 高效的分页

有时候,列表页在查询数据时,为了避免一次性返回过多的数据影响接口性能,我们一般会对查询接口做分页处理。

在mysql中分页一般用的limit关键字:

1
2
sql复制代码select id,name,age 
from user limit 10,20;

如果表中数据量少,用limit关键字做分页,没啥问题。但如果表中数据量很多,用它就会出现性能问题。

比如现在分页参数变成了:

1
2
sql复制代码select id,name,age 
from user limit 1000000,20;

mysql会查到1000020条数据,然后丢弃前面的1000000条,只查后面的20条数据,这个是非常浪费资源的。

那么,这种海量数据该怎么分页呢?

优化sql:

1
2
bash复制代码select id,name,age 
from user where id > 1000000 limit 20;

先找到上次分页最大的id,然后利用id上的索引查询。不过该方案,要求id是连续的,并且有序的。

还能使用between优化分页。

1
2
sql复制代码select id,name,age 
from user where id between 1000000 and 1000020;

需要注意的是between要在唯一索引上分页,不然会出现每页大小不一致的问题。

9 用连接查询代替子查询

mysql中如果需要从两张以上的表中查询出数据的话,一般有两种实现方式:子查询 和 连接查询。

子查询的例子如下:

1
2
sql复制代码select * from order
where user_id in (select id from user where status=1)

子查询语句可以通过in关键字实现,一个查询语句的条件落在另一个select语句的查询结果中。程序先运行在嵌套在最内层的语句,再运行外层的语句。

子查询语句的优点是简单,结构化,如果涉及的表数量不多的话。

但缺点是mysql执行子查询时,需要创建临时表,查询完毕后,需要再删除这些临时表,有一些额外的性能消耗。

这时可以改成连接查询。具体例子如下:

1
2
3
sql复制代码select o.* from order o
inner join user u on o.user_id = u.id
where u.status=1

10 join的表不宜过多

根据阿里巴巴开发者手册的规定,join表的数量不应该超过3个。

反例:

1
2
3
4
5
6
7
8
csharp复制代码select a.name,b.name.c.name,d.name
from a 
inner join b on a.id = b.a_id
inner join c on c.b_id = b.id
inner join d on d.c_id = c.id
inner join e on e.d_id = d.id
inner join f on f.e_id = e.id
inner join g on g.f_id = f.id

如果join太多,mysql在选择索引的时候会非常复杂,很容易选错索引。

并且如果没有命中中,nested loop join 就是分别从两个表读一行数据进行两两对比,复杂度是 n^2。

所以我们应该尽量控制join表的数量。

正例:

1
2
3
4
css复制代码select a.name,b.name.c.name,a.d_name 
from a 
inner join b on a.id = b.a_id
inner join c on c.b_id = b.id

如果实现业务场景中需要查询出另外几张表中的数据,可以在a、b、c表中冗余专门的字段,比如:在表a中冗余d_name字段,保存需要查询出的数据。

不过我之前也见过有些ERP系统,并发量不大,但业务比较复杂,需要join十几张表才能查询出数据。

所以join表的数量要根据系统的实际情况决定,不能一概而论,尽量越少越好。

最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。

我以往的技术群里技术氛围非常不错,大佬很多。

image.png

加微信:su_san_java,备注:加群,即可加入该群。

11 join时要注意

我们在涉及到多张表联合查询的时候,一般会使用join关键字。

而join使用最多的是left join和inner join。

  • left join:求两个表的交集外加左表剩下的数据。
  • inner join:求两个表交集的数据。

使用inner join的示例如下:

1
2
3
4
sql复制代码select o.id,o.code,u.name 
from order o 
inner join user u on o.user_id = u.id
where u.status=1;

如果两张表使用inner join关联,mysql会自动选择两张表中的小表,去驱动大表,所以性能上不会有太大的问题。

使用left join的示例如下:

1
2
3
4
sql复制代码select o.id,o.code,u.name 
from order o 
left join user u on o.user_id = u.id
where u.status=1;

如果两张表使用left join关联,mysql会默认用left join关键字左边的表,去驱动它右边的表。如果左边的表数据很多时,就会出现性能问题。

要特别注意的是在用left join关联查询时,左边要用小表,右边可以用大表。如果能用inner join的地方,尽量少用left join。

12 控制索引的数量

众所周知,索引能够显著的提升查询sql的性能,但索引数量并非越多越好。

因为表中新增数据时,需要同时为它创建索引,而索引是需要额外的存储空间的,而且还会有一定的性能消耗。

阿里巴巴的开发者手册中规定,单表的索引数量应该尽量控制在5个以内,并且单个索引中的字段数不超过5个。

mysql使用的B+树的结构来保存索引的,在insert、update和delete操作时,需要更新B+树索引。如果索引过多,会消耗很多额外的性能。

那么,问题来了,如果表中的索引太多,超过了5个该怎么办?

这个问题要辩证的看,如果你的系统并发量不高,表中的数据量也不多,其实超过5个也可以,只要不要超过太多就行。

但对于一些高并发的系统,请务必遵守单表索引数量不要超过5的限制。

那么,高并发系统如何优化索引数量?

能够建联合索引,就别建单个索引,可以删除无用的单个索引。

将部分查询功能迁移到其他类型的数据库中,比如:Elastic Seach、HBase等,在业务表中只需要建几个关键索引即可。

13 选择合理的字段类型

char表示固定字符串类型,该类型的字段存储空间的固定的,会浪费存储空间。

1
2
sql复制代码alter table order 
add column code char(20) NOT NULL;

varchar表示变长字符串类型,该类型的字段存储空间会根据实际数据的长度调整,不会浪费存储空间。

1
2
sql复制代码alter table order 
add column code varchar(20) NOT NULL;

如果是长度固定的字段,比如用户手机号,一般都是11位的,可以定义成char类型,长度是11字节。

但如果是企业名称字段,假如定义成char类型,就有问题了。

如果长度定义得太长,比如定义成了200字节,而实际企业长度只有50字节,则会浪费150字节的存储空间。

如果长度定义得太短,比如定义成了50字节,但实际企业名称有100字节,就会存储不下,而抛出异常。

所以建议将企业名称改成varchar类型,变长字段存储空间小,可以节省存储空间,而且对于查询来说,在一个相对较小的字段内搜索效率显然要高些。

我们在选择字段类型时,应该遵循这样的原则:

  1. 能用数字类型,就不用字符串,因为字符的处理往往比数字要慢。
  2. 尽可能使用小的类型,比如:用bit存布尔值,用tinyint存枚举值等。
  3. 长度固定的字符串字段,用char类型。
  4. 长度可变的字符串字段,用varchar类型。
  5. 金额字段用decimal,避免精度丢失问题。

还有很多原则,这里就不一一列举了。

14 提升group by的效率

我们有很多业务场景需要使用group by关键字,它主要的功能是去重和分组。

通常它会跟having一起配合使用,表示分组后再根据一定的条件过滤数据。

反例:

1
2
3
sql复制代码select user_id,user_name from order
group by user_id
having user_id <= 200;

这种写法性能不好,它先把所有的订单根据用户id分组之后,再去过滤用户id大于等于200的用户。

分组是一个相对耗时的操作,为什么我们不先缩小数据的范围之后,再分组呢?

正例:

1
2
3
vbnet复制代码select user_id,user_name from order
where user_id <= 200
group by user_id

使用where条件在分组前,就把多余的数据过滤掉了,这样分组时效率就会更高一些。

其实这是一种思路,不仅限于group by的优化。我们的sql语句在做一些耗时的操作之前,应尽可能缩小数据范围,这样能提升sql整体的性能。

15 索引优化

sql优化当中,有一个非常重要的内容就是:索引优化。

很多时候sql语句,走了索引,和没有走索引,执行效率差别很大。所以索引优化被作为sql优化的首选。

索引优化的第一步是:检查sql语句有没有走索引。

那么,如何查看sql走了索引没?

可以使用explain命令,查看mysql的执行计划。

例如:

1
sql复制代码explain select * from `order` where code='002';

结果:图片)通过这几列可以判断索引使用情况,执行计划包含列的含义如下图所示:图片如果你想进一步了解explain的详细用法,可以看看我的另一篇文章《explain | 索引优化的这把绝世好剑,你真的会用吗?》

说实话,sql语句没有走索引,排除没有建索引之外,最大的可能性是索引失效了。

下面说说索引失效的常见原因:图片如果不是上面的这些原因,则需要再进一步排查一下其他原因。

此外,你有没有遇到过这样一种情况:明明是同一条sql,只有入参不同而已。有的时候走的索引a,有的时候却走的索引b?

没错,有时候mysql会选错索引。

必要时可以使用force index来强制查询sql走某个索引。

至于为什么mysql会选错索引,后面有专门的文章介绍的,这里先留点悬念。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

本文转载自: 掘金

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

高并发场景下模拟商品秒杀 1简介 2项目准备 3项目实

发表于 2021-11-10

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

1.简介

通过模拟抢购商品的实践阐述高并发与锁的问题。这里假设电商网站抢购的场景,电商网站往往存在很多的商品,有些商品会以低价限量推销,并且会在推销之前做广告以吸引网站会员购买。特别是热销产品,很有可能会出现瞬时高并发的抢购,也就是我们常说的“商品秒杀”。这种情况在工作中很是常见,而且在面试的时候往往也是一个热点考察的问题,下面就由我来给大家讲解下如何处理这类高并发问题。

2.项目准备

首先,我先在redis中放入stock和商品数量,数量为100。
然后再通过jmeter来模拟高并发场景。

3.项目实践

3.1 单机服务版本代码

首先,我们先来看一个基础的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RequestMapping("/getStock")
public String getStock(){
//加锁
synchronized (this){
//从redis中取库存
int stock = Integer.parseInt(redisTemplate.opsForValue().get(STOCK));
//判断剩余库存
if(stock>0){
//如果还有库存,则购买
int newStock = stock-1;
//再放回redis
redisTemplate.opsForValue().set(STOCK,String.valueOf(newStock));
System.out.println("扣减成功,当前库存为:"+newStock);
}else {
System.out.println("扣减【失败】,当前库存为:"+stock);
}
return "success";
}

扣减成功,当前库存为:99

扣减成功,当前库存为:98

扣减成功,当前库存为:97

………………

扣减成功,当前库存为:9

扣减成功,当前库存为:8

扣减成功,当前库存为:7

扣减成功,当前库存为:6

扣减成功,当前库存为:5

扣减成功,当前库存为:4

扣减成功,当前库存为:3

扣减成功,当前库存为:2

扣减成功,当前库存为:1

扣减成功,当前库存为:0

扣减【失败】,当前库存为:0

扣减【失败】,当前库存为:0

扣减【失败】,当前库存为:0

扣减【失败】,当前库存为:0

扣减【失败】,当前库存为:0

…………

从结果看来,这样的代码在单机环境下没有什么问题
但如果是分布式部署,那这样就会出现超卖现象!

image.png

image.png

很显然,上面的代码并不满足分布式环境下的系统需求,下面我们将使用分布式锁来改进这个问题。

3.2 分布式服务版本代码

在这个2.0的版本里,我们使用redis中的setnx数据结构来做分布式锁。

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
java复制代码    private static String LOCK_KEY = "lockKey";

@RequestMapping("/getStock2")
public String getStock2() {
//如果这里不设置过期时间,很可能会出现问题,例如其中一台机器未释放锁便出现异常或宕机,那后面的请求都无法购买商品
Boolean result = redisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY, "lock", 10, TimeUnit.SECONDS);
if (!result) {
return "活动太火爆了,请稍后重试";
}

try {
int stock = Integer.parseInt(redisTemplate.opsForValue().get(STOCK));
if (stock > 0) {
int newStock = stock - 1;
redisTemplate.opsForValue().set(STOCK, String.valueOf(newStock));
System.out.println("扣减成功,扣减后库存为:" + newStock);
} else {
System.out.println("扣减【失败】,当前库存为:" + stock);
}
} finally {
//此处要尽量保证锁的释放
redisTemplate.delete(LOCK_KEY);
}
return "success";
}

上面这个代码,在并发不太高的情况下,基本可以使用,然而在高并发情况下依然存在锁失效问题。

image.png

解决这个问题的关键是,我自己加的锁应该只能由我自己释放。

3.3 使用redisson实现分布式锁

要实现一个好的分布式锁,应包含以下功能:

  1. 指定一个 key 作为锁标记,存入 Redis 中,指定一个 唯一的用户标识 作为 value。
  2. 当 key 不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足 互斥性 特性。
  3. 设置一个过期时间,防止因系统异常导致没能删除这个 key,满足 防死锁 特性。
  4. 当处理完业务之后需要清除这个 key 来释放锁,清除 key 时需要校验 value 值,需要满足 只有加锁的人才能释放锁 。

image.png

这些我们都可以通过redisson来轻松的使用分布式锁,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码 @RequestMapping("/getStock3")
public String getStock3() {
RLock rLock = redisson.getLock(LOCK_KEY);
try {
rLock.lock();
int stock = Integer.parseInt(redisTemplate.opsForValue().get(STOCK));
if (stock > 0) {
int newStock = stock - 1;
redisTemplate.opsForValue().set(STOCK, String.valueOf(newStock));
System.out.println("扣减成功,扣减后库存为:" + newStock);
} else {
System.out.println("扣减【失败】,当前库存为:" + stock);
}
} finally {
rLock.unlock();
}
return "success";
}

在大部分情况下,我们都可以使用redisson来完成我们的分布式锁,来应对高并发的问题,

本文转载自: 掘金

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

yum 指令

发表于 2021-11-10

这是我参与11月更文挑战的第 9 天,活动详情查看:2021最后一次更文挑战

yum(Yellowdog Updater Modified):是一个基于 RPM 的软件包管理器,能够从指定服务器自动下载RPM包并且安装,可以处理软件之间的依赖关系,一次性安装所有依赖的软件包,无需一个个下载安装。

工作原理示意图

yum 客户端及服务器的工作原理如下图所示

image.png

yum 的配置文件

配置文件所在目录:/etc/yum.repos.d

1
2
3
4
5
6
7
ini复制代码# vim /etc/yum.repos.d/alios.repo
----------------------------------------
[alios.7u2.base.$basearch]
name=alios
baseurl=http://yum.tbsite.net/alios/7u2/os/$basearch/ # $basearch:系统基础架构,如 x86_64
gpgcheck=0
----------------------------------------

x.repo 文件相关配置 key 简介:

  • 1、[alios.7u2.base.$basearch]: 仓库的ID,可以取任意名字,只要不和其他的ID冲突即可
  • 2、name=xxx: 用于描述容器含义
  • 3、enabled={1|0}: 是否启用这个仓库,0表示不启用,1表示启用,默认是启用的
  • 4、mirrorlist: 列出这个容器可以使用的镜像站点(如果不想使用,可以注释,本案例中没有体现)
  • 5、baseurl=url: 容器地址,mirrorlist 是由 yum 程序自行找镜像站点,baseurl 则是指定一个固定容器地址
  • 6、gpgcheck={1|0}: 是否进行签名合法性检测,0 表示不启用,1表示启用,如果选择启用 gpg 检查,则需要告知其 key 是什么
  • 7、gpgkey=url: 如果启用 gpg 检测,则需要指定 gpgkey 的路径,即使导入过 gpgkey,这里仍然需要手动为其指定路径,这个路径可以是远程服务器上的,也可以是本地的,只要让本地客户端访问到即可

如果两个仓库里的 RPM 包是一样的,一个在远程服务器上,另一个在本地光盘上,那么本地光盘的访问速度通常会快于远程服务器上。在配置文件中,我们可以定义这样的两个仓库,为其中一个设定优先级

注: gpgme.GpgmeError: (7, 32870, u’\ufffd\ufffd\ufffd\u8c78\ufffd\ufffd\ufffd\u02b5\ufffd\ufffd\ufffd ioctl \ufffd\ufffd\ufffd\ufffd’) #如果执行报这个错,表示当前系统字符编码不支持 Unicode,改一下就好**

配置 yum 数据源

默认情况下 yum 使用的源都是国外的地址,那我们如果期望下载速度更快一些时,就可以考虑使用国内的一些源,比如 aliyun 提供的, 以配置 docker yum 数据源为例:

1
2
3
4
5
bash复制代码$ sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
已加载插件:bestyumcache, fastestmirror, langpacks
adding repo from: http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
grabbing file http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo to /etc/yum.repos.d/docker-ce.repo
repo saved to /etc/yum.repos.d/docker-ce.repo

然后就可以在 /etc/yum.repos.d/ 下看到 docker-ce.repo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码$ sudo cat docker-ce.repo
[docker-ce-stable]
name=Docker CE Stable - $basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/7/$basearch/stable
enabled=1
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

[docker-ce-stable-debuginfo]
name=Docker CE Stable - Debuginfo $basearch
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/7/debug-$basearch/stable
enabled=0
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

[docker-ce-stable-source]
name=Docker CE Stable - Sources
baseurl=https://mirrors.aliyun.com/docker-ce/linux/centos/7/source/stable
enabled=0
gpgcheck=1
gpgkey=https://mirrors.aliyun.com/docker-ce/linux/centos/gpg

# ...省略其他

yum 的一些基本功能

yum 基本功能主要包括:查询、删除、更新/升级以及软件组等

查询

yum 查询有以下几种姿势,这里挨个举例。

yum search xxx

搜索某个软件名称或者描述的重要关键字

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码$ yum search java
已加载插件:bestyumcache, branch, fastestmirror, langpacks
Loading mirror speeds from cached hostfile
kubernetes 579/579
================================================================================ N/S matched: java =================================================================================
abrt-java-connector.x86_64 : JNI Agent library converting Java exceptions to ABRT problems
aether-javadoc.noarch : Java API documentation for Aether
alicpp-gcc492-netlib-java.x86_64 : alicpp-gcc492-netlib-java-1.1.2.odps
ant-antunit-javadoc.noarch : Javadoc for ant-antunit
ant-contrib-javadoc.noarch : Javadoc for ant-contrib
ant-javadoc.noarch : Javadoc for ant

yum info xxx

列出软件功能(不太方便透露的信息,以 xxxx 代替了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ruby复制代码$ yum info docker\
已加载插件:bestyumcache, branch, fastestmirror, langpacks\
Loading mirror speeds from cached hostfile\
可安装的软件包\
名称 :docker\
架构 :x86_64\
版本 :xxxx\
发布 :xxxx\
大小 :103 M\
源 :xxxx\
简介 :xxxx\
网址 :xxxx\
协议 : Commercial\
描述 : CodeUrl:git@xxxx\
: CodeRev:e3xx79d\
: AoneLog: xxxx\
: AoneUrl:xxxx\
: xxx container service docker-xxx; branch: vxxx\

yum list

列出 yum 服务器上面所有的软件名称,这里就不列了(下面一个是按规则搜的)

yum list xxxx*

找出以 xxx 开头的软件名称

1
2
3
4
5
csharp复制代码$ yum list docker
已加载插件:bestyumcache, branch, fastestmirror, langpacks
Loading mirror speeds from cached hostfile
可安装的软件包
docker.x86_64 version-xxx xxxx

yum list updates

列出 yum 服务器上可提供本机进行升级的软件(返回的信息是 yum list 的子集,这里也不举例了)

安装 or 升级

  • yum install/update 软件名称
  • yum install 软件名称 -y #安装过程中免输入y确认

删除

  • yum remove 软件名称

软件组

  • yum grouplist //查看容器和本机上可用与安装过的软件组
  • yum groupinfo group_name //查看group内所有组名称
  • yum install/remove group_name //安装与删除

除此之外,如果我们想升级所有的软件包,可以通过如下方式搞定

  • yum -y update 升级所有包,改变软件设置和系统设置,系统版本内核都升级
  • yum -y upgrade 升级所有包,不改变软件设置和系统设置,系统版本升级,内核不改变

已经上线的用yum -y upgrade 比较稳,全新的用yum -y update 会更好

参考

  • yum.baseurl.org/
  • blog.csdn.net/guohaosun/a…
  • blog.51cto.com/wuyelan/154…

本文转载自: 掘金

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

C++-左值和右值 左值和右值

发表于 2021-11-10

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」

左值和右值

什么是表达式

表达式由一个或多个运算对象(operand)组成,对表达式求值得到一个结果(result),字面值和变量是最简单的表达式,其结果就是字面值和变量的值。运算符(operator)和运算对象组合可以生成复杂的表达式。

左值和右值

C++的表达式的结果要么是左值(lvalue),要么是右值(rvalue):

  • 左值指向一个特定的内存位置
  • 右值不指向特定的内存位置
    通常来说右值是临时的和短暂的,而左值有更长的生存期。一个有意思的比喻是:左值是容器而右值是容器内的东西,没有容器,东西将很快过期。
    举个例子:
1
cpp复制代码int x=666;// ok

666是一个右值,因为一个数字(字面值常量)没有特定的内存地址,它仅在运行时临时保存在寄存器中
x变量有特定的内存地址,所以它是一个左值。
等号运算符要求左边的运算对象是一个左值,所以这里的表达式是合法的

1
cpp复制代码int* y = &x;// ok

这里取址运算符接受一个左值x生成一个右值,并把这个右值保存到y中

1
2
cpp复制代码int y;
666 = y; // error!

前面说过666是一个右值,没有特定的内存地址,把y的值保存到一个没有特定地址的内存中,这从常理上来说是没有意义的。
gcc会报错:

1
cpp复制代码error: lvalue required as left operand of assignment

类似的

1
cpp复制代码int* y = &666; // error!

gcc报错:

1
cpp复制代码error: lvalue required as unary '&' operand`

取址运算符要求一个左值作为输入,因为只有左值才有特定的地址可以取址

函数返回左值和右值

1
2
3
4
5
6
7
8
cpp复制代码int setValue()
{
return 6;
}

// ... somewhere in main() ...

setValue() = 3; // error!

这是错误的,因为赋值运算符要求左边的运算符是一个左值(准确的说是可修改的左值,后面解释),而函数返回的是右值

1
2
3
4
5
6
7
8
9
10
cpp复制代码int global = 100;

int& setGlobal()
{
return global;
}

// ... somewhere in main() ...

setGlobal() = 400; // OK

这是对的,因为函数返回了global变量的引用,它指向了global的地址,是一个左值,因此赋值表达式是合法的。

左值到右值的转换

根据C++规范,+运算符要求两个右值作为输入,并返回右值,观察如下代码

1
2
3
cpp复制代码int x = 1;
int y = 3;
int z = x + y; // ok

x和y都是左值,但是+要求两个右值,这是怎么回事?
这是因为底层发生了隐式的左值到右值的转换,很多运算符都会执行这种隐式转换。

左值引用

那么反过来,右值能转化为左值吗?不行。这并非技术限制,而是编程语言设计的限制。

1
2
3
cpp复制代码int y = 10;
int& yref = y;
yref++; // y is now 11

这里yref引用指向了y的地址,是一个左值引用。如果直接这么写

1
cpp复制代码int& yref = 10;  // will it work?

10是一个右值,没有特定的内存地址,而引用要求指向特定内存地址,因此这将出错。这就是“禁止右值转为左值”的一个例子。
试想一下,如果这种转换是合法的,那么就可以通过这个引用改变字面量常量,这听起来完全没有意义。最重要的是,如果右值消失,那么引用将指向什么?
下面的代码错误原因正是如此

1
2
3
4
5
6
7
8
9
10
11
cpp复制代码void fnc(int& x)
{
}

int main()
{
fnc(10); // Nope!
// This works instead:
// int x = 10;
// fnc(x);
}

一种解决方法是,声明一个变量保存10,再作为实参传入

常量左值引用

C++语言允许将常量左值绑定到右值,因此如下代码正确

1
cpp复制代码const int& ref = 10;  // OK!

背后的思想在于:引用一个右值存在刚刚说的两个问题:

  1. 改变字面量常量没有意义
  2. 右值消失
    如果我们让引用成为常量,就解决了这两个问题。

声明一个右值引用,给一个临时内存位置分配一个名称,这使得程序的其他部分访问该内存位置成为了可能,并且可以将这个临时位置变成一个左值。
左值表达式的求值结果是一个对象或者一个函数。当一个对象被用作右值时,用的时对象的值(内容),当对象被用作左值时,用的时候对象的身份(内存中的位置)
同理

1
2
3
4
5
6
7
8
cpp复制代码void fnc(const int& x)
{
}

int main()
{
fnc(10); // OK!
}

实际上编译器创建了一个隐藏的变量来保存字面量常量,引用x实际上绑定到了这个变量,在函数中我们甚至可以打印x的地址。
当我们写下

1
2
cpp复制代码// the following...
const int& ref = 10;

编译器为我们转换为

1
2
3
cpp复制代码// ... would translate to:
int __internal_unique_name = 10;
const int& ref = __internal_unique_name;

参考链接:

  1. www.internalpointers.com/post/unders…

本文转载自: 掘金

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

Laravel Artisan(工匠) 命令使用技巧 Art

发表于 2021-11-10

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战」

天下武功没有高低之分,只是习武之人有强弱之别。

上一篇介绍了 Laravel Eloquent 关联模型 进阶使用技巧 ,这篇介绍 Laravel Artisan 命令使用技巧。

Artisan 命令

学会用 Artisan 调试项目

Artisan 意为 工匠 手艺人,是非常好用的开发利器。

我们在开发过程中经常要进行各种测试,通过postman请求接口测试是一种思路,但是比较耗时。

我们可以使用 artisan 命令创建测试工具文件

1
bash复制代码php artisan make:command TestPhp

执行上述命令后会在项目中自动生成下述文件(非核心代码已用三个竖着的.省略),
我们可以在 handle() 方法中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
php复制代码<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class TestPhp extends Command
{
//改成自己的名字
protected $signature = 'TestPhp:Tool';

.
.
.

public function handle()
{
//在这里写需要测试的代码
}
}

我们通过下述命令就可以很方便的测试了

1
ruby复制代码php artisan TestPhp:Tool

在代码中使用 Artisan

我们不仅可以在命令行中启动 Artisan 命令,还可以携带参数地在代码中启动它,使用 Artisan::call() 方法即可:

1
2
3
4
5
6
7
css复制代码Route::get('/foo', function () {
$exitCode = Artisan::call('sms:send', [
'user' => 1, '--queue' => 'default'
]);

//
});

Artisan 命令参数

创建 Artisan 命令时,我们可以用以下方式询问输入:$this-> confirm() (确认),$this-> anticipate() (预期输入),$this->choice() (选择)。

1
2
3
4
5
6
7
8
9
10
kotlin复制代码// 输入是或者否
if ($this->confirm('你要继续执行吗?')) {
//
}

// 带有自动填充的开放问题
$name = $this->anticipate('你是谁?', ['小明', '小华']);

// 带有默认选中项的选项列表
$name = $this->choice('你是谁?', ['小明', '小华'], $defaultIndex);

维护模式

如果我们想要在网站上启用维护模式,执行下面的 Artisan 命令:

1
复制代码php artisan down

然后人们会看到默认的 503 页面。

在 Laravel 8 里,我们还可以提供下述标识:

用户将会重定向的路径地址
预渲染的维护模式视图页面
绕过维护模式的秘钥
维护模式返回的状态吗
每 X 秒重新加载页面

1
ini复制代码php artisan down --redirect="/" --render="errors::503" --secret="xxxxxxx-xxx-xxx-xxx-xxxxxxxxx" --status=200 --retry=60

在 Laravel 8 之前有:

维护模式显示的消息
每 X 秒重新加载页面
允许访问的 IP 地址

1
css复制代码php artisan down --message="系统更新中,请稍后访问" --retry=60 --allow=127.0.0.1

当我们完成了维护工作,只需要运行下述命令,网站就可以正常访问了。

1
复制代码php artisan up

Artisan 命令行帮助

要查看 Artisan 命令的相关选项,可以运行 Artisan 命令带上 –help 标识参数,比如 php artisan make:model --help

然后我们就可以看到提示了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
less复制代码Options:
-a, --all 为模型生成迁移类,填充类,工厂类和资源类
-c, --controller 为模型创建一个新的控制器
-f, --factory 为模型创建一个新的工厂类
--force 当模型已存在的时候强制创建类
-m, --migration 为模型创建一个新的迁移类
-s, --seed 为模型创建一个新的填充文件
-p, --pivot 用来标识生成的模型是否是自定义中间表模型
-r, --resource 用来标识生成的控制器是否是资源控制器
--api 用来标识生成的控制器是否是API控制器
-h, --help 显示帮助信息
-q, --quiet 不输出任何信息
-V, --version 显示应用版本
--ansi 使用ANSI输出
--no-ansi 禁用ANSI输出
-n, --no-interaction 不询问任何交互式问题
--env[=ENV] 该命令运行的配置环境
-v|vv|vvv, --verbose 显示更详细的消息,-v表示正常输出,-vv表示更详细的输出,-vvv表示增加显示调试信息

查看 Laravel 的版本

通过以下命令行,可以查看并确认我们的应用所使用 Lavavel 版本

1
css复制代码php artisan --version

Last but not least

技术交流群请到 这里来。 或者添加我的微信 wangzhongyang0601 ,一起学习。

感谢大家的点赞、评论、关注,谢谢大佬们的支持,感谢。

本文转载自: 掘金

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

1…377378379…956

开发者博客

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