分解关联查询
Java应用层面
首先将主查询的left join拿掉,将主查询得到的逻辑外键字段通过sql的in语句查询出的列表映射为(主键:实体),然后再对主查询的列表一次循环,将需要放入的逻辑外键对应的业务字段进行set操作。
优点:
- 将主查询的sql的left join移除,避免了主查询的笛卡尔积
- 实时性高
缺点:
- 通用性不强,每个业务模块在进行此类操作时需要按需对逻辑外键的字段进行处理
- 增加了与数据库的交互次数
- 对主查询需要进行循环遍历,优化sql笛卡尔积的操作其实被下放到了for循
冗余字段
现在数据库表由于严格遵循了第三范式,即每个表不会存在有字段间接依赖主键的情况。但是随着数据库里数据不断增加,这个时候,通过两个表连接的操作就比较费力。
这个时候,我们可以尝试将客户表的username加到主业务表中,这样做的好处是,联表查询变为了单表查询。而弊端是,当尝试更新客户表信息时,我们必须记得客户表中被更新的字段里,有哪些是冗余字段,分别属于哪些表,然后加入到更新程序段中
数据库层面
触发器维护冗余字段
给客户表增加更新与插入的触发器,给触发器绑定一个复杂的存储过程实现同步数据操作。
优点:
- 数据一致性:通过触发器的自动同步,可以确保主业务表的冗余字段与客户表的数据始终保持一致。
缺点:
- 维护成本增加:当客户表的结构或字段发生变化时,需要相应的更新触发器和存储过程。且存储过程无法进行调试,当存储过程内部逻辑出现错误时,无法快速的定位错误。
应用层层面
消息队列
当客户表更新或删除时,给消息队列发送一条消息,由各个模块消费者进行消费并更新冗余字段。
优点:
- 异步处理:客户表的更新或删除不会阻塞在更新冗余字段的路上
- 解耦合:客户表只需要发送消息到消息队列,不需要关心具体的更新逻辑
缺点:
- 一致性问题:由于消息队列的异步处理特性,可能会存在一定的一致性问题,即客户表的更新与删除操作已经完成,但由于消息队列的处理延迟,冗余字段的更新可能会有一段时间的不一致。
- 需要考虑消息的版本号,如果客户表在某段时间内做了频繁更新,需要区分哪一条消息是最新的。
- 若客户表更新太频繁,会触发对应业务表大量的更新,可能会导致锁表的异常问题
定时任务
利用定时任务,减少数据更新的频率,更新的流程与消息队列的方式相同。
具体更新流程
- 创表
新建一个名为冗余字段表的数据库表,当客户表或其他冗余字段源表进行更新或删除时,异步的往此表插入或更新此表的信息。
初步规划如下:
冗余字段表
CREATE TABLE redundant_field (
redundant_key bigint PRIMARY KEY AUTO_INCREMENT,
redundant_tb varchar(32),
redundant_field_name varchar(32),
redundant_field_value varchar(32),
redundant_current_data varchar(255),
sync_status char(2),
err_msg varchar(255),
diff_module_sync_status int,
create_time date,
update_time date,
del_flag int
)
表数据如图所示,标识了冗余字段所在表
- 【redundant_tb】:冗余字段所在表
- 【redundant_fied_name】:冗余字段主键名称
- 【redundant_fied_value】:冗余字段主键值
- 【redundant_current_data】:冗余字段当前值【Json格式】
- 【sync_statas】:同步状态【枚举值】
- 【err_msg】:错误信息
- 【diff_modue_sync_status】:int值,利用位运算存放所有模块同步状态
2、每隔一定时间利用定时任务查询此条未同步或者部分同步的数据并解析,进行所有模块的更新,并在互斥代码块中将同步状态置为同步中,所有模块更新完后,将同步状态置为已同步,否则置为未同步或者部分同步。
- 数据准备
- 冗余字段未同步状态下diff_module_sync_status的值为0,其业务意义如下图
从最低位到最高位分别代表每个业务模块对于该字段行的同步状态,mysql的int值有32位,所以最高可支持32个模块的同步状态的记录
- 对于每一个冗余字段,分别开设一个定时任务。例如对于客户表的冗余字段,查询条件需要带上客户表的表名,客户表冗余字段名称,同步状态为【未同步/部分同步】
- 字段更新
- 背景假设:现在系统中只有三个模块(假设为main1,main2,main3模块)涉及到客户表的更新操作。
- 准备模块列表与模块全部刷新完毕的状态值参数
如果全部模块都更新完毕,则对应的二进制数就是(1<<模块数量) – 1,初始化此值用来判断客户表对应的冗余字段业务模块是否已全部刷新完毕。
- 状态掩码的赋值
对此列表进行for循环的同时,计算这三个模块的状态掩码。数组第一位二进制数为001,数组第二位的掩码为010,数组第三位的掩码为100
- 与操作判断对应模块是否已同步
通过掩码与冗余字段表中的diff_module_sync_status进行与操作,得到对应模块的同步情况。
如图所示,当diff_module_sync_status的值为011时,代表了main3模块没有同步,main2和main1模块已同步;由于main3模块的掩码为100,与diff_module_sync_status进行&操作后,得到的值为0。
如图所示,当模块没有被同步时,就走刷新函数
- 或操作刷新对应模块同步状态
在刷新函数处理完后,如果成功同步,就将对应模块的状态掩码与diff_module_sync_status进行|操作,将对应模块的同步状态刷新至diff_module_sync_status中,避免补偿机制触发时,对已同步成功模块的重复刷新操作。
完整代码
package lego.wing.wingframework;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.google.common.collect.Lists;
import lego.wing.wingframework.async.AsyncService;
import lego.wing.wingframework.dao.*;
import lego.wing.wingframework.entity.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
@Slf4j
@SpringBootTest
class WingFrameworkApplicationTests {
static String getRandomString(int length) {
String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
int number = random.nextInt(62);
sb.append(str.charAt(number));
}
return sb.toString();
}
@Autowired
MainTb1Dao mainTb1Dao;
@Autowired
MainTb2Dao mainTb2Dao;
@Autowired
MainTb3Dao mainTb3Dao;
@Autowired
ForeignTb1Dao foreignTb1Dao;
@Autowired
ForeignTb2Dao foreignTb2Dao;
@Autowired
ForeignTb3Dao foreignTb3Dao;
@Autowired
AsyncService asyncService;
@Autowired
RedundantFieldDao redundantFieldDao;
ExecutorService executor = Executors.newFixedThreadPool(100);
@Test
public void testSyncRedundantField() {
// 定时器拿取冗余表中【未同步】【部分同步】的字段
}
private static final String WAIT_SYNC = "00";
private static final String SYNCING = "01";
private static final String PART_SYNC = "02";
private static final String FINISH_SYNC = "03";
private static final String FOREIGN_TB1_NAME = "foreign_tb1";
private static final String FKEY1_NAME = "fkey_name";
private static final String F1KEY = "f1key";
private static final String F2KEY = "f2key";
private static final String F3KEY = "f3key";
private static final Integer HAS_DEL = 1;
private static final Integer NO_DEL = 0;
@Test
public void testUpdateRedundantTb() {
// 更新外键表1
ForeignTb1Entity newForeignTb = new ForeignTb1Entity();
ForeignTb1Entity oldForeignTb = foreignTb1Dao.selectById(200001L);
BeanUtils.copyProperties(oldForeignTb, newForeignTb);
newForeignTb.setFkeyName("new fkey name version2");
Map<String, Object> redundantData = new HashMap<>();
redundantData.put("fkey_name", newForeignTb.getFkeyName());
String jsonString = JSON.toJSON(redundantData).toString();
foreignTb1Dao.updateById(newForeignTb);
LambdaQueryWrapper<RedundantFieldEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper
.eq(RedundantFieldEntity::getRedundantTb, FOREIGN_TB1_NAME)
.eq(RedundantFieldEntity::getRedundantFieldName, F1KEY)
.eq(RedundantFieldEntity::getRedundantFieldValue, newForeignTb.getF1key())
.eq(RedundantFieldEntity::getDelFlag, NO_DEL);
RedundantFieldEntity redundantFieldEntity = redundantFieldDao.selectOne(queryWrapper);
if (Objects.isNull(redundantFieldEntity)) {
redundantFieldEntity = new RedundantFieldEntity();
redundantFieldEntity.setRedundantTb(FOREIGN_TB1_NAME);
redundantFieldEntity.setRedundantFieldName(F1KEY);
redundantFieldEntity.setRedundantFieldValue(newForeignTb.getF1key().toString());
redundantFieldEntity.setRedundantCurrentData(jsonString);
redundantFieldEntity.setSyncStatus(WAIT_SYNC);
redundantFieldEntity.setErrMsg("");
redundantFieldEntity.setDiffModuleSyncStatus(0);
redundantFieldEntity.setCreateTime(new Date());
redundantFieldEntity.setUpdateTime(new Date());
redundantFieldEntity.setDelFlag(0);
redundantFieldDao.insert(redundantFieldEntity);
} else {
redundantFieldEntity.setDiffModuleSyncStatus(0);
redundantFieldEntity.setRedundantCurrentData(jsonString);
redundantFieldEntity.setSyncStatus(WAIT_SYNC);
redundantFieldEntity.setDiffModuleSyncStatus(0);
redundantFieldDao.updateById(redundantFieldEntity);
}
}
static List<String> hasRedundantModuleList = new ArrayList<>();
static {
hasRedundantModuleList.add("main1");
hasRedundantModuleList.add("main2");
hasRedundantModuleList.add("main3");
}
static Integer allFinishStatus = (1 << hasRedundantModuleList.size()) - 1;
@Test
public void refreshAllModuleRedundantField() {
// 首先获取外键表1中未同步或者部分同步的数据
LambdaQueryWrapper<RedundantFieldEntity> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper
.eq(RedundantFieldEntity::getRedundantTb, FOREIGN_TB1_NAME)
.eq(RedundantFieldEntity::getRedundantFieldName, F1KEY)
.in(RedundantFieldEntity::getSyncStatus, Arrays.asList(WAIT_SYNC, PART_SYNC))
.eq(RedundantFieldEntity::getDelFlag, NO_DEL);
RedundantFieldEntity redundantField = redundantFieldDao.selectOne(queryWrapper);
if (Objects.isNull(redundantField)) return;
// 将此值分发给各个模块
// ① 业务1模块
Integer diffModuleSyncStatus = redundantField.getDiffModuleSyncStatus();
for (int i = 0; i < hasRedundantModuleList.size(); i++) {
// 判断此模块是否已同步完成
int currentModuleStatus = 1 << i;
/*
假设diffModuleSyncStatus为100,从右往左代表第一个模块,第二个模块,第三个模块是否同步完成的标识符
所以第一个模块的标识符掩码为 001
第二个模块的标识符掩码为 010
第三个模块的标识符掩码为 100
这三个掩码分别与diffModuleSyncStatus做与操作后,如果得到的值是本身,则代表对应模块的同步状态为已同步,如果得到的值是0,则代表未同步
同步完成之后,将掩码与diffModuleSyncStatus做或操作的结果赋值给diffModuleSyncStatus,就能将对应模块的同步状态设为已同步
*/
if ((currentModuleStatus & diffModuleSyncStatus) == 0) {
// 刷新字段
refreshDiffModuleRedundantField(hasRedundantModuleList.get(i), redundantField);
// 将掩码对应的值刷入diffModuleSyncStatus中
diffModuleSyncStatus = currentModuleStatus | diffModuleSyncStatus;
redundantField.setDiffModuleSyncStatus(diffModuleSyncStatus);
if (diffModuleSyncStatus.equals(allFinishStatus)) {
redundantField.setSyncStatus(FINISH_SYNC);
} else if (diffModuleSyncStatus > 0) {
redundantField.setSyncStatus(PART_SYNC);
}
redundantFieldDao.updateById(redundantField);
}
}
}
private void refreshDiffModuleRedundantField(String moduleType, RedundantFieldEntity redundantField) {
switch (moduleType) {
case "main1":
main1Refresh(redundantField);
break;
case "main2":
main2Refresh(redundantField);
break;
case "main3":
main3Refresh(redundantField);
break;
default:
break;
}
}
private void main3Refresh(RedundantFieldEntity redundantField) {
String redundantFieldValue = redundantField.getRedundantFieldValue();
String redundantCurrentData = redundantField.getRedundantCurrentData();
Map<String, Object> mapTypes = JSON.parseObject(redundantCurrentData);
String f1keyName = (String) mapTypes.getOrDefault(FKEY1_NAME, "");
LambdaQueryWrapper<MainTb3Entity> queryWrapper = new LambdaQueryWrapper<>();
// 查询对应字段且值对应的数据
queryWrapper
.eq(MainTb3Entity::getF1key, redundantFieldValue)
.and(childAndWrapper ->
childAndWrapper
.ne(MainTb3Entity::getF1keyName, f1keyName)
.or()
.isNull(MainTb3Entity::getF1keyName)
);
List<MainTb3Entity> selectList = mainTb3Dao.selectList(queryWrapper);
if (CollectionUtils.isNotEmpty(selectList)) {
List<Long> mainTbKeyList = selectList.stream().map(MainTb3Entity::getMainTbPkey).collect(Collectors.toList());
Map<String, Object> params = new HashMap<>();
params.put("keys", mainTbKeyList);
params.put("f1keyName", f1keyName);
mainTb3Dao.batchUpdate(params);
}
}
private void main2Refresh(RedundantFieldEntity redundantField) {
String redundantFieldValue = redundantField.getRedundantFieldValue();
String redundantCurrentData = redundantField.getRedundantCurrentData();
Map<String, Object> mapTypes = JSON.parseObject(redundantCurrentData);
String f1keyName = (String) mapTypes.getOrDefault(FKEY1_NAME, "");
LambdaQueryWrapper<MainTb2Entity> queryWrapper = new LambdaQueryWrapper<>();
// 查询对应字段且值对应的数据
queryWrapper
.eq(MainTb2Entity::getF1key, redundantFieldValue)
.and(childAndWrapper ->
childAndWrapper
.ne(MainTb2Entity::getF1keyName, f1keyName)
.or()
.isNull(MainTb2Entity::getF1keyName)
);
List<MainTb2Entity> selectList = mainTb2Dao.selectList(queryWrapper);
if (CollectionUtils.isNotEmpty(selectList)) {
List<Long> mainTbKeyList = selectList.stream().map(MainTb2Entity::getMainTbPkey).collect(Collectors.toList());
Map<String, Object> params = new HashMap<>();
params.put("keys", mainTbKeyList);
params.put("f1keyName", f1keyName);
mainTb2Dao.batchUpdate(params);
}
}
private void main1Refresh(RedundantFieldEntity redundantField) {
String redundantFieldValue = redundantField.getRedundantFieldValue();
String redundantCurrentData = redundantField.getRedundantCurrentData();
Map<String, Object> mapTypes = JSON.parseObject(redundantCurrentData);
String f1keyName = (String) mapTypes.getOrDefault(FKEY1_NAME, "");
LambdaQueryWrapper<MainTb1Entity> queryWrapper = new LambdaQueryWrapper<>();
// 查询对应字段且值对应的数据
queryWrapper
.eq(MainTb1Entity::getF1key, redundantFieldValue)
.and(childAndWrapper ->
childAndWrapper
.ne(MainTb1Entity::getF1keyName, f1keyName)
.or()
.isNull(MainTb1Entity::getF1keyName)
);
List<MainTb1Entity> selectList = mainTb1Dao.selectList(queryWrapper);
if (CollectionUtils.isNotEmpty(selectList)) {
List<Long> mainTbKeyList = selectList.stream().map(MainTb1Entity::getMainTbPkey).collect(Collectors.toList());
Map<String, Object> params = new HashMap<>();
params.put("keys", mainTbKeyList);
params.put("f1keyName", f1keyName);
mainTb1Dao.batchUpdate(params);
}
}
@Test
public void initForeignTb2Data() {
int size = 500000;
int batchSize = 10;
List<String> foreignTb2EntityList = new ArrayList<>();
// for (int i = 0; i < size; i++) {
// foreignTb2EntityList.add(getRandomString(15));
// }
for (int i = 0; i < size; i++) {
foreignTb2EntityList.add(String.valueOf(i + 1));
}
// 每 100000 条数据插入开一个线程
List<List<String>> lists = Lists.partition(foreignTb2EntityList, 10000);
for (int i = 0; i < batchSize; i++) {
CountDownLatch countDownLatch = new CountDownLatch(lists.size());
long startTime = System.currentTimeMillis();
lists.forEach(listSub -> asyncService.executeAsync(countDownLatch, listSub, mainTb3Dao));
// lists.forEach(listSub -> asyncService.executeAsync(countDownLatch, listSub, foreignTb1Dao));
// lists.forEach(listSub -> asyncService.executeAsync(countDownLatch, listSub, foreignTb2Dao));
// lists.forEach(listSub -> asyncService.executeAsync(countDownLatch, listSub, foreignTb3Dao));
try {
// 保证之前的所有的线程都执行完成,才会走下面的
countDownLatch.await();
} catch (InterruptedException e) {
log.error("阻塞异常:" + e.getMessage());
}
long endTime = System.currentTimeMillis();
log.info("共耗时:{} 秒", (endTime - startTime) / 1000);
}
}
}