测试连接redis

main
刘泽璋 2024-06-25 10:30:31 +08:00
parent 056778a476
commit 331d061358
22 changed files with 227 additions and 216 deletions

View File

@ -5,7 +5,6 @@
<database-info product="MySQL" version="5.7.44" jdbc-version="4.2" driver-name="MySQL Connector/J" driver-version="mysql-connector-java-8.0.25 (Revision: 08be9e9b4cba6aa115f9b27b215887af40b159e0)" dbms="MYSQL" exact-version="5.7.44" exact-driver-version="8.0">
<extra-name-characters>#@</extra-name-characters>
<identifier-quote-string>`</identifier-quote-string>
<jdbc-catalog-is-schema>true</jdbc-catalog-is-schema>
</database-info>
<case-sensitivity plain-identifiers="lower" quoted-identifiers="lower" />
<secret-storage>master_key</secret-storage>

View File

@ -5,7 +5,28 @@
</component>
<component name="ChangeListManager">
<list default="true" id="e53020f6-f301-415a-a26a-d22b9ac05907" name="更改" comment="集成easycode">
<change afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-common/src/main/java/com/etl/data/source/common/config/Limit.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-common/src/main/java/com/etl/data/source/common/pojo/Person.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-common/target/classes/com/etl/data/source/common/config/Limit.class" afterDir="false" />
<change afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-common/target/classes/com/etl/data/source/common/pojo/Person.class" afterDir="false" />
<change afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/src/main/java/com/etl/data/source/server/aop/LimitAspect.java" afterDir="false" />
<change afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/classes/com/etl/data/source/server/aop/LimitAspect.class" afterDir="false" />
<change afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/test-classes/com/etl/data/source/server/ElDataSourceServerApplicationTests$Person.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/dataSources.local.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/dataSources.local.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-common/src/main/java/com/etl/data/source/common/config/RedisLimit.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-common/target/classes/com/etl/data/source/common/config/RedisLimit.class" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/src/main/java/com/etl/data/source/server/aop/RedisLimitAspect.java" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/src/main/java/com/etl/data/source/server/config/RedisConfig.java" beforeDir="false" afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/src/main/java/com/etl/data/source/server/config/RedisConfig.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/src/main/java/com/etl/data/source/server/config/RedissonConfig.java" beforeDir="false" afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/src/main/java/com/etl/data/source/server/config/RedissonConfig.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/src/main/java/com/etl/data/source/server/controller/DatabaseController.java" beforeDir="false" afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/src/main/java/com/etl/data/source/server/controller/DatabaseController.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/src/test/java/com/etl/data/source/server/ElDataSourceServerApplicationTests.java" beforeDir="false" afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/src/test/java/com/etl/data/source/server/ElDataSourceServerApplicationTests.java" afterDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/classes/com/etl/data/source/server/aop/RedisLimitAspect$1.class" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/classes/com/etl/data/source/server/aop/RedisLimitAspect.class" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/classes/com/etl/data/source/server/config/RedisConfig.class" beforeDir="false" afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/classes/com/etl/data/source/server/config/RedisConfig.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/classes/com/etl/data/source/server/config/RedissonConfig.class" beforeDir="false" afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/classes/com/etl/data/source/server/config/RedissonConfig.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/classes/com/etl/data/source/server/controller/DatabaseController.class" beforeDir="false" afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/classes/com/etl/data/source/server/controller/DatabaseController.class" afterDir="false" />
<change beforePath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/test-classes/com/etl/data/source/server/ElDataSourceServerApplicationTests.class" beforeDir="false" afterPath="$PROJECT_DIR$/etl-data-source/el-data-source-server/target/test-classes/com/etl/data/source/server/ElDataSourceServerApplicationTests.class" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -62,7 +83,7 @@
"WebServerToolWindowFactoryState": "false",
"git-widget-placeholder": "main",
"jdk.selected.JAVA_MODULE": "1.8",
"last_opened_file_path": "D:/workspace/etl-cloud/etl-heihei/src/main/java",
"last_opened_file_path": "D:/workspace/etl-cloud/etl-data-source/el-data-source-common/src/main/java/com/etl/data/source/common",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
@ -72,7 +93,7 @@
"project.structure.proportion": "0.17",
"project.structure.side.proportion": "0.29885057",
"settings.editor.selected.configurable": "preferences.lookFeel",
"spring.configuration.checksum": "21940204b7449cbc44619bf9f15c4b27",
"spring.configuration.checksum": "1d871ffdc938601b3b3345b2b129e57c",
"vue.rearranger.settings.migration": "true"
},
"keyToStringList": {
@ -91,6 +112,7 @@
<recent name="com.etl.data.source.server.service" />
</key>
<key name="CopyFile.RECENT_KEYS">
<recent name="D:\workspace\etl-cloud\etl-data-source\el-data-source-common\src\main\java\com\etl\data\source\common" />
<recent name="D:\workspace\etl-cloud\etl-common\src\main" />
<recent name="D:\workspace\etl-cloud\etl-data-source\el-data-source-server\src\main" />
<recent name="D:\workspace\etl-cloud\etl-data-source\src\main" />
@ -100,6 +122,10 @@
<recent name="com.etl.data.source.server" />
<recent name="com.etl.data.source" />
</key>
<key name="CopyClassDialog.RECENTS_KEY">
<recent name="com.etl.data.source.server.service" />
<recent name="com.etl.data.source.server.service.impl" />
</key>
</component>
<component name="RunDashboard">
<option name="configurationTypes">
@ -108,7 +134,20 @@
</set>
</option>
</component>
<component name="RunManager" selected="Spring Boot.CarApplication">
<component name="RunManager" selected="应用程序.ElDataSourceServerApplicationTests">
<configuration name="ElDataSourceServerApplicationTests" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="com.etl.data.source.server.ElDataSourceServerApplicationTests" />
<module name="el-data-source-server" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="com.etl.data.source.server.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="Main" type="Application" factoryName="Application" temporary="true" nameIsGenerated="true">
<option name="MAIN_CLASS_NAME" value="com.Main" />
<module name="etl-heihei" />
@ -122,22 +161,6 @@
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="ElDataSourceServerApplicationTests.contextLoads (1)" type="JUnit" factoryName="JUnit" temporary="true" nameIsGenerated="true">
<module name="el-data-source-server" />
<extension name="coverage">
<pattern>
<option name="PATTERN" value="com.etl.data.source.server.*" />
<option name="ENABLED" value="true" />
</pattern>
</extension>
<option name="PACKAGE_NAME" value="com.etl.data.source.server" />
<option name="MAIN_CLASS_NAME" value="com.etl.data.source.server.ElDataSourceServerApplicationTests" />
<option name="METHOD_NAME" value="contextLoads" />
<option name="TEST_OBJECT" value="method" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration name="ElDataSourceServerApplicationTests.contextLoads" type="JUnit" factoryName="JUnit" temporary="true" nameIsGenerated="true">
<module name="el-data-source-server" />
<extension name="coverage">
@ -183,11 +206,11 @@
</configuration>
<recent_temporary>
<list>
<item itemvalue="应用程序.ElDataSourceServerApplicationTests" />
<item itemvalue="Spring Boot.ElDataSourceServerApplication" />
<item itemvalue="Spring Boot.CarApplication" />
<item itemvalue="应用程序.Main" />
<item itemvalue="Spring Boot.ElDataSourceServerApplication" />
<item itemvalue="JUnit.ElDataSourceServerApplicationTests.contextLoads" />
<item itemvalue="JUnit.ElDataSourceServerApplicationTests.contextLoads (1)" />
</list>
</recent_temporary>
</component>
@ -209,7 +232,8 @@
<workItem from="1719026241976" duration="9213000" />
<workItem from="1719104447484" duration="9376000" />
<workItem from="1719188994279" duration="10247000" />
<workItem from="1719215606901" duration="7883000" />
<workItem from="1719215606901" duration="11291000" />
<workItem from="1719275369526" duration="6275000" />
</task>
<task id="LOCAL-00001" summary="第一次">
<option name="closed" value="true" />
@ -235,7 +259,15 @@
<option name="project" value="LOCAL" />
<updated>1719229225799</updated>
</task>
<option name="localTasksCounter" value="4" />
<task id="LOCAL-00004" summary="集成easycode">
<option name="closed" value="true" />
<created>1719230664247</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1719230664247</updated>
</task>
<option name="localTasksCounter" value="5" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@ -257,4 +289,8 @@
<MESSAGE value="集成easycode" />
<option name="LAST_COMMIT_MESSAGE" value="集成easycode" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
</project>

View File

@ -0,0 +1,26 @@
package com.etl.data.source.common.config;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface Limit {
// 资源key
String key() default "";
// 最多访问次数
double permitsPerSecond();
// 时间
long timeout();
// 时间类型
TimeUnit timeunit() default TimeUnit.MILLISECONDS;
// 提示信息
String msg() default "系统繁忙,请稍后再试";
}

View File

@ -1,35 +0,0 @@
package com.etl.data.source.common.config;
import com.etl.data.source.common.ennum.LimitType;
import java.lang.annotation.*;
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RedisLimit {
// 资源名称
String name() default "";
// 资源key
String key() default "";
// 前缀
String prefix() default "";
// 时间
int period();
// 最多访问次数
int count();
// 类型
LimitType limitType() default LimitType.CUSTOM;
// 提示信息
String msg() default "系统繁忙,请稍后再试";
}

View File

@ -0,0 +1,23 @@
package com.etl.data.source.common.pojo;
import io.swagger.models.auth.In;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
/**
* @ClassName Persion
* @Description
* @Author ZeZhang.Liu
* @Date 2024/6/25 9:32
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private String name;
private Integer age;
}

View File

@ -0,0 +1,52 @@
package com.etl.data.source.server.aop;
import com.etl.common.exception.LimitException;
import com.etl.data.source.common.config.Limit;
import com.google.common.collect.Maps;
import com.google.common.util.concurrent.RateLimiter;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Map;
@Slf4j
@Aspect
@Component
public class LimitAspect {
private final Map<String, RateLimiter> limitMap = Maps.newConcurrentMap();
@Around("@annotation(com.etl.data.source.common.config.Limit)")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature)pjp.getSignature();
Method method = signature.getMethod();
//拿limit的注解
Limit limit = method.getAnnotation(Limit.class);
if (limit != null) {
//key作用不同的接口不同的流量控制
String key=limit.key();
RateLimiter rateLimiter;
//验证缓存是否有命中key
if (!limitMap.containsKey(key)) {
// 创建令牌桶
rateLimiter = RateLimiter.create(limit.permitsPerSecond());
limitMap.put(key, rateLimiter);
log.info("新建了令牌桶={},容量={}",key,limit.permitsPerSecond());
}
rateLimiter = limitMap.get(key);
// 拿令牌
boolean acquire = rateLimiter.tryAcquire(limit.timeout(), limit.timeunit());
// 拿不到命令,直接返回异常提示
if (!acquire) {
log.debug("令牌桶={},获取令牌失败",key);
throw new LimitException(limit.msg());
}
}
return pjp.proceed();
}
}

View File

@ -1,115 +0,0 @@
package com.etl.data.source.server.aop;
import com.etl.common.exception.LimitException;
import com.etl.data.source.common.config.RedisLimit;
import com.etl.data.source.common.ennum.LimitType;
import com.google.common.collect.ImmutableList;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Objects;
@Slf4j
@Aspect
@Configuration
public class RedisLimitAspect {
private final RedisTemplate<String, Object> redisTemplate;
public RedisLimitAspect(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Around("@annotation(com.etl.data.source.common.config.RedisLimit)")
public Object around(ProceedingJoinPoint pjp){
MethodSignature methodSignature = (MethodSignature)pjp.getSignature();
Method method = methodSignature.getMethod();
RedisLimit annotation = method.getAnnotation(RedisLimit.class);
LimitType limitType = annotation.limitType();
String name = annotation.name();
String key;
int period = annotation.period();
int count = annotation.count();
switch (limitType){
case IP:
key = getIpAddress();
break;
case CUSTOMER:
key = annotation.key();
break;
default:
key = StringUtils.upperCase(method.getName());
}
ImmutableList<String> keys = ImmutableList.of(StringUtils.join(annotation.prefix(), key));
try {
String luaScript = buildLuaScript();
DefaultRedisScript<Number> redisScript = new DefaultRedisScript<>(luaScript, Number.class);
Number number = redisTemplate.execute(redisScript, keys, count, period);
log.info("Access try count is {} for name = {} and key = {}", number, name, key);
if(number != null && number.intValue() == 1){
return pjp.proceed();
}
throw new LimitException(annotation.msg());
}catch (Throwable e){
if(e instanceof LimitException){
log.debug("令牌桶={},获取令牌失败",key);
throw new LimitException(e.getLocalizedMessage());
}
e.printStackTrace();
throw new RuntimeException("服务器异常");
}
}
public String buildLuaScript(){
return "redis.replicate_commands(); local listLen,time" +
"\nlistLen = redis.call('LLEN', KEYS[1])" +
// 不超过最大值,则直接写入时间
"\nif listLen and tonumber(listLen) < tonumber(ARGV[1]) then" +
"\nlocal a = redis.call('TIME');" +
"\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])" +
"\nelse" +
// 取出现存的最早的那个时间,和当前时间比较,看是小于时间间隔
"\ntime = redis.call('LINDEX', KEYS[1], -1)" +
"\nlocal a = redis.call('TIME');" +
"\nif a[1]*1000000+a[2] - time < tonumber(ARGV[2])*1000000 then" +
// 访问频率超过了限制返回0表示失败
"\nreturn 0;" +
"\nelse" +
"\nredis.call('LPUSH', KEYS[1], a[1]*1000000+a[2])" +
"\nredis.call('LTRIM', KEYS[1], 0, tonumber(ARGV[1])-1)" +
"\nend" +
"\nend" +
"\nreturn 1;";
}
public String getIpAddress(){
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("Proxy-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getHeader("WL-Client-IP");
}
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)){
ip = request.getRemoteAddr();
}
return ip;
}
}

View File

@ -31,19 +31,6 @@ public class RedisConfig {
return jedisPool;
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 设置序列化器(如果需要)
// 例如:
// template.setKeySerializer(new StringRedisSerializer());
// template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
// ... 其他配置
return template;
}
}

View File

@ -6,6 +6,8 @@ import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
/**
* @ClassName RedissonConfig
@ -36,4 +38,19 @@ public class RedissonConfig {
return Redisson.create(config);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 设置序列化器(如果需要)
// 例如:
// template.setKeySerializer(new StringRedisSerializer());
// template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
// ... 其他配置
return template;
}
}

View File

@ -1,7 +1,7 @@
package com.etl.data.source.server.controller;
import com.etl.common.result.Result;
import com.etl.data.source.common.config.RedisLimit;
import com.etl.data.source.common.config.Limit;
import com.etl.data.source.common.pojo.DatabaseConfig;
import com.etl.data.source.common.pojo.DatabaseRedis;
import com.etl.data.source.common.pojo.resq.ColumnInfo;
@ -36,7 +36,7 @@ public class DatabaseController {
// POST请求映射到/test-database-connection路径
@PostMapping("/testDatabaseMysql")
@ApiOperation(value = "测试数据库连接")
@RedisLimit(key = "cachingTest", count = 2, period = 2, msg = "当前排队人数较多,请稍后再试!")
@Limit(key = "testDatabaseMysql", permitsPerSecond = 1, timeout = 500, msg = "当前排队人数较多,请稍后再试!")
public Result<String> testDatabaseConnection(@Valid @RequestBody DatabaseConfig config) {
String string = databaseService.testDatabaseConnection(config);
if (string.equals("ok")) {

View File

@ -1,5 +1,6 @@
package com.etl.data.source.server;
import com.etl.data.source.common.pojo.Person;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.sql.*;
@ -7,33 +8,53 @@ import java.util.*;
@SpringBootTest
class ElDataSourceServerApplicationTests {
@Test
void contextLoads() {
// List<String> list = Arrays.asList("a", "b", "c");
// list.forEach(item -> System.out.println("Lambda方式遍历元素: "+item));
//创建一个未排序的列表
List<String> list = Arrays.asList("b", "a", "c","aa");
//使用传统方式进行排序
// Collections.sort(list, new Comparator<String>() {
// @Override
// public int compare(String o1, String o2) {
// return o1.compareTo(o2);
// }
// });
//
// System.out.println("传统方式排序结果:"+list);
// list.sort((s1,s2) -> s1.compareTo(s2));
// System.out.println("Lambda方式排序结果"+list);
//创建一个新的列表来存储过滤后的结果
// ArrayList<String> filteredLis = new ArrayList<>();
// //使用传统方式过滤
// for (String item : list) {
// if(item.startsWith("a")){
// filteredLis.add(item);
// }
// }
// //打印过滤后的列表
// System.out.println("传统过滤结果:"+filteredLis);
// 假设我们有一个简单的Person类包含姓名和年龄
static class Person {
String name;
Integer age;
Person(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
public static void main(String[] args) {
// 示例数据集
List<Person> people = new ArrayList<>();
people.add(new Person("Alice", 25));
people.add(new Person("Bob", null)); // 缺失值
people.add(new Person("Charlie", 30));
people.add(new Person("Alice", 25)); // 重复数据
// 1. 处理缺失值
List<Person> cleanedPeople = new ArrayList<>();
for (Person person : people) {
if (person.age == null) {
// 假设我们用平均值或某个默认值替换缺失的年龄
// 这里简单起见我们设置为0
person.age = 0;
}
cleanedPeople.add(person);
}
// 2. 处理重复数据
Set<Person> uniquePeople = new LinkedHashSet<>(cleanedPeople); // LinkedHashSet保持插入顺序
cleanedPeople.clear();
cleanedPeople.addAll(uniquePeople);
// 3. 数据格式化(这里只是一个简单的打印示例)
for (Person person : cleanedPeople) {
System.out.println(person);
}
}
}