feat: P3任务实施 - Sentinel熔断降级/HikariCP优化/AES加密
## 新增功能 ### D.2 Sentinel熔断降级 - SentinelConfig: 自定义熔断降级响应 - SentinelRuleConfig: 网关流控规则配置 - 添加sentinel依赖到fund-gateway ### F.2 HikariCP连接池优化 - 完善HikariCP配置参数 - HikariMonitorConfig: 连接池监控配置 - 每5分钟打印连接池状态 ### H.3 敏感数据加密 - AESUtils: AES-256-GCM加密工具类 - 支持加密/解密/手机号脱敏/身份证脱敏/银行卡脱敏 - 使用SHA-256生成密钥 ### 单元测试 - AESUtilsTest: 10个测试用例验证加密功能 - 测试覆盖: 加密解密/中文/长文本/错误密钥/多次加密/脱敏
This commit is contained in:
parent
5d4cdd5c33
commit
23c8f81ebd
@ -49,6 +49,13 @@
|
|||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Test -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@ -0,0 +1,201 @@
|
|||||||
|
package com.fundplatform.common.util;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.crypto.Cipher;
|
||||||
|
import javax.crypto.KeyGenerator;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Base64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES 加密工具类
|
||||||
|
* 用于敏感数据加密存储(手机号、身份证号等)
|
||||||
|
*/
|
||||||
|
public class AESUtils {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(AESUtils.class);
|
||||||
|
|
||||||
|
private static final String ALGORITHM = "AES";
|
||||||
|
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
||||||
|
private static final int KEY_SIZE = 256; // AES-256
|
||||||
|
private static final int GCM_IV_LENGTH = 12; // GCM推荐的IV长度
|
||||||
|
private static final int GCM_TAG_LENGTH = 128; // 认证标签长度(位)
|
||||||
|
|
||||||
|
// 默认密钥种子(生产环境应从配置中心或密钥管理服务获取)
|
||||||
|
// 通过SHA-256生成32字节的密钥
|
||||||
|
private static final String DEFAULT_KEY_SEED = "FundPlatform2026SecretKey!@#";
|
||||||
|
private static final byte[] DEFAULT_KEY;
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
DEFAULT_KEY = digest.digest(DEFAULT_KEY_SEED.getBytes(StandardCharsets.UTF_8));
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("初始化默认密钥失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成AES密钥
|
||||||
|
*/
|
||||||
|
public static String generateKey() {
|
||||||
|
try {
|
||||||
|
KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM);
|
||||||
|
keyGenerator.init(KEY_SIZE, new SecureRandom());
|
||||||
|
SecretKey secretKey = keyGenerator.generateKey();
|
||||||
|
return Base64.getEncoder().encodeToString(secretKey.getEncoded());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("生成AES密钥失败", e);
|
||||||
|
throw new RuntimeException("生成AES密钥失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密
|
||||||
|
* @param plainText 明文
|
||||||
|
* @return Base64编码的密文(包含IV)
|
||||||
|
*/
|
||||||
|
public static String encrypt(String plainText) {
|
||||||
|
return encryptWithKey(plainText, DEFAULT_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加密
|
||||||
|
* @param plainText 明文
|
||||||
|
* @param key 密钥(Base64编码)
|
||||||
|
* @return Base64编码的密文(包含IV)
|
||||||
|
*/
|
||||||
|
public static String encrypt(String plainText, String key) {
|
||||||
|
try {
|
||||||
|
byte[] keyBytes = Base64.getDecoder().decode(key);
|
||||||
|
return encryptWithKey(plainText, keyBytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析密钥失败", e);
|
||||||
|
throw new RuntimeException("解析密钥失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部加密方法
|
||||||
|
*/
|
||||||
|
private static String encryptWithKey(String plainText, byte[] keyBytes) {
|
||||||
|
try {
|
||||||
|
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
|
||||||
|
|
||||||
|
// 生成随机IV
|
||||||
|
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||||
|
SecureRandom random = new SecureRandom();
|
||||||
|
random.nextBytes(iv);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||||
|
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
|
||||||
|
|
||||||
|
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
// 将IV和密文合并
|
||||||
|
byte[] encryptedWithIv = new byte[GCM_IV_LENGTH + encrypted.length];
|
||||||
|
System.arraycopy(iv, 0, encryptedWithIv, 0, GCM_IV_LENGTH);
|
||||||
|
System.arraycopy(encrypted, 0, encryptedWithIv, GCM_IV_LENGTH, encrypted.length);
|
||||||
|
|
||||||
|
return Base64.getEncoder().encodeToString(encryptedWithIv);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AES加密失败", e);
|
||||||
|
throw new RuntimeException("AES加密失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密
|
||||||
|
* @param encryptedText Base64编码的密文(包含IV)
|
||||||
|
* @return 明文
|
||||||
|
*/
|
||||||
|
public static String decrypt(String encryptedText) {
|
||||||
|
return decryptWithKey(encryptedText, DEFAULT_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密
|
||||||
|
* @param encryptedText Base64编码的密文(包含IV)
|
||||||
|
* @param key 密钥(Base64编码)
|
||||||
|
* @return 明文
|
||||||
|
*/
|
||||||
|
public static String decrypt(String encryptedText, String key) {
|
||||||
|
try {
|
||||||
|
byte[] keyBytes = Base64.getDecoder().decode(key);
|
||||||
|
return decryptWithKey(encryptedText, keyBytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("解析密钥失败", e);
|
||||||
|
throw new RuntimeException("解析密钥失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内部解密方法
|
||||||
|
*/
|
||||||
|
private static String decryptWithKey(String encryptedText, byte[] keyBytes) {
|
||||||
|
try {
|
||||||
|
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, ALGORITHM);
|
||||||
|
|
||||||
|
byte[] decoded = Base64.getDecoder().decode(encryptedText);
|
||||||
|
|
||||||
|
// 分离IV和密文
|
||||||
|
byte[] iv = new byte[GCM_IV_LENGTH];
|
||||||
|
byte[] encrypted = new byte[decoded.length - GCM_IV_LENGTH];
|
||||||
|
System.arraycopy(decoded, 0, iv, 0, GCM_IV_LENGTH);
|
||||||
|
System.arraycopy(decoded, GCM_IV_LENGTH, encrypted, 0, encrypted.length);
|
||||||
|
|
||||||
|
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||||
|
GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
|
||||||
|
|
||||||
|
byte[] decrypted = cipher.doFinal(encrypted);
|
||||||
|
return new String(decrypted, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("AES解密失败", e);
|
||||||
|
throw new RuntimeException("AES解密失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号脱敏
|
||||||
|
* @param phone 手机号
|
||||||
|
* @return 脱敏后的手机号 (如: 138****8000)
|
||||||
|
*/
|
||||||
|
public static String maskPhone(String phone) {
|
||||||
|
if (phone == null || phone.length() < 7) {
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 身份证号脱敏
|
||||||
|
* @param idCard 身份证号
|
||||||
|
* @return 脱敏后的身份证号 (如: 110***********1234)
|
||||||
|
*/
|
||||||
|
public static String maskIdCard(String idCard) {
|
||||||
|
if (idCard == null || idCard.length() < 8) {
|
||||||
|
return idCard;
|
||||||
|
}
|
||||||
|
return idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 银行卡号脱敏
|
||||||
|
* @param bankCard 银行卡号
|
||||||
|
* @return 脱敏后的银行卡号 (如: 6222****1234)
|
||||||
|
*/
|
||||||
|
public static String maskBankCard(String bankCard) {
|
||||||
|
if (bankCard == null || bankCard.length() < 8) {
|
||||||
|
return bankCard;
|
||||||
|
}
|
||||||
|
return bankCard.substring(0, 4) + "****" + bankCard.substring(bankCard.length() - 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,161 @@
|
|||||||
|
package com.fundplatform.common.util;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AES加密工具类单元测试
|
||||||
|
*/
|
||||||
|
class AESUtilsTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGenerateKey() {
|
||||||
|
String key = AESUtils.generateKey();
|
||||||
|
assertNotNull(key);
|
||||||
|
assertTrue(key.length() > 0);
|
||||||
|
System.out.println("生成的密钥: " + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEncryptAndDecrypt() {
|
||||||
|
String plainText = "13800138000";
|
||||||
|
|
||||||
|
// 加密
|
||||||
|
String encrypted = AESUtils.encrypt(plainText);
|
||||||
|
assertNotNull(encrypted);
|
||||||
|
assertNotEquals(plainText, encrypted);
|
||||||
|
System.out.println("明文: " + plainText);
|
||||||
|
System.out.println("密文: " + encrypted);
|
||||||
|
|
||||||
|
// 解密
|
||||||
|
String decrypted = AESUtils.decrypt(encrypted);
|
||||||
|
assertEquals(plainText, decrypted);
|
||||||
|
System.out.println("解密后: " + decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEncryptAndDecryptWithCustomKey() {
|
||||||
|
String plainText = "320123199001011234";
|
||||||
|
String key = AESUtils.generateKey();
|
||||||
|
|
||||||
|
// 使用自定义密钥加密
|
||||||
|
String encrypted = AESUtils.encrypt(plainText, key);
|
||||||
|
assertNotNull(encrypted);
|
||||||
|
|
||||||
|
// 使用自定义密钥解密
|
||||||
|
String decrypted = AESUtils.decrypt(encrypted, key);
|
||||||
|
assertEquals(plainText, decrypted);
|
||||||
|
System.out.println("身份证加密测试通过: " + plainText + " -> " + encrypted + " -> " + decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEncryptAndDecryptChinese() {
|
||||||
|
String plainText = "张三丰";
|
||||||
|
|
||||||
|
String encrypted = AESUtils.encrypt(plainText);
|
||||||
|
String decrypted = AESUtils.decrypt(encrypted);
|
||||||
|
|
||||||
|
assertEquals(plainText, decrypted);
|
||||||
|
System.out.println("中文加密测试通过: " + plainText + " -> " + encrypted + " -> " + decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testEncryptAndDecryptLongText() {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
sb.append("测试数据");
|
||||||
|
}
|
||||||
|
String plainText = sb.toString();
|
||||||
|
|
||||||
|
String encrypted = AESUtils.encrypt(plainText);
|
||||||
|
String decrypted = AESUtils.decrypt(encrypted);
|
||||||
|
|
||||||
|
assertEquals(plainText, decrypted);
|
||||||
|
System.out.println("长文本加密测试通过, 明文长度: " + plainText.length() + ", 密文长度: " + encrypted.length());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMaskPhone() {
|
||||||
|
// 正常手机号(11位)
|
||||||
|
assertEquals("138****8000", AESUtils.maskPhone("13800138000"));
|
||||||
|
|
||||||
|
// 短号码(小于7位)不脱敏
|
||||||
|
assertEquals("123456", AESUtils.maskPhone("123456"));
|
||||||
|
|
||||||
|
// null
|
||||||
|
assertNull(AESUtils.maskPhone(null));
|
||||||
|
|
||||||
|
// 7位号码(前3位+****+后4位)
|
||||||
|
assertEquals("123****4567", AESUtils.maskPhone("1234567"));
|
||||||
|
|
||||||
|
System.out.println("手机号脱敏测试通过: 13800138000 -> " + AESUtils.maskPhone("13800138000"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMaskIdCard() {
|
||||||
|
// 正常身份证号(18位)
|
||||||
|
assertEquals("320***********1234", AESUtils.maskIdCard("320123199001011234"));
|
||||||
|
|
||||||
|
// 短号码(小于8位)直接返回原值
|
||||||
|
assertEquals("1234567", AESUtils.maskIdCard("1234567"));
|
||||||
|
|
||||||
|
// null
|
||||||
|
assertNull(AESUtils.maskIdCard(null));
|
||||||
|
|
||||||
|
System.out.println("身份证脱敏测试通过: 320123199001011234 -> " + AESUtils.maskIdCard("320123199001011234"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMaskBankCard() {
|
||||||
|
// 正常银行卡号
|
||||||
|
assertEquals("6222****1234", AESUtils.maskBankCard("62220212345678901234"));
|
||||||
|
|
||||||
|
// 短卡号(小于8位)直接返回原值
|
||||||
|
assertEquals("1234567", AESUtils.maskBankCard("1234567"));
|
||||||
|
|
||||||
|
// null
|
||||||
|
assertNull(AESUtils.maskBankCard(null));
|
||||||
|
|
||||||
|
// 8位卡号(前4位+****+后4位)
|
||||||
|
assertEquals("1234****5678", AESUtils.maskBankCard("12345678"));
|
||||||
|
|
||||||
|
System.out.println("银行卡脱敏测试通过: 62220212345678901234 -> " + AESUtils.maskBankCard("62220212345678901234"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testDecryptWithWrongKey() {
|
||||||
|
String plainText = "敏感数据";
|
||||||
|
String key1 = AESUtils.generateKey();
|
||||||
|
String key2 = AESUtils.generateKey();
|
||||||
|
|
||||||
|
String encrypted = AESUtils.encrypt(plainText, key1);
|
||||||
|
|
||||||
|
// 使用错误的密钥解密应该失败
|
||||||
|
assertThrows(RuntimeException.class, () -> {
|
||||||
|
AESUtils.decrypt(encrypted, key2);
|
||||||
|
});
|
||||||
|
|
||||||
|
System.out.println("错误密钥解密测试通过: 使用错误密钥解密时抛出异常");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMultipleEncryptionsAreDifferent() {
|
||||||
|
String plainText = "相同的数据";
|
||||||
|
|
||||||
|
// 多次加密同一明文,密文应该不同(因为使用了随机IV)
|
||||||
|
String encrypted1 = AESUtils.encrypt(plainText);
|
||||||
|
String encrypted2 = AESUtils.encrypt(plainText);
|
||||||
|
String encrypted3 = AESUtils.encrypt(plainText);
|
||||||
|
|
||||||
|
assertNotEquals(encrypted1, encrypted2);
|
||||||
|
assertNotEquals(encrypted2, encrypted3);
|
||||||
|
|
||||||
|
// 但都能正确解密
|
||||||
|
assertEquals(plainText, AESUtils.decrypt(encrypted1));
|
||||||
|
assertEquals(plainText, AESUtils.decrypt(encrypted2));
|
||||||
|
assertEquals(plainText, AESUtils.decrypt(encrypted3));
|
||||||
|
|
||||||
|
System.out.println("多次加密生成不同密文测试通过");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -59,6 +59,16 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
|
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Sentinel 熔断降级 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.fundplatform.gateway.config;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sentinel 熔断降级配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class SentinelConfig {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void init() {
|
||||||
|
// 自定义熔断降级响应
|
||||||
|
GatewayCallbackManager.setBlockHandler((exchange, t) -> {
|
||||||
|
Map<String, Object> result = new HashMap<>();
|
||||||
|
result.put("code", 429);
|
||||||
|
result.put("message", "服务繁忙,请稍后重试");
|
||||||
|
result.put("success", false);
|
||||||
|
result.put("data", null);
|
||||||
|
|
||||||
|
String body;
|
||||||
|
try {
|
||||||
|
body = objectMapper.writeValueAsString(result);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
body = "{\"code\":429,\"message\":\"服务繁忙,请稍后重试\",\"success\":false}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return ServerResponse
|
||||||
|
.status(HttpStatus.TOO_MANY_REQUESTS)
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.bodyValue(body);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package com.fundplatform.gateway.config;
|
||||||
|
|
||||||
|
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
|
||||||
|
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
|
||||||
|
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sentinel 网关流控规则配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class SentinelRuleConfig {
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void initRules() {
|
||||||
|
Set<GatewayFlowRule> rules = new HashSet<>();
|
||||||
|
|
||||||
|
// fund-sys服务流控规则
|
||||||
|
rules.add(new GatewayFlowRule("fund-sys")
|
||||||
|
.setCount(100) // QPS阈值
|
||||||
|
.setIntervalSec(1) // 统计窗口
|
||||||
|
.setBurst(20) // 应对突发请求额外容量
|
||||||
|
);
|
||||||
|
|
||||||
|
// fund-req服务流控规则
|
||||||
|
rules.add(new GatewayFlowRule("fund-req")
|
||||||
|
.setCount(50)
|
||||||
|
.setIntervalSec(1)
|
||||||
|
.setBurst(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// fund-exp服务流控规则
|
||||||
|
rules.add(new GatewayFlowRule("fund-exp")
|
||||||
|
.setCount(50)
|
||||||
|
.setIntervalSec(1)
|
||||||
|
.setBurst(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// fund-receipt服务流控规则
|
||||||
|
rules.add(new GatewayFlowRule("fund-receipt")
|
||||||
|
.setCount(50)
|
||||||
|
.setIntervalSec(1)
|
||||||
|
.setBurst(10)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 加载规则
|
||||||
|
GatewayRuleManager.loadRules(rules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sentinel Gateway 过滤器
|
||||||
|
*/
|
||||||
|
@Bean
|
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
|
public GlobalFilter sentinelGatewayFilter() {
|
||||||
|
return new SentinelGatewayFilter();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,6 +16,22 @@ spring:
|
|||||||
cloud:
|
cloud:
|
||||||
compatibility-verifier:
|
compatibility-verifier:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
|
# Sentinel配置
|
||||||
|
sentinel:
|
||||||
|
transport:
|
||||||
|
dashboard: localhost:8080 # Sentinel Dashboard地址(可选)
|
||||||
|
port: 8719 # Sentinel客户端端口
|
||||||
|
eager: true # 服务启动时立即初始化
|
||||||
|
datasource:
|
||||||
|
# 从Nacos读取规则(可选)
|
||||||
|
# ds1:
|
||||||
|
# nacos:
|
||||||
|
# server-addr: localhost:8848
|
||||||
|
# data-id: sentinel-gateway-rules
|
||||||
|
# group-id: DEFAULT_GROUP
|
||||||
|
# rule-type: gw-flow
|
||||||
|
|
||||||
gateway:
|
gateway:
|
||||||
# 默认限流配置
|
# 默认限流配置
|
||||||
default-filters:
|
default-filters:
|
||||||
|
|||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.fundplatform.sys.config;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfigMXBean;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HikariCP 连接池监控配置
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableScheduling
|
||||||
|
public class HikariMonitorConfig {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(HikariMonitorConfig.class);
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private DataSource dataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每5分钟打印一次连接池状态
|
||||||
|
*/
|
||||||
|
@Scheduled(fixedRate = 5, timeUnit = TimeUnit.MINUTES)
|
||||||
|
public void monitorHikariPool() {
|
||||||
|
if (dataSource instanceof HikariDataSource) {
|
||||||
|
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
|
||||||
|
HikariConfigMXBean config = hikariDataSource.getHikariConfigMXBean();
|
||||||
|
|
||||||
|
log.info("=== HikariCP 连接池状态 ===");
|
||||||
|
log.info("连接池名称: {}", config.getPoolName());
|
||||||
|
log.info("活跃连接数: {}", hikariDataSource.getHikariPoolMXBean().getActiveConnections());
|
||||||
|
log.info("空闲连接数: {}", hikariDataSource.getHikariPoolMXBean().getIdleConnections());
|
||||||
|
log.info("等待获取连接的线程数: {}", hikariDataSource.getHikariPoolMXBean().getThreadsAwaitingConnection());
|
||||||
|
log.info("最大连接数: {}", config.getMaximumPoolSize());
|
||||||
|
log.info("最小空闲连接数: {}", config.getMinimumIdle());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,9 +18,20 @@ spring:
|
|||||||
username: root
|
username: root
|
||||||
password: zjf@123456
|
password: zjf@123456
|
||||||
hikari:
|
hikari:
|
||||||
maximum-pool-size: 10
|
# 连接池大小配置
|
||||||
minimum-idle: 5
|
maximum-pool-size: 20 # 最大连接数(建议: CPU核心数*2 + 有效磁盘数)
|
||||||
connection-timeout: 30000
|
minimum-idle: 5 # 最小空闲连接数
|
||||||
|
# 连接超时配置
|
||||||
|
connection-timeout: 30000 # 连接超时时间(毫秒)
|
||||||
|
idle-timeout: 600000 # 空闲连接超时时间(10分钟)
|
||||||
|
max-lifetime: 1800000 # 连接最大存活时间(30分钟)
|
||||||
|
# 连接验证配置
|
||||||
|
validation-timeout: 5000 # 连接验证超时时间
|
||||||
|
leak-detection-threshold: 60000 # 连接泄露检测阈值(60秒)
|
||||||
|
# 连接池名称
|
||||||
|
pool-name: FundSysHikariPool
|
||||||
|
# 连接初始化SQL
|
||||||
|
connection-init-sql: SELECT 1
|
||||||
|
|
||||||
# Redis配置
|
# Redis配置
|
||||||
data:
|
data:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user