0%

Java|项目深度解析

img

常用集合解析

必看资料:

渐进式本地缓存开发总结

Java集合面试题

Java集合源码分析

Java 编译期与运行期

JVM调试与栈溢出

必看资料:

循环依赖的解决方案

JDK 监控和故障处理工具总结

JVM 堆溢出抽丝剥茧定位的过程

JVM源码分析之栈溢出完全解读

JVM核心知识

一.栈溢出应用场景:循环依赖

在Springboot + JPA的架构中,容易出现循环依赖问题,一般会出现在一对多的场景下,总结来说是一对多实体中都要引用对方来维持OnetoMany的关系,所以极容易出现循环依赖:(

1.经典场景

订单项中引用订单,以构成多对一关系(可以使用订单id查到订单项)

1
2
3
4
// 一个订单可以有多个订单项(不同产品/不同用户,同一订单)
@ManyToOne
@JoinColumn(name="oid")
private Order order;

订单中引用订单项存储在集合中,用来存储从数据库查询来的结构(往往是因为要利用这些字段进行计算)

1
2
3
4
5
6
7
8
9
// 订单项列表
@Transient
private List<OrderItem> orderItems;
// 订单总金额
@Transient
private float total;
// 订单物品总数量
@Transient
private int totalNumber;

这样的结构就是循环依赖,导致数据重复加载,因为orderItems要调用方法填充,所以会为空(一般情况下会栈溢出)最终造成的数据是:Order含有orderItems,orderItems含有Order,Order的orderItem列表为空,所以这里的Order重复了一次

2.方案一:@JsonBackReference注解

JsonBackReference注解用在一(一对多的一)的一方,可以阻止其被序列化,前提是对应的接口不需要调用到它,而只是需要用它来查询

如:一个产品有多张图片,我们不需要在图片列表接口使用到产品信息,而只是需要用产品id查询其图片

产品类

1
2
3
4
5
6
7
@Transient
// 产品首图
private ProductImage firstProductImage;
@Transient
private List<ProductImage> productSingleImages;
@Transient
private List<ProductImage> productDetailImages;

产品图片类

1
2
3
4
@ManyToOne
@JoinColumn(name="pid")
@JsonBackReference
private Product product;

缺点

  • 关系是双向的,使用了JsonBackReference,就无法使用根据图片找到其属于的产品的方法,只能单方向查询即根据产品查找到其图片列表
  • JsonBackReference标记的字段与Redis的整合会有冲突
3.方案二:及时清除法

在服务层定义清除方法,在控制层调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Orderitem中有Order字段,标注多对一关系
// Order中有Orderitem列表,用于存储订单项列表
// Order中有Orderitem列表,而Orderitem中又有Order字段,产生无穷的递归
// 所以这里需要设置Orderitem的Order设为空
public void removeOrderFromOrderItem(List <Order> orders) {
for (Order order : orders) {
removeOrderFromOrderItem(order);
}
}

public void removeOrderFromOrderItem(Order order) {
List<OrderItem> orderItems= order.getOrderItems();
for (OrderItem orderItem : orderItems) {
orderItem.setOrder(null);
}
}
1
2
3
4
// 填充Order的orderItem列表
orderItemService.fill(page.getContent());
// 清除orderItem中的Order字段
orderService.removeOrderFromOrderItem(page.getContent());

二.JVM

参考资料:JVM核心知识

多线程解析

必看资料:

Java多线程核心知识

多线程应用场景

Spring多线程批量发送邮件

Spring AOP解析

必看资料:

Spring @Cacheable注解类内部调用失效的解决方案

Spring AOP应用

AOP面试题

一.缓存AOP拦截失效问题

Spring @Cacheable注解类内部调用失效的解决方案

1.问题出现原因

因为Springboot的缓存机制是通过切面编程aop来实现,从fill方法中调用listByCategory即内部调用,aop是拦截不到的,自然不会走缓存,这里我们可以通过SpringContextUtil工具类诱发aop

1
2
3
4
5
6
7
8
9
10
// 填充分类中的产品集合
public void fill(Category category) {
// 通过SpringContextUtil调用listByCategory上的缓存方法
// 即 @Cacheable(key="'products-cid-'+ #p0.id")
// 这样在方法内部的查询也能够使用缓存
ProductService productService = SpringContextUtil.getBean(ProductService.class);
List<Product> products = productService.listByCategory(category);
productImageService.setFirstProdutImages(products);
category.setProducts(products);
}
2.问题解决方案

SpringContextUtil工具类诱发aop

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
package com.how2java.tmall.util;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

@Component
public class SpringContextUtil implements ApplicationContextAware {

private SpringContextUtil() {

}

private static ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext){
SpringContextUtil.applicationContext = applicationContext;
}

public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}

}

二.AOP与日志处理

Spring AOP应用

三.SpringBoot原理

1.SpringBoot自动配置过程

image-20220923081735147

2.SpringBoot启动过程

image-20220923081811756

MySQL解析

必看资料

MySQL常见面试题总结

谈谈 MySQL 的 JSON 数据类型

简单总结 mysql json类型的利与弊

MySQL索引详解

数据库索引为什么使用B+树

一.物资申请系统数据库信息

MySQL常见面试题总结

1.物资申请表

image-20220504104541342

  • 用户id : 物资申请条目 = 1 : n
  • 机构id : 物资申请条目 = 1 : n
2.物资申请详情表

image-20220504105313339

  • 物资申请条目id : 物资申请详情条目 = n : n
  • 物资id : 物资申请详情条目 = n : n
  • (物资申请条目id,物资id) : 物资申请详情条目 = 1 : n
3.物资表

image-20220504105953610

4.用户表

image-20220504110242618

  • 用户信息条目 : 权限id = 1 : 1
5.权限表

image-20220504110408506

二.数据库分表

谈谈 MySQL 的 JSON 数据类型

简单总结 mysql json类型的利与弊

1.相关表的结构

此处展示的表结构为维护前

物资申请表:共4817条数据

image-20220303154338625

物资信息表:

image-20220303154513022

goods_count:当前仓库物品数(物理的)

good_leftCount:当前物品可借数(网络的:存在部分未借出,但已被预订仍在审核中的物品)

2.优化思路:物资申请表分表

改版前的系统使用的数据库是5.4版本,其默认的引擎是MyISAM 引擎,为了让数据库有更好的性能,我们将系统的数据库升级到了5.7.26版本,InnoDB 是 气的默认存储引擎

从上面的tw_apply表就可以知道:

  • 在用户提出申请后,物资申请信息被后端拼成了一个字符串存储在apply_content(同时利用了前端的数据执行了物品可借数的预扣除,所以这部分没有用到物资申请信息字符串的解析)

  • 通过审核后,物品正式借出,这时候只留有物资申请信息的字符串存储在数据库,所以需要后端对该字符串解析提取出申请物资与其借用数量,再去操作数据库

由于Mysql对JSON类型的支持是5.7以后的版本才有的,所以之前版本的物品申请内容字符串是以物品 + 申请数量并用逗号隔开多个物品申请内容这样的格式构成,我一开始也考虑其转换为JSON格式,但是在考虑到应用场景后,决定对其进行分表,将多对多关系分为了两个一对多关系

数据库设计十分不合理,甚至不符合第一范式,浪费数据库大量存储空间不说,而且后端拼接字符串解析字符串这一过程十分耗时且占用内存,而且最新的需求是需要增加一个审核过程申请物资调整功能

所以我将物资申请表进行分表(水平分表),分出物资申请详情表并联系物资信息表,其结构如下

image-20220303164602406

删除掉apply_content字段,节省数据库空间

分表后,通过tw_applydetail表,我们对物资申请信息的所以内容进行操作,省去了物资审核接口对字符串解析的耗时过程并且方便审核过程申请物资调整功能的开发(通过tw_appdetail找到物品信息和物品数量)

3.优化操作:存储过程脚本

存储过程(Stored Procedure)是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象

这里值得注意的是在遍历游标的循环中,如果查询不存在或为空会跳出循环

当时经过一天的对存储过程的学习,我总结出了以下经验:存储过程非常不方便调试,而且报错信息只定位不报错误类型(sql是这样的)。如果能重来,对数据库的批量操作,首选Python或Shell

4.优化结果
  • 截至目前物资申请表已有4817条数据,考虑到后面数据会长期积累,这样的优化是有必要的
  • 去掉后端耗时耗内存的字符串解析工作
  • 节省数据库存储空间,优化前申请表内存占0.79MB,优化后占0.56MB

另外附加一个容量查询小工具,可查询数据库各表容量大小

1
2
3
4
5
6
7
8
9
select
table_schema as '数据库',
table_name as '表名',
table_rows as '记录数',
truncate(data_length/1024/1024, 2) as '数据容量(MB)',
truncate(index_length/1024/1024, 2) as '索引容量(MB)'
from information_schema.tables
where table_schema='bgs'
order by data_length desc, index_length desc;

三.数据库索引的使用

MySQL索引详解

数据库索引为什么使用B+树

由于日志表数据庞大,有3万条数据,为了达到快速通过用户名模糊查找到日志操作内容和操作时间,我一开始的方案是选择了使用索引,以操作人作为索引模糊查询操作日志

1.相关表结构

日志记录表:共33687条数据

image-20220303191047146

2.优化思路:添加索引

另外在模糊查询中,like语句要使索引生效,like后不能以%开始,也就是说 (like %字段名%) 、(like %字段名)这类语句会使索引失效,而(like 字段名)、(like 字段名%)这类语句索引是可以正常使用

所以我将查询的模糊匹配由“%xxxx%”改为“xxxx%”,只模糊匹配前面部分

3.优化操作

这里直接使用Navicat可视化添加索引,因为后台查询日志是需要用用户名模糊查找到日志操作内容和操作时间,所以需要添加的索引为log_realnam

image-20220303193939350

更改mybatis的sql映射,解决sql注入和索引失效问题

1
SELECT log_realname, log_content, log_time FROM tw_log WHERE log_realname LIKE "%${log_name}%";  

在这种情况下使用#程序会报错,新手程序员就把#号改成了$,这样如果java代码层面没有对用户输入的内容做处理势必会产生SQL注入漏洞。

正确写法:

1
SELECT log_realname, log_content, log_time FROM tw_log WHERE log_realname LIKE concat(‘%’,#{log_name}, ‘%’) 
4.优化结果
  • 添加索引前使用用户名模糊查询日志,耗时大约0.045s,添加索引后耗时大约0.032s,减少了磁盘IO,提高了查询速度
  • 修改mybatis中模糊查询的sql语句,解决索引失效的问题,并解决了模糊查询中拼接字符串的sql注入问题

但是加索引这种方案没有被采纳,因为在系统上操作日志包括了,注册登录申请审批等操作,插入是非常频繁的,而日志查询只会被管理员少量使用,所以后续使用了ES来提高查询效率

大数据框架解析

必看资料:

Spring Data Elasticsearch基本使用

史上最全面的Elasticsearch使用指南

一.ES搜索操作日志

史上最全面的Elasticsearch使用指南

1.ES准备

ES是什么

elasticsearch简写es,es是一个高扩展、开源的全文检索和分析引擎,它可以准实时地快速存储、搜索、分析海量的数据,而这正好符合我们的需求,物资申请系统的操作日志刚好是一个存储频繁,又需要对大量数据进行查询统计的场景

什么是全文检索

全文检索是指计算机索引程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。这个过程类似于通过字典中的检索字表查字的过程。全文搜索搜索引擎数据库中的数据。

配置ES

1
2
# ElasticSearch
spring.data.elasticsearch.cluster-nodes = 127.0.0.1:9300

ES注解实体类

1
2
3
// @Document注解Category实体类,一个Category对象即为一个Document(相当于数据库的一行)
// 连接到es的tmall_springboot索引(相当于数据库),produt类(相当于表)上
@Document(indexName = "tmall_springboot",type = "product")
2.esDAO的创建

由于整合了ES的JPA和操作数据库使用的JPA有冲突,所以不能放在同一个包下

1
2
3
4
5
6
7
8
9
10
11
12
package com.how2java.tmall.es;

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

import com.how2java.tmall.pojo.Product;
// 用于链接es的DAO
// esDAO和其他DAO不能放在一个包下否则会启动异常
// 主要使用es实现对产品的模糊查询
public interface ProductESDAO extends ElasticsearchRepository<Product,Integer>{

}

3.ES与数据库同步

增删改操作

增删改操作的数据需要同步ES和数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 通过ProductDAO对数据库有影响的
// 都要通过productESDAO同步到es
@CacheEvict(allEntries=true)
public void add(Product bean) {
productDAO.save(bean);
productESDAO.save(bean);
}


@CacheEvict(allEntries=true)
public void delete(int id) {
productDAO.delete(id);
productESDAO.delete(id);
}


@CacheEvict(allEntries=true)
public void update(Product bean) {
productDAO.save(bean);
productESDAO.save(bean);
}

ES初始化

ES内数据为空,就将数据库中的数据同步到es

1
2
3
4
5
6
7
8
9
10
11
12
13
// 初始化数据到es
private void initDatabase2ES() {
Pageable pageable = new PageRequest(0, 5);
Page<Product> page =productESDAO.findAll(pageable);
// 查询es中是否有数据
if(page.getContent().isEmpty()) {
// 如果数据为空,将数据从数据库同步到es中
List<Product> products= productDAO.findAll();
for (Product product : products) {
productESDAO.save(product);
}
}
}
4.ES查询
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
// 通过es进行查询
public List<Product> search(String keyword, int start, int size) {
// 初始化es
initDatabase2ES();

// QueryBuilders提供了大量静态方法,用于生成各种不同类型的查询对象
// 构建查询条件(多条件查询)
FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery()
// 为提供的字段名和文本创建一个通用查询
.add(QueryBuilders.matchPhraseQuery("name", keyword),
ScoreFunctionBuilders.weightFactorFunction(100))
// 设置权重分为求和模式
.scoreMode("sum")
// 设置权重分最低分
.setMinScore(10);

// 设置分页参数
Sort sort = new Sort(Sort.Direction.DESC,"id");
Pageable pageable = new PageRequest(start, size,sort);

// 添加分页参数和查询条件
SearchQuery searchQuery = new NativeSearchQueryBuilder()
.withPageable(pageable)
.withQuery(functionScoreQueryBuilder).build();

// 执行查询获取结果
Page<Product> page = productESDAO.search(searchQuery);
// 返回结果
return page.getContent();
}

Redis解析

必看资料:

缓存基础常见面试题总结

Redis常见面试题总结

渐进式本地缓存开发总结

缓存一致性问题解决基本方案

缓存一致性问题解决进阶方案

一.Redis需求分析

我们为了避免用户在请求数据的时候获取速度过于缓慢,同时也为了承受大量的并发请求,所以我们在数据库之上增加了缓存这一层来弥补,本系统主要使用的是Redis,将常用的数据存储在缓存中(如物品,用户信息等)

另外推荐使用RedisClient,数据一般都在db0中

image-20220310201748019

二.Redis配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Redis数据库索引(默认为0)
spring.redis.database=0
# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
# 连接池最大连接数(使用负值表示没有限制)
spring.redis.pool.max-active=10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
spring.redis.pool.max-wait=-1
# 连接池中的最大空闲连接
spring.redis.pool.max-idle=8
# 连接池中的最小空闲连接
spring.redis.pool.min-idle=0
# 连接超时时间(毫秒)
spring.redis.timeout=0

三.缓存的使用

缓存的使用一般在服务层使用

1.有序集合管理

通过在服务层中注解@CacheConfig,创建一个有序集合类型的缓存,管理该服务下所有的keys

1
2
3
4
5
6
7
8
// 分类服务层
@Service
// redis缓存一般都在服务层进行操作
// 分类服务下的所有keys都由categories来管理(数据存储与categories是平行关系)
@CacheConfig(cacheNames="categories")
public class CategoryService {
.....
}

image-20220310202613389

2查询插入缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 获得单条分类服务
// 添加一条缓存到redis中,以categories-one- + 参数id为key值
// 存储的主要数据为Category对象
@Cacheable(key="'categories-one-'+ #p0")
public Category get(int id) {
Category c= categoryDAO.findOne(id);
return c;
}

// 列出单页分类表(提供分页组索引)
// 添加一条缓存到redis中,以categories-page- + 参数start + 参数size 为key值
// 存储的主要数据为Page4Navigator<Category>数组
@Cacheable(key="'categories-page-'+#p0+ '-' + #p1")
public Page4Navigator<Category> list(int start, int size, int navigatePages) {
Sort sort = new Sort(Sort.Direction.DESC, "id");
Pageable pageable = new PageRequest(start, size, sort);
Page pageFromJPA =categoryDAO.findAll(pageable);

return new Page4Navigator<>(pageFromJPA,navigatePages);
}

返回的java对象或集合都会变成JSON字符串

image-20220310203123882

image-20220310203207786

3.更新删除缓存

为了应对并发的申请请求提高,我们在Mysql数据库前加了一层Redis,所以在我开发后台物资储存量调整接口时遇到了缓存和数据库中物品数量不一致的问题

准确来说是插入,删除,更新删除缓存以保持数据一致性

使用@CacheEvict(allEntries=true)删除category~keys的所有keys

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 增加删除更新时
// 增加分类服务
@CacheEvict(allEntries=true)
public void add(Category bean) {
categoryDAO.save(bean);
}

// 删除分类服务
@CacheEvict(allEntries=true)
public void delete(int id) {
categoryDAO.delete(id);
}

// 更新分类服务
@CacheEvict(allEntries=true)
public void update(Category bean) {
categoryDAO.save(bean);
}

四.缓存一致问题解决

基本:缓存一致性问题解决基本方案

进阶:缓存一致性问题解决进阶方案

分布式微服务解析

参考资料:

RPC与Dubbo

SpringCloud Alibaba 及其组件

消息队列

SpringCloud Alibaba详解

高并发高可用解析

高可用系统设计指南

限流相关算法

系统设计与性能测试解析

系统设计