优化多表查询:使用中间表和观察者模式提升性能

HYF Lv3

在现代软件开发中,数据库设计和查询优化是至关重要的。阿里巴巴 Java 开发手册中明确指出,使用多张表进行 JOIN 操作时不应超过三张表,因为超出这一数量会带来显著的性能问题。尤其是在数据量大、查询复杂的情况下,这种多表连接会导致查询效率低下,增加数据库负担,甚至可能影响系统的响应时间。

面对这些挑战,开发者们常常需要寻找有效的解决方案以优化查询性能。传统的做法是尽量减少 JOIN 操作的复杂度,但这并不总是可行。尤其是在需要从多个数据源获取信息时,如何高效地整合这些数据成为一个难题。

在本文中,我们提出了一种改进的方法——通过引入中间表来简化多表 JOIN 操作。具体来说,我们将多个表的数据冗余到一个中间表中,从而减少了查询时的 JOIN 复杂度。此外,我们还结合了观察者模式来解决数据同步的痛点问题,从而确保数据一致性,并提高了系统的维护性和性能。

接下来,我们将详细探讨这一方法的实现过程和效果,包括中间表的设计、观察者模式的应用,以及实际案例中的表现。

多表 JOIN 产生的问题

在关系型数据库中,JOIN 操作用于将来自不同表的数据合并在一起。虽然这种操作在处理多表数据时非常有用,但当涉及到多张表的 JOIN 时,会面临一系列挑战和问题。

性能瓶颈

随着 JOIN 的表数增加,数据库在执行查询时需要处理的计算量也随之增加。这通常导致以下性能问题:

查询延迟:多表 JOIN 可能导致查询时间显著增加,尤其是在数据量较大的情况下。数据库需要对多个表的数据进行排序、合并和计算,这些操作都消耗大量的计算资源

索引效率下降:虽然在单表 JOIN 中合理使用索引可以提高查询效率,但在多表 JOIN 中,索引的效果可能会被削弱,特别是当连接条件复杂或表的数据量很大时

复杂性增加

随着 JOIN 的表数增加,数据库在执行查询时需要处理的计算量也随之增加。这通常导致以下性能问题:

查询语句复杂:多表 JOIN 的查询语句通常非常复杂,涉及到多个表的条件、连接类型以及字段选择。这样的复杂查询不仅难以编写和维护,还容易出错

调试困难:当查询出现性能问题或错误时,调试复杂的多表 JOIN 语句往往很困难。定位和修复问题可能需要大量的时间和精力

扩展性问题

横向扩展困难:当系统需要扩展以处理更多数据时,多表 JOIN 的复杂性会进一步增加,尤其是在分布式数据库或分片环境中。有效地管理和优化这些查询变得更加困难

在应对这些挑战时,开发者需要寻找优化策略和方法,以提高查询性能和系统的可维护性。接下来,我们将介绍一种有效的解决方案——通过引入中间表来简化查询,并结合观察者模式来优化数据同步问题。

解决方案:使用中间表

中间表的概念

在处理多表 JOIN 操作时,中间表是一种有效的优化策略。中间表,顾名思义,是一种用于简化查询和数据管理的表,它存储了从多个源表中冗余的数据。通过将多个表的相关字段预先合并到一个中间表中,可以减少查询时需要进行的 JOIN 操作,从而降低查询的复杂性和提高性能。

中间表的主要优势包括

简化查询:减少复杂的多表 JOIN 操作,只需查询中间表即可获得所需的数据

提高性能:通过减少实时计算和连接操作,提高查询的执行效率

改善维护性:减少复杂查询语句的维护和调试工作

设计中间表

假设我们现在有以下表: goods (商品表)、inventory (库存表)、purchase (采购表)。他们的表字段分别为:

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
MariaDB [blog]> desc goods;
+---------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| id | char(19) | NO | PRI | NULL | |
| name | varchar(64) | NO | | NULL | |
| description | varchar(255) | YES | | NULL | |
| status | tinyint(4) | NO | | NULL | |
| specification | varchar(255) | YES | | NULL | |
+---------------+--------------+------+-----+---------+-------+
5 rows in set (0.001 sec)

# 字段依次为 id、商品名称、商品简介、商品状态、商品规格

MariaDB [blog]> desc inventory;
+----------+----------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------+----------+------+-----+---------+-------+
| id | char(19) | NO | PRI | NULL | |
| goods_id | char(19) | NO | | NULL | |
| quantity | int(11) | NO | | 0 | |
+----------+----------+------+-----+---------+-------+
3 rows in set (0.001 sec)

# 字段依次为 id、商品id、库存

MariaDB [blog]> desc purchase;
+---------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------+--------------+------+-----+---------+-------+
| id | char(19) | NO | PRI | NULL | |
| goods_id | char(19) | NO | | NULL | |
| purchase_date | date | NO | | NULL | |
| quantity | int(11) | NO | | 0 | |
| quantity | int(11) | NO | | 0 | |
| supplier_name | varchar(128) | NO | | NULL | |
+---------------+--------------+------+-----+---------+-------+
5 rows in set (0.001 sec)

# 字段依次为 id、商品id、采购日期、采购数量、供应商名称
MariaDB [blog]>

假设业务需求是,在填写采购单时,页面需要自动带出选中商品的最新库存信息(不考虑业务合理性哈,这里只是示例)。为此,我们可以通过创建一个中间表 inventory_goods,将 goodsinventory 表中的字段冗余到这个中间表中,从而简化查询。

原始的查询语句应该为:
1
2
3
4
5
6
7
8
9
10
11
SELECT
a.id AS id,
g.id AS goods_id,
g.NAME AS goods_name,
i.quantity AS latest_quantity
FROM
purchase a
LEFT JOIN goods g ON a.goods_id = g.id
LEFT JOIN inventory i ON g.id = i.goods_id
WHERE
a.id = '5555555555555555555';
现在我们将 goodsinventory 的字段冗余到一个中间表中 inventory_goods:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
MariaDB [blog]> desc inventory_goods;
+---------------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------------+--------------+------+-----+---------+-------+
| id | char(19) | NO | PRI | NULL | |
| inventory_id | char(19) | NO | | NULL | |
| goods_id | char(19) | NO | | NULL | |
| quantity | int(11) | NO | | 0 | |
| goods_name | varchar(64) | NO | | NULL | |
| goods_description | varchar(255) | YES | | NULL | |
| goods_status | tinyint(4) | NO | | NULL | |
| goods_specification | varchar(255) | YES | | NULL | |
+---------------------+--------------+------+-----+---------+-------+
8 rows in set (0.001 sec)
使用中间表 inventory_goods 后,我们可以将查询语句简化为:
1
2
3
4
5
6
7
8
9
10
SELECT
a.id AS id,
ig.id AS goods_id,
ig.NAME AS goods_name,
ig.quantity AS latest_quantity
FROM
purchase a
LEFT JOIN inventory_goods ig ON a.goods_id = ig.goods_id
WHERE
a.id = '5555555555555555555';

新的查询语句只需访问中间表 inventory_goods,避免了复杂的多表 JOIN 操作。这不仅简化了查询逻辑,还提高了查询性能。

这种方法显著简化了查询逻辑,并提高了查询效率。通过使用中间表,我们能够避免多表 JOIN 操作所带来的性能开销,使查询更加高效且易于维护。然而,在实际业务中,涉及的表可能不止两个,可能有几个甚至十几个表。在这种情况下,中间表的数据同步更新就成为一个重要的问题。接下来,我们将详细探讨如何解决这个问题。

观察者模式的应用

观察者模式简介

观察者模式是一种行为设计模式,用于建立一种对象之间的一对多依赖关系。当一个对象(称为“主题”)的状态发生变化时,它的所有依赖对象(称为“观察者”)都会被自动通知并更新。这种模式通常用于实现事件驱动的系统和解耦的设计。

基本概念包括:

主题:状态发生变化的对象。它维护一个观察者列表,负责注册和通知观察者

观察者:对主题的状态变化做出响应的对象。当主题状态发生变化时,观察者会接收到通知并执行相应的操作

通知机制:当主题的状态变化时,通知所有注册的观察者,以便它们可以进行相应的更新操作

解决数据同步问题

在我们的案例中,数据同步问题指的是如何在表 inventory 或表 goods 的数据发生变化时,将这些变化同步到中间表 inventory_goods。为了解决这个问题,我们可以使用观察者模式来实现数据的自动同步。

设计主题和观察者

主题:表 inventory 和表 goods,它们是数据变化的源

观察者:中间表 inventory_goods,它需要同步更新以反映表 inventory 和表 goods 的最新数据

实现思路

定义事件类:定义一个自定义事件类,可以包含 inventory 和表 goods 表的相关数据

在服务类实现触发事件方法:在 inventory 和表 goods 表的服务类中,触发事件以便通知监听器

实现事件监听器:创建一个事件监听器,处理 ObserverEvent 并同步更新 inventory_goods

具体实现

自定义事件类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author -侑枫
* @date 2024/8/15 1:13:13
*/
@Getter
public class ObserverEvent extends ApplicationEvent {
private final Object data;
private final String tableName;

public ObserverEvent(Object source, Object data, String tableName) {
super(source);
this.data = data;
this.tableName = tableName;
}

}
GoodsServiceImpl 服务实现类
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
/**
* <p>
* 商品表 服务实现类
* </p>
*
* @author 侑枫
* @since 2024-08-15 00:21:56
*/
@Service
@RequiredArgsConstructor(onConstructor = @__(@Lazy))
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements IGoodsService {

private final ApplicationEventPublisher eventPublisher;

@Override
public void updateGoods(Goods goods) {

/**
* 做了一些修改
*/
this.updateById(goods);

eventPublisher.publishEvent(new ObserverEvent(this, goods, Goods.class.getSimpleName()));
}

}
InventoryServiceImpl 服务实现类
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
/**
* <p>
* 库存表 服务实现类
* </p>
*
* @author 侑枫
* @since 2024-08-15 00:21:56
*/
@Service
@RequiredArgsConstructor(onConstructor = @__(@Lazy))
public class InventoryServiceImpl extends ServiceImpl<InventoryMapper, Inventory> implements IInventoryService {

private final ApplicationEventPublisher eventPublisher;

@Override
public void updateInventory(Inventory inventory) {

/**
* 做了一些修改
*/
this.updateById(inventory);

eventPublisher.publishEvent(new ObserverEvent(this, inventory, Inventory.class.getSimpleName()));
}

}
IInventoryGoodsService 服务接口、服务实现类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* <p>
* 库存与商品的中间表,包含库存和商品的所有信息 服务类
* </p>
*
* @author 侑枫
* @since 2024-08-15 00:21:56
*/
public interface IInventoryGoodsService extends IService<InventoryGoods> {

/**
*
* @param goods
*/
@Async
void syncUpdateByGoods(Object goods);

/**
*
* @param inventory
*/
@Async
void syncUpdateByInventory(Object inventory);
}
这里使用了 @Async 注解,需在配置类添加 @EnableAsync 启动异步支持,且建议自行实现线程池而非使用默认的 SimpleAsyncTaskExecutor(当然如不需要也可以去掉 @Async)
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
/**
* <p>
* 库存与商品的中间表,包含库存和商品的所有信息 服务实现类
* </p>
*
* @author 侑枫
* @since 2024-08-15 00:21:56
*/
@Service
public class InventoryGoodsServiceImpl extends ServiceImpl<InventoryGoodsMapper, InventoryGoods> implements IInventoryGoodsService {

@Override
public void syncUpdateByGoods(Object goods) {
if (goods instanceof List && ObjectUtils.isNotEmpty (goods) && ((List<?>) goods).get(0) instanceof Goods) {
updateGoodsBatch((List<Goods>) goods);
} else if (goods instanceof Goods) {
updateGoods((Goods) goods);
}
}

@Override
public void syncUpdateByInventory(Object inventory) {
if (inventory instanceof List && ObjectUtils.isNotEmpty (inventory) && ((List<?>) inventory).get(0) instanceof Inventory) {
updateInventoryBatch((List<Inventory>) inventory);
} else if (inventory instanceof Inventory) {
updateInventory((Inventory) inventory);
}
}

private void updateGoodsBatch(List<Goods> goodsList) {
goodsList.forEach(this::updateGoods);
}

private void updateInventoryBatch(List<Inventory> inventoryList) {
inventoryList.forEach(this::updateInventory);
}

private void updateGoods(Goods data) {
lambdaUpdate().eq(InventoryGoods::getGoodsId, data.getId())
.set(InventoryGoods::getGoodsName, data.getName())
.set(InventoryGoods::getGoodsStatus, data.getStatus())
.set(InventoryGoods::getGoodsDescription, data.getDescription())
.set(InventoryGoods::getGoodsSpecification, data.getSpecification())
.update();
}

private void updateInventory(Inventory data) {
lambdaUpdate()
.eq(InventoryGoods::getInventoryId, data.getId())
.set(InventoryGoods::getQuantity, data.getQuantity())
.update();
}

}
实现事件监听器
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
/**
* @author -侑枫
* @date 2024/8/15 1:24:33
*/
@Component
@RequiredArgsConstructor(onConstructor = @__(@Lazy))
public class ObserverListener implements ApplicationListener<ObserverEvent> {

private final IInventoryGoodsService iInventoryGoodsService;

/**
* 本来打算用 switch 但很遗憾 case 只能接常量没办法接 xxx.class.getSimpleName() 来实现动态调度
* 用 if else-if 我只能说丑拒
* 所以用一个 map 映射表明来处理
*/
private final Map<String, Consumer<Object>> handlers = new HashMap<>();

@PostConstruct
public void init() {
handlers.put(Inventory.class.getSimpleName(), iInventoryGoodsService::syncUpdateByInventory);
handlers.put(Goods.class.getSimpleName(), iInventoryGoodsService::syncUpdateByGoods);
}

@Override
public void onApplicationEvent(ObserverEvent event) {
String tableName = event.getTableName();
Object data = event.getData();

Consumer<Object> handler = handlers.get(tableName);
if (ObjectUtils.isEmpty(handler)) {
return;
}

handler.accept(data);
}
}
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
/**
* @author -侑枫
* @date 2024/8/15 1:24:33
*/
@Component
@RequiredArgsConstructor(onConstructor = @__(@Lazy))
public class ObserverListener implements ApplicationListener<ObserverEvent> {

private final IInventoryGoodsService iInventoryGoodsService;

/**
* 本来打算用 switch 但很遗憾 case 只能接常量没办法接 xxx.class.getSimpleName() 来实现动态调度
* 用 if else-if 我只能说丑拒
* 所以用一个 map 映射表明来处理
* 使用 Supplier 延迟了 Consumer 实例的创建
*/
private final Map<String, Supplier<Consumer<Object>>> handlers = new HashMap<>();

@PostConstruct
public void init() {
handlers.put(Inventory.class.getSimpleName(), () -> iInventoryGoodsService::syncUpdateByInventory);
handlers.put(Goods.class.getSimpleName(), () -> iInventoryGoodsService::syncUpdateByGoods);
}

@Override
public void onApplicationEvent(ObserverEvent event) {
String tableName = event.getTableName();
Object data = event.getData();

Supplier<Consumer<Object>> handlerSupplier = handlers.get(tableName);
if (ObjectUtils.isEmpty(handlerSupplier)) {
return;
}

Consumer<Object> handler = handlerSupplier.get();
if (ObjectUtils.isEmpty(handler)) {
return;
}

handler.accept(data);
}
}

性能和维护的提升

使用观察者模式后的主要性能和维护优势包括

性能改进

减少数据库查询次数:通过及时更新中间表,可以避免每次查询时进行复杂的 JOIN 操作,从而提高查询效率

自动更新:数据变化时自动通知相关的表,减少了手动同步数据的频率和复杂度

维护优势

解耦设计:观察者模式使得主题和观察者之间的依赖关系解耦,修改数据源时不需要修改中间表的实现,降低了维护成本

代码清晰:通过将数据同步逻辑集中在观察者类中,代码变得更为清晰和易于管理。系统的扩展性和可维护性也得到了提升

优化观察者模式

在上一节中,我们虽然成功地使用继承自 ApplicationEventObserverEvent 实现了观察者类,但每次需要通过观察者类来同步修改中间表时,都必须手动触发事件发布。这种方式显然繁琐、不方便且不够灵活。因此,接下来我们将通过使用拦截器、注解和 AOP 的方法来优化上一节中实现的观察者类。

自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author -侑枫
* @date 2024/8/15 2:59:56
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ObserverEventTrigger {
/**
* 要筛选的类
*/
Class<?>[] targetClasses() default {};

/**
* 要排除的类
*/
Class<?>[] excludedClasses() default {};
}
在我们的注解中,我们添加了两个可选参数,分别是 要筛选的类要排除的类,这样可以使数据的处理更加灵活

实现更新数据持有器

1
2
3
4
5
6
7
8
9
10
11
/**
* @author -侑枫
* @date 2024/8/16 0:51:24
*/
public class UpdateDataHolder {
public static final ThreadLocal<List<Object>> UPDATE_DATA = ThreadLocal.withInitial(ArrayList::new);

public static void remove() {
UPDATE_DATA.remove();
}
}
使用 ThreadLocal 存储和维护线程局部的数据列表

实现数据更新拦截器

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
/**
* @author -侑枫
* @date 2024/8/16 1:07:31
*/
@Intercepts({@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})})
public class DataUpdateInterceptor extends AbstractSqlParserHandler implements Interceptor {

@Override
@SneakyThrows
public Object intercept(Invocation invocation) {
Statement statement;
Object firstArg = invocation.getArgs()[0];
if (Proxy.isProxyClass(firstArg.getClass())) {
statement = (Statement) SystemMetaObject.forObject(firstArg).getValue("h.statement");
} else {
statement = (Statement) firstArg;
}

MetaObject stmtMetaObj = SystemMetaObject.forObject(statement);
try {
statement = (Statement) stmtMetaObj.getValue("stmt.statement");
} catch (Exception e) {
// do nothing
}
if (stmtMetaObj.hasGetter("delegate")) {
// Hikari
try {
statement = (Statement) stmtMetaObj.getValue("delegate");
} catch (Exception ignored) {
// do nothing
}
}

String originalSql = statement.toString();
originalSql = originalSql.replaceAll("[\\s]+", StringPool.SPACE);
int index = indexOfSqlStart(originalSql);
if (index > 0) {
originalSql = originalSql.substring(index);
}
System.err.println("执行SQL:" + originalSql);

StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

// 获取执行Sql
String sql = originalSql.replace("where", "WHERE");
// 使用mybatis-plus 工具解析sql获取表名
Collection<String> tables = new TableNameParser(sql).tables();
if (CollectionUtils.isEmpty(tables)) {
return invocation.proceed();
}
String tableName = tables.iterator().next();
// 使用mybatis-plus 工具根据表名找出对应的实体类
TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
Class<?> entityType = tableInfo.getEntityType();

// 查询插入数据
SqlSessionFactory sqlSessionFactory = SqlHelper.sqlSessionFactory(entityType);

// 更新操作
if (SqlCommandType.UPDATE.equals(mappedStatement.getSqlCommandType())) {
// 设置sql用于执行完后查询新数据
String selectSql = "AND " + sql.substring(sql.lastIndexOf("WHERE") + 5);

// 查询更新后的数据
Map<String, Object> map = new HashMap<>(1);
map.put(Constants.WRAPPER, Wrappers.query().eq("1", 1).last(selectSql));

SqlSession sqlSession = sqlSessionFactory.openSession();
try {
List<?> updatedData = sqlSession.selectList(tableInfo.getSqlStatement(SqlMethod.SELECT_LIST.getMethod()), map);
// 存储更新后的数据到 ThreadLocal
if (ObjectUtils.isNotEmpty(updatedData)) {
UpdateDataHolder.UPDATE_DATA.get().add(updatedData);
}
} finally {
SqlSessionUtils.closeSqlSession(sqlSession, sqlSessionFactory);
}
}

return invocation.proceed();
}

@Override
public void setProperties(Properties properties) {
}

/**
* 获取sql语句开头部分
*
* @param sql ignore
* @return ignore
*/
private int indexOfSqlStart(String sql) {
String upperCaseSql = sql.toUpperCase();
Set<Integer> set = new HashSet<>();
set.add(upperCaseSql.indexOf("SELECT "));
set.add(upperCaseSql.indexOf("UPDATE "));
set.add(upperCaseSql.indexOf("INSERT "));
set.add(upperCaseSql.indexOf("DELETE "));
set.remove(-1);
if (CollectionUtils.isEmpty(set)) {
return -1;
}
List<Integer> list = new ArrayList<>(set);
list.sort(Comparator.naturalOrder());
return list.get(0);
}
}
该方法用于拦截 SQL 更新操作并将更新的处理数据存储到 ThreadLocal

配置拦截器

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @author -侑枫
* @date 2024/8/16 1:08:48
*/
@Configuration
public class MybatisPlusConfig {

@Bean
public ConfigurationCustomizer configurationCustomizer() {
return configuration -> configuration.addInterceptor(new DataUpdateInterceptor());
}
}
将拦截器添加到 MyBatis-Plus 的配置中

实现观察者切面类

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
/**
* @author -侑枫
* @date 2024/8/15 3:00:54
*/
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Lazy))
public class ObserverAspect {

private final ApplicationEventPublisher eventPublisher;

@Pointcut("@annotation(observerEventTrigger)")
public void observerPointcut(ObserverEventTrigger observerEventTrigger) {
}

@AfterReturning(value = "observerPointcut(observerEventTrigger)", argNames = "observerEventTrigger")
public void afterDataUpdate(ObserverEventTrigger observerEventTrigger) {
Class<?>[] targetClasses = observerEventTrigger.targetClasses();
Class<?>[] excludedClasses = observerEventTrigger.excludedClasses();
List<Object> objects = UpdateDataHolder.UPDATE_DATA.get();

if (ObjectUtils.isEmpty(objects)) {
UpdateDataHolder.remove();
return;
}

// 防止更新中间表的记录也被保存进来
List<Object> finalObjects = new ArrayList<>(objects);

// 获取表名的方法缓存
Map<Class<?>, String> tableNameCache = new HashMap<>(finalObjects.size());

// 将 excludedClasses、targetClasses 转换成 Set 以加快查找速度
Set<Class<?>> excludedSet = new HashSet<>(Arrays.asList(excludedClasses));
Set<Class<?>> targetSet = new HashSet<>(Arrays.asList(targetClasses));

finalObjects.stream()
.filter(item -> item instanceof List<?>)
.map(item -> (List<?>) item)
.filter(ObjectUtils::isNotEmpty)
.forEach(list -> {
Class<?> itemType = list.get(0).getClass();
boolean isTargetClass = ObjectUtils.isEmpty(targetSet) || targetSet.contains(itemType);
boolean isExcludedClass = excludedSet.contains(itemType);

if (isTargetClass && !isExcludedClass) {
String tableName = tableNameCache.computeIfAbsent(itemType, this::getTableName);
eventPublisher.publishEvent(new ObserverEvent(this, list, tableName));
}
});

UpdateDataHolder.remove();
}


private String getTableName(Class<?> entityClass) {
return entityClass.getSimpleName();
}
}
该方法用于处理数据更新后的事件发布,即根据更新结果自动触发相应的事件处理逻辑。通过结合注解和切面,实现在数据更新后灵活的数据管理和事件发布机制。该方法采用了 stream 流的方式来处理数据,此外,还可以使用并行流或 for 循环的方式来处理,具体选择可以根据实际需求进行调整
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
/**
* @author -侑枫
* @date 2024/8/15 3:00:54
*/
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Lazy))
public class ObserverAspect {

private final ApplicationEventPublisher eventPublisher;

@Pointcut("@annotation(observerEventTrigger)")
public void observerPointcut(ObserverEventTrigger observerEventTrigger) {
}

@AfterReturning(value = "observerPointcut(observerEventTrigger)", argNames = "observerEventTrigger")
public void afterDataUpdate(ObserverEventTrigger observerEventTrigger) {
Class<?>[] targetClasses = observerEventTrigger.targetClasses();
Class<?>[] excludedClasses = observerEventTrigger.excludedClasses();
List<Object> objects = UpdateDataHolder.UPDATE_DATA.get();

if (ObjectUtils.isEmpty(objects)) {
UpdateDataHolder.remove();
return;
}

// 防止更新中间表的记录也被保存进来
List<Object> finalObjects = new ArrayList<>(objects);

// 获取表名的方法缓存
Map<Class<?>, String> tableNameCache = new HashMap<>(finalObjects.size());

// 将 excludedClasses、targetClasses 转换成 Set 以加快查找速度
Set<Class<?>> excludedSet = new HashSet<>(Arrays.asList(excludedClasses));
Set<Class<?>> targetSet = new HashSet<>(Arrays.asList(targetClasses));

for (Object item : finalObjects) {
if (item instanceof List<?>) {
List<?> list = (List<?>) item;
if (ObjectUtils.isNotEmpty(list)) {
Class<?> itemType = list.get(0).getClass();
boolean isTargetClass = ObjectUtils.isEmpty(targetSet) || targetSet.contains(itemType);
boolean isExcludedClass = excludedSet.contains(itemType);

if (isTargetClass && !isExcludedClass) {
String tableName = tableNameCache.computeIfAbsent(itemType, this::getTableName);
eventPublisher.publishEvent(new ObserverEvent(this, list, tableName));
}
}
}
}

UpdateDataHolder.remove();
}


private String getTableName(Class<?> entityClass) {
return entityClass.getSimpleName();
}
}
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
/**
* @author -侑枫
* @date 2024/8/15 3:00:54
*/
@Aspect
@Component
@RequiredArgsConstructor(onConstructor = @__(@Lazy))
public class ObserverAspect {

private final ApplicationEventPublisher eventPublisher;

@Pointcut("@annotation(observerEventTrigger)")
public void observerPointcut(ObserverEventTrigger observerEventTrigger) {
}

@AfterReturning(value = "observerPointcut(observerEventTrigger)", argNames = "observerEventTrigger")
public void afterDataUpdate(ObserverEventTrigger observerEventTrigger) {
Class<?>[] targetClasses = observerEventTrigger.targetClasses();
Class<?>[] excludedClasses = observerEventTrigger.excludedClasses();
List<Object> objects = UpdateDataHolder.UPDATE_DATA.get();

if (ObjectUtils.isEmpty(objects)) {
UpdateDataHolder.remove();
return;
}

// 防止更新中间表的记录也被保存进来
List<Object> finalObjects = new ArrayList<>(objects);

// 获取表名的方法缓存,使用 ConcurrentHashMap 确保并行流的线程安全
Map<Class<?>, String> tableNameCache = new ConcurrentHashMap<>(finalObjects.size());

// 将 excludedClasses、targetClasses 转换成 Set 以加快查找速度
Set<Class<?>> excludedSet = new HashSet<>(Arrays.asList(excludedClasses));
Set<Class<?>> targetSet = new HashSet<>(Arrays.asList(targetClasses));

finalObjects.parallelStream()
.filter(item -> item instanceof List<?>)
.map(item -> (List<?>) item)
.filter(ObjectUtils::isNotEmpty)
.forEach(list -> {
Class<?> itemType = list.get(0).getClass();
boolean isTargetClass = ObjectUtils.isEmpty(targetSet) || targetSet.contains(itemType);
boolean isExcludedClass = excludedSet.contains(itemType);

if (isTargetClass && !isExcludedClass) {
String tableName = tableNameCache.computeIfAbsent(itemType, this::getTableName);
eventPublisher.publishEvent(new ObserverEvent(this, list, tableName));
}
});

UpdateDataHolder.remove();
}


private String getTableName(Class<?> entityClass) {
return entityClass.getSimpleName();
}
}
需要注意的是,并行流涉及多线程操作,因此必须保证线程安全。在并发环境中,HashMap 不具备线程安全性,多个线程同时修改它可能导致数据损坏或不一致。而 ConcurrentHashMap 是一个线程安全的集合类,它在设计时考虑了高并发场景,通过分段锁机制提升性能,使得多个线程可以并行访问不同的段,从而减少了锁竞争。

服务实现类使用示例

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
/**
* <p>
* 商品表 服务实现类
* </p>
*
* @author 侑枫
* @since 2024-08-15 00:21:56
*/
@Service
@Transactional
@RequiredArgsConstructor(onConstructor = @__(@Lazy))
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements IGoodsService {

private final IInventoryService iInventoryService;

@Override
@ObserverEventTrigger
// @ObserverEventTrigger(targetClasses = Goods.class)
// @ObserverEventTrigger(targetClasses = {Goods.class, Inventory.class})
// @ObserverEventTrigger(excludedClasses = Goods.class)
// @ObserverEventTrigger(excludedClasses = {Goods.class, Inventory.class})
// @ObserverEventTrigger(targetClasses = Goods.class, excludedClasses = Inventory.class)
public void updateGoods() {
lambdaUpdate().eq(Goods::getId, "1234567890123456789")
.set(Goods::getName, "商品D").update();

iInventoryService.lambdaUpdate().eq(Inventory::getId, "1111111111111111111")
.set(Inventory::getQuantity, "300").update();
}

}
该案例演示了注解的基本用法。默认情况下,注解会尝试对所有更新操作触发观察者事件。你也可以使用 targetClasses 参数来指定哪些类的更新操作需要触发事件,或者使用 excludedClasses 参数来排除某些类的更新操作。targetClassesexcludedClasses 是可选参数,你可以选择不使用、单独使用其中之一,或同时使用这两个参数。通过本方案,可以有效解决灵活性不足和操作繁琐的问题。

结语

在处理复杂的多表 JOIN 操作时,采用中间表和观察者模式显著提升了系统的性能和维护性。中间表通过减少复杂的 JOIN 操作,简化了查询语句,并提升了查询效率,减少了系统的计算负担。而观察者模式则通过自动同步数据,减少了手动更新的频率,确保了数据的一致性,同时增强了系统的灵活性和扩展性。

未来,我们还可以考虑其他优化方法,如使用表分区、缓存机制和数据仓库等,进一步提升系统的性能。此外,类似的优化策略也可以应用于实时数据处理、微服务架构和大数据环境中,以解决数据同步和性能优化的问题。

  • 标题: 优化多表查询:使用中间表和观察者模式提升性能
  • 作者: HYF
  • 创建于 : 2024-08-15 00:33:18
  • 更新于 : 2024-08-18 05:22:09
  • 链接: https://youfeng.ink/observer-optimization-join-059e3ffedf44/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。