20260830215003
This commit is contained in:
1 parent
47c0bf08aa
commit
d6a56eb17f
47 files changed
+682
-351
No files matched your search
+48
-10
@@ -5,6 +5,7 @@ import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.Locale;
|
||||
|
||||
@Component
|
||||
public class OrgDataSourceFactory {
|
||||
@@ -28,19 +29,33 @@ public class OrgDataSourceFactory {
|
||||
dataSource.setTestWhileIdle(poolProperties.isTestWhileIdle());
|
||||
dataSource.setTestOnBorrow(poolProperties.isTestOnBorrow());
|
||||
dataSource.setTestOnReturn(poolProperties.isTestOnReturn());
|
||||
// 连接失败快速失败,不做重试:重试没有意义,等下次请求再触发建连
|
||||
dataSource.setConnectionErrorRetryAttempts(0);
|
||||
dataSource.setBreakAfterAcquireFailure(true);
|
||||
dataSource.setFailFast(true);
|
||||
// 定期 ping 空闲连接,对抗云 NAT/防火墙回收空闲 TCP 会话(默认 120s)
|
||||
dataSource.setKeepAlive(true);
|
||||
dataSource.setKeepAliveBetweenTimeMillis(120_000);
|
||||
dataSource.setTimeBetweenEvictionRunsMillis(
|
||||
poolProperties.getTimeBetweenEvictionRuns().toMillis()
|
||||
);
|
||||
// 建连失败后必须继续重试,绝不能停掉建连线程:
|
||||
// breakAfterAcquireFailure=true 会让 Druid 的 CreateConnectionThread 直接 return,
|
||||
// 这个池从此再也建不出连接,failContinuous 也永远不会被重置 -> 只能重启进程
|
||||
dataSource.setConnectionErrorRetryAttempts(1);
|
||||
dataSource.setBreakAfterAcquireFailure(false);
|
||||
dataSource.setTimeBetweenConnectErrorMillis(2_000);
|
||||
// failFast=true 时,只要 failContinuous 被置位,getConnection 会直接抛
|
||||
// DataSourceNotAvailableException(缓存的第一次 createError),不再尝试建连,
|
||||
// 即使网络早已恢复也一直报错。快速失败语义由 initial-size + init() 抛异常保证
|
||||
dataSource.setFailFast(false);
|
||||
// 公网链路必须有握手/读超时上限:缺 socketTimeout 时一次卡死的查询会长期占满 maxActive
|
||||
dataSource.setConnectionProperties(buildConnectionProperties(config.url()));
|
||||
// 连接最长存活 30 分钟,主动轮换,避开 NAT/防火墙静默回收空闲 TCP 会话
|
||||
dataSource.setPhyTimeoutMillis(1_800_000);
|
||||
// 兜底连接泄漏:maxActive 只有 10 条,泄漏几小时就足以拖垮整个机构
|
||||
dataSource.setRemoveAbandoned(true);
|
||||
dataSource.setRemoveAbandonedTimeoutMillis(180_000);
|
||||
dataSource.setLogAbandoned(true);
|
||||
long timeBetweenEvictionRunsMillis = poolProperties.getTimeBetweenEvictionRuns().toMillis();
|
||||
dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
|
||||
dataSource.setMinEvictableIdleTimeMillis(
|
||||
poolProperties.getMinEvictableIdleTime().toMillis()
|
||||
);
|
||||
// 定期 ping 空闲连接,对抗云 NAT/防火墙回收空闲 TCP 会话(需配合 min-idle >= 1 才生效)
|
||||
// Druid 要求 keepAliveBetweenTimeMillis 严格大于 timeBetweenEvictionRunsMillis
|
||||
dataSource.setKeepAlive(true);
|
||||
dataSource.setKeepAliveBetweenTimeMillis(Math.max(120_000L, timeBetweenEvictionRunsMillis * 2));
|
||||
|
||||
try {
|
||||
dataSource.init();
|
||||
@@ -50,4 +65,27 @@ public class OrgDataSourceFactory {
|
||||
throw new IllegalStateException("初始化机构数据库连接池失败: " + orgId, exception);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SQL Server JDBC 的超时属性单位不统一:connectTimeout / loginTimeout 为秒,socketTimeout 为毫秒。
|
||||
* 机构配置里已显式声明的属性不覆盖。
|
||||
*/
|
||||
private String buildConnectionProperties(String url) {
|
||||
String lowerUrl = url.toLowerCase(Locale.ROOT);
|
||||
StringBuilder builder = new StringBuilder();
|
||||
appendIfAbsent(builder, lowerUrl, "loginTimeout", "loginTimeout=10");
|
||||
appendIfAbsent(builder, lowerUrl, "connectTimeout", "connectTimeout=10");
|
||||
appendIfAbsent(builder, lowerUrl, "socketTimeout", "socketTimeout=60000");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void appendIfAbsent(StringBuilder builder, String lowerUrl, String key, String property) {
|
||||
if (lowerUrl.contains(key.toLowerCase(Locale.ROOT))) {
|
||||
return;
|
||||
}
|
||||
if (builder.length() > 0) {
|
||||
builder.append(';');
|
||||
}
|
||||
builder.append(property);
|
||||
}
|
||||
}
|
||||
@@ -452,7 +452,7 @@ public class DataSaveService {
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
try {
|
||||
switch (action) {
|
||||
case "delete" -> dbUtils.delete(connection, table, keyColumn, rows.get(index));
|
||||
case "delete" -> dbUtils.delete(connection, table, rows.get(index));
|
||||
case "update" -> dbUtils.update(connection, table, keyColumn, rows.get(index));
|
||||
case "insert" -> dbUtils.insert(connection, table, keyColumn, rows.get(index));
|
||||
default -> throw new IllegalArgumentException("不支持的保存操作: " + action);
|
||||
|
||||
@@ -316,17 +316,26 @@ public class DbUtils {
|
||||
return executeWrite(connection, sql, parameters, "修改");
|
||||
}
|
||||
|
||||
/**
|
||||
* 按行内全部列值删除:列值组合为 AND 条件({b_id: x} 即按主键删,
|
||||
* {b_module_id: m} 即按外键批量删)。值为 null 的条件按 SQL 语义匹配不到任何行。
|
||||
* 行内没有表中的列时拒绝执行,避免无 WHERE 条件的全表删除。
|
||||
*/
|
||||
public int delete(
|
||||
Connection connection,
|
||||
TableMetadata table,
|
||||
ColumnMetadata keyColumn,
|
||||
Map<String, Object> row
|
||||
) throws SQLException {
|
||||
Map<String, Object> normalizedRow = normalizeRow(row);
|
||||
Object keyValue = requiredKeyValue(normalizedRow, keyColumn, table.requestedName());
|
||||
String sql = "DELETE FROM " + table.quotedName()
|
||||
+ " WHERE " + quoteColumn(keyColumn) + " = ?";
|
||||
return executeDelete(connection, sql, List.of(keyValue));
|
||||
LinkedHashMap<ColumnMetadata, Object> conditions = writableValues(table, normalizedRow, null);
|
||||
if (conditions.isEmpty()) {
|
||||
throw new BusinessException("删除条件不能为空: " + table.requestedName());
|
||||
}
|
||||
String whereSql = conditions.keySet().stream()
|
||||
.map(column -> quoteColumn(column) + " = ?")
|
||||
.collect(Collectors.joining(" AND "));
|
||||
String sql = "DELETE FROM " + table.quotedName() + " WHERE " + whereSql;
|
||||
return executeDelete(connection, sql, new ArrayList<>(conditions.values()));
|
||||
}
|
||||
|
||||
private int executeDelete(
|
||||
|
||||
@@ -20,15 +20,19 @@ fms:
|
||||
pool:
|
||||
# 建池时先建 1 条物理连接:配置指向不可达的库时快速失败,保留旧池不切换
|
||||
initial-size: 1
|
||||
min-idle: 0
|
||||
# 保留 1 条温连接:min-idle=0 时 keepAlive 无连接可保活,空闲连接被回收后每次请求都要跨公网重新建连
|
||||
min-idle: 1
|
||||
max-active: 10
|
||||
max-wait: 5000
|
||||
# 公网建连本身可能超过 5s(握手上限 10s),池空时需留够等待时间
|
||||
max-wait: 10000
|
||||
validation-query: SELECT 1
|
||||
test-while-idle: true
|
||||
test-on-borrow: true
|
||||
# 每次借连接都做一次跨公网 SELECT 1,既慢又放大失败面,由 testWhileIdle + keepAlive 兜底
|
||||
test-on-borrow: false
|
||||
test-on-return: false
|
||||
time-between-eviction-runs: 30s
|
||||
min-evictable-idle-time: 30s
|
||||
time-between-eviction-runs: 60s
|
||||
# 原 30s:业务稍一空闲连接就被销毁,每个请求都要重新建连,公网建连超时概率被大幅放大
|
||||
min-evictable-idle-time: 10m
|
||||
file:
|
||||
storage-type: local
|
||||
local:
|
||||
|
||||
@@ -140,6 +140,43 @@ class DataSaveServiceTests {
|
||||
verify(fixture.connection).commit();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteCombinesRowColumnsIntoAndConditions() throws Exception {
|
||||
JdbcFixture fixture = new JdbcFixture("b_id", "b_module_id");
|
||||
DataSaveService service = fixture.createService();
|
||||
PreparedStatement andDeleteStatement = mock(PreparedStatement.class);
|
||||
when(fixture.connection.prepareStatement(
|
||||
"DELETE FROM [b_user] WHERE [b_id] = ? AND [b_module_id] = ?"
|
||||
)).thenReturn(andDeleteStatement);
|
||||
when(andDeleteStatement.executeUpdate()).thenReturn(2);
|
||||
|
||||
service.save(List.of(Map.of(
|
||||
"table", "b_user",
|
||||
"key_field", "b_id",
|
||||
"deletes", List.of(Map.of("b_id", "D001", "b_module_id", "M001"))
|
||||
)));
|
||||
|
||||
verify(andDeleteStatement).setObject(1, "D001");
|
||||
verify(andDeleteStatement).setObject(2, "M001");
|
||||
verify(fixture.connection).commit();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteRejectsRowWithoutTableColumns() throws Exception {
|
||||
JdbcFixture fixture = new JdbcFixture("b_id", "b_name");
|
||||
DataSaveService service = fixture.createService();
|
||||
|
||||
assertThatThrownBy(() -> service.save(List.of(Map.of(
|
||||
"table", "b_user",
|
||||
"key_field", "b_id",
|
||||
"deletes", List.of(Map.of("b_unknown", "x"))
|
||||
)))).isInstanceOf(BusinessException.class)
|
||||
.hasMessageContaining("删除条件不能为空");
|
||||
|
||||
verify(fixture.connection).rollback();
|
||||
verify(fixture.connection, org.mockito.Mockito.never()).commit();
|
||||
}
|
||||
|
||||
private static final class JdbcFixture {
|
||||
|
||||
private final DataSource dataSource = mock(DataSource.class);
|
||||
|
||||
Reference in new issue
Block a user